Cube.js Blog

Adding an E-Commerce Admin Dashboard to Saleor

Author avatarArtyom Keydunov/AnalyticsServerlessTutorials/January 30, 2019
Adding an E-Commerce Admin Dashboard to Saleor
Show Original

In this tutorial, we’ll explore how to add analytical features, such as reporting and dashboarding to an existing e-commerce web application with Cube.js. We’re going to use Saleor as our e-commerce platform. It is powered by a GraphQL server running on top of Python 3 and a Django 2 framework. Both the storefront and the admin dashboard are React applications written in TypeScript and powered by Apollo GraphQL. We’ll add dashboarding and reporting capabilities to the admin dashboard with the Cube.js.

Installing Saleor

Saleor server and the admin dashboard are located in a single repo , and the storefront is in the separate repo. For the purpose of the tutorial, we need only the server and the admin dashboard. It comes with very good documentation and detailed installation guide, which you can find here. Please follow the instructions on installation and them come back here.

Saleor server and the admin dashboard are located in a single repo here. It comes with very good documentation and detailed installation guide, which you can find here. Please follow the installation instructions and them come back here.

Once, it is installed, let’s populate our store with example products and orders, as well as the admin account. Run the following command in your terminal.

$ python manage.py populatedb --createsuperuser

Next, start the local server.

$ python manage.py runserver

Open http://localhost:8000 in your browser and it will show you your storefront. Since in previous step we loaded some example data, it will show you some categories and products inside. In this tutorial we’re going to work with the admin dashboard, which could be found here http://localhost:8000/dashboard/next. Notice, next in the url - Saleor is updating the admin UI and we’re going to work the latest one. To access admin dashboard enter admin@example.com for email and admin for password.

This how it should look like after you log in.

Installing Cube.js

Now, as we have Saleor up and running we can install Cube.js to start adding analytical features to our e-commerce dashboard. The Cube.js backend is a Node app, which could be run as a separate microservice or in a serverless mode. Let’s create our Cube.js backend instance.

Run the following commands in your terminal

$ npm install -g cubejs-cli
$ cubejs create saleor-analytics -d postgres

The above commands should create a new Cube.js application. Since we are using Postgres with Saleor we set it as a database type (-d). Cube.js use ‘dotenv’ to manage credentials, so we need to update .env file with our database's credentials.

# Update database credentials in the .env file

CUBEJS_DB_TYPE=postgres
CUBEJS_DB_NAME=saleor
CUBEJS_DB_USER=saleor

The next step is to tell Cube.js how to convert raw data in the database into meaningful analytics insights. This process is sometimes called data modeling. It is a way to translate business level definitions, such as customer lifetime value into SQL. In Cube.js it’s called schema and schema files are located in ‘schema’ folder.

Cube.js CLI generates an example schema file - schema/Orders.js. Change it’s content to the following.

cube(`Orders`, {
  sql: `select * from order_order`,

  measures: {
    count: {
      type: `count` 
    }
  },
    
  dimensions: {
    created: {
      type: `time`,
      sql: `created` 
    }
  }
});

Cube.js uses schema to generate a SQL code, which will run in your database and return a result back. A schema is also used to manage pre-aggregations and cache, you can learn more about it here.

That is all we need to start our first analytics server. In the project folder run:

$ npm run dev

It will spin up the development server at http://localhost:4000. If you open it in the browser it’ll show the codesandbox with an example on how to use an API with React.

Now, as we have Cube.js server up and running the next step is to add analytics section to our Saleor admin dashboard.

Add Analytics Section to Saleor Dashboard

We’re going to add analytics section to to the Saleor dashboard. The admin dashboard is a React app written in Typescript, which is located in saleor/static/dashboard-next folder. Within this folder create a new folder - analytics. it is going to contain all our code for analytics sections. First, we’ll add an analytics page, which is going to be a container for all of our charts.

// Create analytics/index.tsx with the following content

import * as React from "react";

import DashboardPage from "./views/DashboardPage";

const Component = () => <DashboardPage />;

export default Component;

// Create analytics/views/DashboardPage.tsx with the following content
import * as React from "react";

import Container from "../../components/Container";
import PageHeader from "../../components/PageHeader";
import i18n from "../../i18n";

const DashboardPage = () => (
  <Container width="md">
    <PageHeader title={i18n.t("Analytics")} />
  </Container>
);

DashboardPage.displayName = "DashboardPage";
export default DashboardPage;
// Add analytics route to index.tsx

import AnalyticsPage from "./analytics";

// Add analytics SectionRoute just after Home

<SectionRoute path="/analytics" component={AnalyticsPage} />

That should be enough to render our new blank analytics page at http://localhost:8000/dashboard/next/analytics. You can try to open it in your browser. Next, let’s add Analytics item to the left menu.

First, will add new icon component and then edit AppRoot.tsx to add menu item.

// Create icons/Analytics.tsx with the following content

import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import * as React from "react";

export const Analytics = createSvgIcon(
  <>
      <g id="Group" transform="translate(-188.000000, -237.000000)">
        <path d="M204,240 L208,240 L208,256 L204,256 Z M198,246 L202,246 L202,256 L198,256 Z M192,250 L196,250 L196,256 L192,256 Z M192,250" />
      </g>
  </>
);
Analytics.displayName = "Analytics";
export default Analytics;
// Import icon and add new element to menuStructure array in the AppRoot.tsx

import Analytics from "./icons/Analytics";

// ...

const menuStructure: IMenuItem[] = [
  // ...
  {
     ariaLabel: "analytics",
     icon: <Analytics />,
     label: i18n.t("Analytics", { context: "Menu label" }),
     url: "/analytics"
  }
]

That’s it! Open admin dashboard in your browser and you should see the new menu item. Now, let’s add some charts to our Analytics Page.

Create First Char

Let’s create a ChartCard React Component in analytics/components/ChartCard.tsx. It will receive a query as an input and will encapsulate getting data from Cube.js API and then rendering the visualization.

We need to install Cube.js javascript client with React wrapper and Bizcharts for visualizations. Bizcharts is easy to use visualization library for React apps based on G2. It comes with pretty nice default styles. Run the following commands in the Saleor root directory:

$ npm install @cubejs-client/core @cubejs-client/react bizcharts moment numeral --save

Our Cube.js server is running on http://localhost:4000 and API endpoint is located at http://localhost:4000/cubejs-api/v1, so we need to specify it in the apiUrl parameter when initializing Cube.js client instance. It also expects an API key, which you can copy from Cube.js server console output when you start it.

// Create analytics/components/ChartCard.tsx with the following content

import * as cubejs from '@cubejs-client/core';
import { QueryRenderer } from '@cubejs-client/react';
import Card from "@material-ui/core/Card";
import { Axis, Chart, Geom, Tooltip } from 'bizcharts';
import * as moment from 'moment';
import * as React from "react";

import CardTitle from "../../../components/CardTitle";
import Skeleton from "../../../components/Skeleton";

const CHART_HEIGHT = 400;
const API_KEY = 'YOUR-CUBE-JS-API-KEY';
const API_URL = 'http://localhost:4000/cubejs-api/v1';
const cubejsApi = cubejs(API_KEY, { apiUrl: API_URL });

const renderChart = (resultSet) => (
  <Chart
    scale={{ category: { tickCount: 8 } }}
    height={CHART_HEIGHT}
    data={resultSet.chartPivot()}
    forceFit
  >
      <Axis name="category" label={{ formatter: val => moment(val).format("MMM DD") }} />
      {resultSet.seriesNames().map(s => (<Axis name={s.key} />))}
      <Tooltip crosshairs={{type : 'y'}} />
      {resultSet.seriesNames().map(s => (<Geom type="line" position={`category*${s.key}`} size={2} />))}
  </Chart>
);

const ChartCard = ({ title, query }) => (
  <Card>
    <CardTitle title={title} />
    <QueryRenderer
      query={query}
      cubejsApi={cubejsApi}
      render={ ({ resultSet }) => {
        if (!resultSet) {
          return (
            <div style={{ padding: "10px" }}>
              <Skeleton />
            </div>
          )
        }

        return renderChart(resultSet);
      }}
    />
  </Card>
);

ChartCard.displayName = "ChartCard";
export default ChartCard;

Now, let’s add our first chart in DashboardPage using ChartCard component.

// Replace the content of analytics/views/DashboardPage.tsx with the following

import * as React from "react";

import CardSpacer from "../../components/CardSpacer";
import Container from "../../components/Container";
import PageHeader from "../../components/PageHeader";
import i18n from "../../i18n";

import ChartCard from "./../components/ChartCard";

const DashboardPage = () => (
  <Container width="md">
    <PageHeader title={i18n.t("Analytics")} />
    <CardSpacer />
    <ChartCard
      title={i18n.t("Orders Last 30 Days")}
      query={{
        measures: [ "Orders.count" ],
        timeDimensions: [{
          dateRange: ['2019-01-01', '2019-01-31'],
          dimension: 'Orders.created',
          granularity: 'day'
        }]
      }}
    />
  </Container>
);

DashboardPage.displayName = "DashboardPage";
export default DashboardPage;

That will render our first chart at http://localhost:8000/dashboard/next/analytics.

Add More Charts

First, let’s add support for other visualizations, such as a pie chart. The query to Cube.js will remain the same, we just need to support the pie chart in the renderChart methods. Also, we’ll add support for currency formatting using numeraljs.

// Replace the content of analytics/components/ChartCard.tsx with the following

import * as cubejs from '@cubejs-client/core';
import { QueryRenderer } from '@cubejs-client/react';
import Card from "@material-ui/core/Card";
import { Axis, Chart, Coord, Geom, Legend, Tooltip } from 'bizcharts';
import * as moment from 'moment';
import * as numeral from 'numeral';
import * as React from "react";

import CardTitle from "../../../components/CardTitle";
import Skeleton from "../../../components/Skeleton";

const CHART_HEIGHT = 400;
const API_KEY = 'YOUR-CUBE-JS-API-KEY';
const API_URL = 'http://localhost:4000/cubejs-api/v1';
const cubejsApi = cubejs(API_KEY, { apiUrl: API_URL });

const formatters = {
  currency: (val) => numeral(val).format('$0,0'),
  date: (val) => moment(val).format("MMM DD"),
  undefined: (val) => val
}
const renderLine = (resultSet) => (
  <Chart
    scale={{ category: { tickCount: 8 } }}
    height={CHART_HEIGHT}
    data={resultSet.chartPivot()}
    forceFit
  >
      <Axis name="category" label={{ formatter: formatters.date }} />
      {resultSet.seriesNames().map(s => (<Axis name={s.key} label={{ formatter: formatters[resultSet.loadResponse.annotation.measures[s.key].format] }} />))}
      <Tooltip crosshairs={{type : 'y'}} />
      {resultSet.seriesNames().map(s => (<Geom type="line" position={`category*${s.key}`} size={2} />))}
  </Chart>
);

const renderPie = (resultSet) => (
  <Chart height={CHART_HEIGHT} data={resultSet.chartPivot()} forceFit>
    <Coord type="theta" radius={0.75} />
    {resultSet.seriesNames().map(s => (<Axis name={s.key} />))}
    <Legend position="right" name="category" />
    <Tooltip showTitle={false} />
    {resultSet.seriesNames().map(s => (<Geom type="intervalStack" position={s.key} color="x" />))}
  </Chart>
);

const renderChart = (resultSet, visualizationType) => (
  {
    'line': renderLine,
    'pie': renderPie
  }[visualizationType](resultSet)
);

const ChartCard = ({ title, query, visualizationType }) => (
  <Card>
    <CardTitle title={title} />
    <QueryRenderer
      query={query}
      cubejsApi={cubejsApi}
      render={ ({ resultSet }) => {
        if (!resultSet) {
          return (
            <div style={{ padding: "10px" }}>
              <Skeleton />
            </div>
          )
        }

        return renderChart(resultSet, visualizationType);
      }}
    />
  </Card>
);

ChartCard.displayName = "ChartCard";
export default ChartCard;

Now, let’s define more measures and dimensions in the Cube.js schema. Open your Cube.js Service project and edit the Orders.js file in the schema folder.

// Replace the content of schema/Orders.js with the following:

cube(`Orders`, {
  sql: `select * from order_order`,

  measures: {
    count: {
      type: `count`
    },

    totalNet: {
      sql: `total_net`,
      type: `sum`,
      format: `currency`
    },

    averageValue: {
      sql: `total_net`,
      type: `avg`,
      format: `currency`
    }
  },

  dimensions: {
    created: {
      sql: `created`,
      type: `time`
    },

    status: {
      sql: `status`,
      type: `string`
    }
  }
});

We have added two new measures — total sales and average sales. And one new dimension — status.

Now, let’s define all new queries in the DashboardPage component. We will display the line charts for total orders, total sales, and average order value (per day), and pie chart for the breakdown of order count by status. The default date range is set to last 30 days.

// Replace the content of analytics/views/DashboardPage.tsx with the following
import * as moment from 'moment';
import * as React from "react";

import CardSpacer from "../../components/CardSpacer";
import Container from "../../components/Container";
import PageHeader from "../../components/PageHeader";
import i18n from "../../i18n";

import ChartCard from "./../components/ChartCard";

const cardContainerStyles = {
  display: "grid",
  gridColumnGap: "24px",
  gridTemplateColumns: "1fr 1fr",
  rowGap: "24px"
}

const timeDimensions = [{
  dateRange: [
    moment().subtract(30, 'days').format("YYYY-MM-DD"),
    moment().format("YYYY-MM-DD")
  ],
  dimension: 'Orders.created',
  granularity: 'day'
}];
const queries = [
  {
    query: {
      measures: ["Orders.count"],
      timeDimensions
    },
    title: i18n.t("Total Orders"),
    visualizationType: 'line',
  },
  {
    query: {
      measures: ["Orders.totalNet"],
      timeDimensions
    },
    title: i18n.t("Total Sales"),
    visualizationType: 'line'
  },
  {
    query: {
      measures: ["Orders.averageValue"],
      timeDimensions
    },
    title: i18n.t("Average Order Value"),
    visualizationType: 'line'
  },
  {
    query: {
      dimensions: ["Orders.status"],
      measures: ["Orders.count"]
    },
    title: i18n.t("Orders by Status"),
    visualizationType: 'pie'
  }
];

const DashboardPage = () => (
  <Container width="md">
    <PageHeader title={i18n.t("Analytics")} />
    <CardSpacer />
    <div style={cardContainerStyles}>
      {queries.map((query, index) => (
        <ChartCard key={index} {...query}  />
      ))}
    </div>
  </Container>
);

DashboardPage.displayName = "DashboardPage";
export default DashboardPage;

This will give us a fully working dashboard with four basic metrics. In the next tutorial, we’ll show you how to add dashboard filters and a query builder to let users build custom reports.

You may also like: