DEV Community

Cover image for React Data Table βš›οΈ with Material UI and a Spark of Joy πŸ˜›
Igor Lukanin for Cube

Posted on

React Data Table βš›οΈ with Material UI and a Spark of Joy πŸ˜›

When you want to visualize large amounts of uniform data, charts don't work well because they effectively hide information about individual data items. However, it's the case when data tables come in handy! πŸ˜‡

In this tutorial, we'll learn how to display massive loads of data in a data table that is built from scratch in React. We'll explore how to fetch data from a database via an API and visualize it in a data table with essential features like filtering, sorting, etc.

We'll use Material UI because it's the most popular UI framework for React. Created with inspiration from Google's Material Design, it provides a lot of components that we can use to get a nice looking user interface.

How to Build a Data Table πŸ€”

Here's our plan for today!

Sounds nice? Let's go!

Before you dive into hacking, see a screenshot of the data table we're going to build. Also, check out the full source code available on GitHub.

Alt Text

Prepare the data in the database πŸ’Ύ

We’ll use, I guess, one of the most popular SQL data stores β€” PostgreSQL database. Please make sure you have PostgreSQL installed. (Otherwise, it may stop being so popular one day πŸ˜›.)

Now we can download and import a carefully prepared sample e-commerce dataset for PostgreSQL. The data set is related an imaginary e-commerce company that wants to track its orders and their statuses:

$ curl <http://cube.dev/downloads/ecom-dump.sql> > ecom-dump.sql
$ createdb ecom
$ psql --dbname ecom -f ecom-dump.sql
Enter fullscreen mode Exit fullscreen mode

So, the database is ready! Let's proceed to...

Launch an API to work with data πŸš€

We're going to use Cube.js for our API. Cube.js is an open-source analytical API platform that helps to create APIs for SQL data stores and build analytical apps. It removes all the hustle of building the API layer, generating SQL, and querying the database. It also provides many production-grade features like multi-level caching for optimal performance, multi-tenancy, security, and more.

So, let's launch an API on top of our database with Cube.js in a few simple steps.

First, we need to install the Cube.js command-line utility (CLI). For convenience, let's install it globally on our machine.

$ npm install -g cubejs-cli

Then, with the CLI installed, we can create a basic backend by running a single command. Cube.js supports all popular databases, so we can pre-configure the backend to work with PostgreSQL:

$ cubejs create <project name> -d <database type>

To create the backend, we run this command:

$ cubejs create react-data-table -d postgres

Now we need to connect it to the database. In order to do that, we provide a few options via the .env file in the root of the Cube.js project folder (react-data-table):

CUBEJS_DB_NAME=ecom
CUBEJS_DB_TYPE=postgres
CUBEJS_API_SECRET=secret
Enter fullscreen mode Exit fullscreen mode

Now we can run the backend!

In development mode, the backend will also run the Cube.js Playground. That's really cool. 🀘 Cube.js Playground a time-saving web application that helps to create a data schema, test out the queries, and generate a React dashboard boilerplate. Run the following command in the Cube.js project folder:

$ node index.js

Next, open http://localhost:4000 in your browser.

We'll use the Cube.js Playground to create a data schema. It's essentially a JavaScript code that declaratively describes the data, defines analytical entities like measures and dimensions, and maps them to SQL queries. Here is an example of the schema which can be used to describe products’ data.

cube(`Products`, {
  sql: `SELECT * FROM public.products`,

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

  dimensions: {
    name: {
      sql: `name`,
      type: `string`
    },

    id: {
      sql: `id`,
      type: `number`,
      primaryKey: true,
      shown: true
    },

    description: {
      sql: `description`,
      type: `string`
    },

    createdAt: {
      sql: `created_at`,
      type: `time`
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Cube.js can generate a simple data schema based on the database’s tables. If you already have a non-trivial set of tables in your database, consider using the data schema generation because it can save a lot of time.

So, let's navigate to the Schema tab of Cube.js Playground, select public group in the tree view, select the line_items, orders, products, and users tables, and click β€œGenerate Schema.” As the result, we'll have 4 generated files in the schema folder β€” exactly one schema file per table.

Once the schema is generated, we can query the data via the Cube.js Playground. To do so, navigate to the β€œBuild” tab and select some measures and dimensions from the schema. Looks like magic, isn't it?

The "Build" tab is a place where you can build sample charts using different visualization libraries and inspect every aspect of how that chart was created, starting from the generated SQL all the way up to the JavaScript code to render the chart. You can also inspect the Cube.js query encoded with JSON which is sent to Cube.js backend.

Okay, we're all set. The API is ready, now let's...

Create an application with React βš›οΈ

Big news! πŸ˜›

The Cube.js Playground can generate a template for any chosen frontend framework and charting library for you. To create a template for our application, navigate to the "Dashboard App" and use these options:

  • Framework: React
  • Main Template: React Material UI Static
  • Charting Library: Chart.js

Congratulations! Now we have the dashboard-app folder in our project. This folder contains all the frontend code that we're going to extend.

Before we proceed, let's make the most crucial change β€” show in the title that we're building a data table. 😝

To do so, chnage a few lines in the public/index.html file of the dashboard-app as follows:

// ...

-    <title>React App</title>
+    <title>React Data Table</title>
+    <style>
+      body {
+          background-color: #eeeeee;
+          margin: 0;
+      }
+    </style>   
// ...
Enter fullscreen mode Exit fullscreen mode

Also, let's install a few dependencies of dashboard-app that will make our task of building a data table easier:

$ npm install --save react-perfect-scrollbar @material-ui/pickers
Enter fullscreen mode Exit fullscreen mode

So, now we're ready to...

Build a basic data table πŸ—

It's great to have lots of data in the table, right? So, let's fetch them via the API.

To do so, we're going to define a number of new metrics: amount of items in an order (its size), an order's price, and a user's full name. With Cube.js, it's super-easy:

First, let's add the full name in the "Users" schema in the schema/Users.js file. To create the full name, we concatenate first name and last name using the SQL function CONCAT:

cube(`Users`, {
  sql: `SELECT * FROM public.users`,

// ...

  dimensions: {    

// ...

    id: {
+      shown: true,
      sql: `id`,
      type: `number`,
      primaryKey: true
    },

    firstName: {
      sql: `first_name`,
      type: `string`
    },

    lastName: {
      sql: `last_name`,
      type: `string`
    },

+    fullName: {
+      sql: `CONCAT(${firstName}, ' ', ${lastName})`,
+      type: `string`
+    },

// ...
Enter fullscreen mode Exit fullscreen mode

Then, let's add other measures to the "Orders" schema in the schema/Orders.js file.

For these measures, we're going to use the subquery feature of Cube.js. You can use subquery dimensions to reference measures from other cubes inside a dimension. Here's how to define such dimensions:

cube(`Orders`, {
  sql: `SELECT * FROM public.orders`,

  dimensions: {

// ...

    id: {
+      shown: true,
      sql: `id`,
      type: `number`,
      primaryKey: true
    },

    createdAt: {
      sql: `created_at`,
      type: `time`
    },

+    size: {
+      sql: `${LineItems.count}`,
+      subQuery: true,
+      type: 'number'
+    },
+
+    price: {
+      sql: `${LineItems.price}`,
+      subQuery: true,
+      type: 'number'
+    },

    completedAt: {
      sql: `completed_at`,
      type: `time`
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

We're almost there! So, to display the data table, let's replace the src/pages/DashboardPage.js file with the following contents:

import React from "react";
import { makeStyles } from "@material-ui/styles";

import Table from "../components/Table.js";

const useStyles = makeStyles(theme => ({
  root: { padding: 15 },
  content: { marginTop: 15 },
}));

const Dashboard = () => {
  const classes = useStyles();

  const query = {
    timeDimensions: [
      {
        dimension: 'Orders.createdAt',
        granularity: 'day'
      }
    ],
    dimensions: [
      'Users.id',
      'Orders.id',
      'Orders.size',
      'Users.fullName',
      'Users.city',
      'Orders.price',
      'Orders.status',
      'Orders.createdAt',
    ]
  };

  return (
    <div className={classes.root}>
      <div className={classes.content}>
        <Table query={query}/>
      </div>
    </div>
  );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

Note that now this file contains a Cube.js query which is quite self-explanatory: we're just asking the API to return a number of dimensions, and it does the thing.

The changes to the Dashboard are minimal, however, all the magic of rendering a data table happens inside the <Table /> component, and changes to the query result are reflected in the table.

Let's create this <Table /> component in the src/components/Table.js file with the following contents:

import React, { useState } from "react";
import clsx from "clsx";
import PropTypes from "prop-types";
import moment from "moment";
import PerfectScrollbar from "react-perfect-scrollbar";
import { makeStyles } from "@material-ui/styles";
import Typography from "@material-ui/core/Typography";
import { useCubeQuery } from "@cubejs-client/react";
import CircularProgress from "@material-ui/core/CircularProgress";
import {
  Card,
  CardActions,
  CardContent,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
  TablePagination
} from "@material-ui/core";

const useStyles = makeStyles(theme => ({
  root: {
    padding: 0
  },
  content: {
    padding: 0
  },
  inner: {
    minWidth: 1050
  },
  nameContainer: {
    display: "flex",
    alignItems: "baseline"
  },
  status: {
    marginRight: 15
  },
  actions: {
    justifyContent: "flex-end"
  },
}));

const TableComponent = props => {

  const { className, query, cubejsApi, ...rest } = props;

  const classes = useStyles();

  const [rowsPerPage, setRowsPerPage] = useState(10);
  const [page, setPage] = useState(0);

  const tableHeaders = [
    { text: "Full Name", value: "Users.fullName" },
    { text: "User city", value: "Users.city" },
    { text: "Order price", value: "Orders.price" },
    { text: "Status", value: "Orders.status" },
    { text: "Created at", value: "Orders.createdAt" }
  ];
  const { resultSet, error, isLoading } = useCubeQuery(query, { cubejsApi });
  if (isLoading) {
    return <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}><CircularProgress color="secondary" /></div>;
  }
  if (error) {
    return <pre>{error.toString()}</pre>;
  }
  if (resultSet) {
    let orders = resultSet.tablePivot();

    const handlePageChange = (event, page) => {
      setPage(page);
    };
    const handleRowsPerPageChange = event => {
      setRowsPerPage(event.target.value);
    };

    return (
      <Card
        {...rest}
        padding={"0"}
        className={clsx(classes.root, className)}
      >
        <CardContent className={classes.content}>
          <PerfectScrollbar>
            <div className={classes.inner}>
              <Table>
                <TableHead className={classes.head}>
                  <TableRow>
                    {tableHeaders.map((item) => (
                      <TableCell key={item.value + Math.random()} 
                                                                 className={classes.hoverable}           
                      >
                        <span>{item.text}</span>

                      </TableCell>
                    ))}
                  </TableRow>
                </TableHead>
                <TableBody>
                  {orders.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map(obj => (
                    <TableRow
                      className={classes.tableRow}
                      hover
                      key={obj["Orders.id"]}
                    >
                      <TableCell>
                        {obj["Orders.id"]}
                      </TableCell>
                      <TableCell>
                        {obj["Orders.size"]}
                      </TableCell>
                      <TableCell>
                        {obj["Users.fullName"]}
                      </TableCell>
                      <TableCell>
                        {obj["Users.city"]}
                      </TableCell>
                      <TableCell>
                        {"$ " + obj["Orders.price"]}
                      </TableCell>
                      <TableCell>
                        {obj["Orders.status"]}
                      </TableCell>
                      <TableCell>
                        {moment(obj["Orders.createdAt"]).format("DD/MM/YYYY")}
                      </TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </div>
          </PerfectScrollbar>
        </CardContent>
        <CardActions className={classes.actions}>
          <TablePagination
            component="div"
            count={orders.length}
            onChangePage={handlePageChange}
            onChangeRowsPerPage={handleRowsPerPageChange}
            page={page}
            rowsPerPage={rowsPerPage}
            rowsPerPageOptions={[5, 10, 25, 50, 100]}
          />
        </CardActions>
      </Card>
    );
  } else {
    return null
  }
};

TableComponent.propTypes = {
  className: PropTypes.string,
  query: PropTypes.object.isRequired
};

export default TableComponent;
Enter fullscreen mode Exit fullscreen mode

Finally! Here's the data table we were waiting for:

Alt Text

Looks great, yeah?

Please note that it actually is not so basic! 😜 You have a built-in pagination that allows to display and navigate vast amounts of data.

However, it looks grey-ish and gloomy. So, let's add color and extend the table with...

Custom Cell Format

The table contains orders' statuses which are at displayed as text at this point. Let's replace them with a custom component!

The idea is to visualize an order's status with a colorful dot. For that, we'll create a custom <StatusBullet /> component. Let's create this component in the src/components/StatusBullet.js file with the following contents:

import React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { makeStyles } from '@material-ui/styles';

const useStyles = makeStyles(theme => ({
  root: {
    display: 'inline-block',
    borderRadius: '50%',
    flexGrow: 0,
    flexShrink: 0
  },
  sm: {
    height: 15,
    width: 15
  },
  md: {
    height: 15,
    width: 15
  },
  lg: {
    height: 15,
    width: 15
  },
  neutral: { backgroundColor: '#fff' },
  primary: { backgroundColor: '#ccc' },
  info: { backgroundColor: '#3cc' },
  warning: { backgroundColor: '#cc3' },
  danger: { backgroundColor: '#c33' },
  success: { backgroundColor: '#3c3' }
}));

const StatusBullet = props => {
  const { className, size, color, ...rest } = props;

  const classes = useStyles();

  return (
    <span
      {...rest}
      className={clsx(
        {
          [classes.root]: true,
          [classes[size]]: size,
          [classes[color]]: color
        },
        className
      )}
    />
  );
};

StatusBullet.propTypes = {
  className: PropTypes.string,
  color: PropTypes.oneOf([
    'neutral',
    'primary',
    'info',
    'success',
    'warning',
    'danger'
  ]),
  size: PropTypes.oneOf(['sm', 'md', 'lg'])
};

StatusBullet.defaultProps = {
  size: 'md',
  color: 'default'
};

export default StatusBullet;
Enter fullscreen mode Exit fullscreen mode

To make it work, we'll need to apply some minimal changes to the data table. Let's modify the src/components/Table.js as follows:

// ...

} from "@material-ui/core";

import StatusBullet from "./StatusBullet";

const statusColors = {
  completed: "success",
  processing: "info",
  shipped: "danger"
};

const useStyles = makeStyles(theme => ({

// ...

<TableCell>
+  <StatusBullet
+    className={classes.status}
+    color={statusColors[obj["Orders.status"]]}
+    size="sm"
+  />
  {obj["Orders.status"]}
</TableCell>

// ...
Enter fullscreen mode Exit fullscreen mode

Nice! πŸŽ‰ Now we have a table which displays information about all orders and has some colorful touch:

Alt Text

Filtering the Data

However, its hard to explore these orders using only the controls provided. To fix this, we'll add a comprehensive toolbar with filters and make our table interactive.

First, let's add a few dependencies. Run the command in the dashboard-app folder:

npm install --save @date-io/date-fns@1.x date-fns @date-io/moment@1.x moment
Enter fullscreen mode Exit fullscreen mode

Then, create the <Toolbar /> component in the src/components/Toolbar.js file with the following contents:

import "date-fns";
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/styles";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
import withStyles from "@material-ui/core/styles/withStyles";

const AntTabs = withStyles({
  indicator: {},
})(Tabs);
const AntTab = withStyles((theme) => ({
  root: {
    textTransform: 'none',
    minWidth: 25,
    fontSize: 12,
    fontWeight: theme.typography.fontWeightRegular,
    marginRight: 0,
    opacity: 0.6,
    '&:hover': {
      opacity: 1,
    },
    '&$selected': {
      fontWeight: theme.typography.fontWeightMedium,
      outline: 'none',
    },
    '&:focus': {
      outline: 'none',
    },
  },
  selected: {},
}))((props) => <Tab disableRipple {...props} />);
const useStyles = makeStyles(theme => ({
  root: {},
  row: {
    marginTop: 15
  },
  spacer: {
    flexGrow: 1
  },
  importButton: {
    marginRight: 15
  },
  exportButton: {
    marginRight: 15
  },
  searchInput: {
    marginRight: 15
  },
  formControl: {
    margin: 25,
    fullWidth: true,
    display: "flex",
    wrap: "nowrap"
  },
  date: {
    marginTop: 3
  },
  range: {
    marginTop: 13
  }
}));

const Toolbar = props => {
  const { className,
    statusFilter,
    setStatusFilter,
    tabs,
    ...rest } = props;
  const [tabValue, setTabValue] = React.useState(statusFilter);

  const classes = useStyles();

  const handleChangeTab = (e, value) => {
    setTabValue(value);
    setStatusFilter(value);
  };

  return (
    <div
      {...rest}
      className={className}
    >
      <Grid container spacing={4}>
        <Grid
          item
          lg={3}
          sm={6}
          xl={3}
          xs={12}
          m={2}
        >
          <div className={classes}>
            <AntTabs value={tabValue} onChange={(e,value) => {handleChangeTab(e,value)}} aria-label="ant example">
              {tabs.map((item) => (<AntTab key={item} label={item} />))}
            </AntTabs>
            <Typography className={classes.padding} />
          </div>
        </Grid>
      </Grid>
    </div>
  );
};

Toolbar.propTypes = {
  className: PropTypes.string
};

export default Toolbar;
Enter fullscreen mode Exit fullscreen mode

Let's modify the src/pages/DashboardPage file:

import React from "react";
import { makeStyles } from "@material-ui/styles";

+ import Toolbar from "../components/Toolbar.js";
import Table from "../components/Table.js";

const useStyles = makeStyles(theme => ({
  root: {
    padding: 15
  },
  content: {
    marginTop: 15
  },
}));

const DashboardPage = () => {
  const classes = useStyles();
+  const tabs = ['All', 'Shipped', 'Processing', 'Completed'];
+  const [statusFilter, setStatusFilter] = React.useState(0);

  const query = {
    "timeDimensions": [
      {
        "dimension": "Orders.createdAt",
        "granularity": "day"
      }
    ],
    "dimensions": [
      "Users.id",
      "Orders.id",
      "Orders.size",
      "Users.fullName",
      "Users.city",
      "Orders.price",
      "Orders.status",
      "Orders.createdAt"
    ],
+    "filters": [
+      {
+        "dimension": "Orders.status",
+        "operator": tabs[statusFilter] !== 'All' ? "equals" : "set",
+        "values": [
+          `${tabs[statusFilter].toLowerCase()}`
+        ]
+      }
+    ]
  };

  return (
    <div className={classes.root}>
+      <Toolbar
+        statusFilter={statusFilter}
+        setStatusFilter={setStatusFilter}
+        tabs={tabs}
+      />
      <div className={classes.content}>
        <Table
          query={query}/>
      </div>
    </div>
  );
};

export default DashboardPage;
Enter fullscreen mode Exit fullscreen mode

Perfect! πŸŽ‰ Now the data table has a filter which switches between different types of orders:

Alt Text

However, orders have other parameters such as price and dates. Let's create filters for these parameters. To do so, modify the src/components/Toolbar.js file:

import "date-fns";
import React from "react";
import PropTypes from "prop-types";
import clsx from "clsx";
import { makeStyles } from "@material-ui/styles";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
import withStyles from "@material-ui/core/styles/withStyles";
+ import DateFnsUtils from "@date-io/date-fns";
+ import {
+   MuiPickersUtilsProvider,
+   KeyboardDatePicker
+ } from "@material-ui/pickers";
+ import Slider from "@material-ui/core/Slider";

// ...

const Toolbar = props => {
  const { className,
+   startDate,
+   setStartDate,
+   finishDate,
+   setFinishDate,
+   priceFilter,
+   setPriceFilter,
    statusFilter,
    setStatusFilter,
    tabs,
    ...rest } = props;
  const [tabValue, setTabValue] = React.useState(statusFilter);
+ const [rangeValue, rangeSetValue] = React.useState(priceFilter);

  const classes = useStyles();

  const handleChangeTab = (e, value) => {
    setTabValue(value);
    setStatusFilter(value);
  };
+  const handleDateChange = (date) => {
+    setStartDate(date);
+  };
+  const handleDateChangeFinish = (date) => {
+    setFinishDate(date);
+  };
+ const handleChangeRange = (event, newValue) => {
+   rangeSetValue(newValue);
+ };
+ const setRangeFilter = (event, newValue) => {
+   setPriceFilter(newValue);
+ };

  return (
    <div
      {...rest}
      className={clsx(classes.root, className)}
    >
      <Grid container spacing={4}>
        <Grid
          item
          lg={3}
          sm={6}
          xl={3}
          xs={12}
          m={2}
        >
          <div className={classes}>
            <AntTabs value={tabValue} onChange={(e,value) => {handleChangeTab(e,value)}} aria-label="ant example">
              {tabs.map((item) => (<AntTab key={item} label={item} />))}
            </AntTabs>
            <Typography className={classes.padding} />
          </div>
        </Grid>
+        <Grid
+          className={classes.date}
+          item
+          lg={3}
+          sm={6}
+          xl={3}
+          xs={12}
+          m={2}
+        >
+          <MuiPickersUtilsProvider utils={DateFnsUtils}>
+            <Grid container justify="space-around">
+              <KeyboardDatePicker
+                id="date-picker-dialog"
+               label={<span style={{opacity: 0.6}}>Start Date</span>}
+                format="MM/dd/yyyy"
+                value={startDate}
+                onChange={handleDateChange}
+                KeyboardButtonProps={{
+                  "aria-label": "change date"
+                }}
+              />
+            </Grid>
+          </MuiPickersUtilsProvider>
+        </Grid>
+        <Grid
+          className={classes.date}
+          item
+          lg={3}
+          sm={6}
+          xl={3}
+          xs={12}
+          m={2}
+        >
+          <MuiPickersUtilsProvider utils={DateFnsUtils}>
+            <Grid container justify="space-around">
+              <KeyboardDatePicker
+                id="date-picker-dialog-finish"
+                label={<span style={{opacity: 0.6}}>Finish Date</span>}
+                format="MM/dd/yyyy"
+                value={finishDate}
+                onChange={handleDateChangeFinish}
+                KeyboardButtonProps={{
+                  "aria-label": "change date"
+                }}
+              />
+            </Grid>
+          </MuiPickersUtilsProvider>
+        </Grid>
+        <Grid
+          className={classes.range}
+          item
+          lg={3}
+          sm={6}
+          xl={3}
+          xs={12}
+          m={2}
+        >
+          <Typography id="range-slider">
+            Order price range
+          </Typography>
+          <Slider
+            value={rangeValue}
+            onChange={handleChangeRange}
+            onChangeCommitted={setRangeFilter}
+            aria-labelledby="range-slider"
+            valueLabelDisplay="auto"
+            min={0}
+            max={2000}
+          />
+        </Grid>
      </Grid>
    </div>
  );
};

Toolbar.propTypes = {
  className: PropTypes.string
};

export default Toolbar;
Enter fullscreen mode Exit fullscreen mode

To make these filters work, we need to connect them to the parent component: add state, modify our query, and add new props to the <Toolbar /> component. Also, we will add sorting to the data table. So, modify the src/pages/DashboardPage.js file like this:

// ...

const DashboardPage = () => {
  const classes = useStyles();
  const tabs = ['All', 'Shipped', 'Processing', 'Completed'];
  const [statusFilter, setStatusFilter] = React.useState(0);
+ const [startDate, setStartDate] = React.useState(new Date("2019-01-01T00:00:00"));
+ const [finishDate, setFinishDate] = React.useState(new Date("2022-01-01T00:00:00"));
+ const [priceFilter, setPriceFilter] = React.useState([0, 200]);
+ const [sorting, setSorting] = React.useState(['Orders.createdAt', 'desc']);

  const query = {
    timeDimensions: [
      {
        "dimension": "Orders.createdAt",
+    "dateRange": [startDate, finishDate],
        "granularity": "day"
      }
    ],
+    order: {
+      [`${sorting[0]}`]: sorting[1]
+    },
    "dimensions": [
      "Users.id",
      "Orders.id",
      "Orders.size",
      "Users.fullName",
      "Users.city",
      "Orders.price",
      "Orders.status",
      "Orders.createdAt"
    ],
    "filters": [
      {
        "dimension": "Orders.status",
        "operator": tabs[statusFilter] !== 'All' ? "equals" : "set",
        "values": [
          `${tabs[statusFilter].toLowerCase()}`
        ]
      },
+     {
+        "dimension": "Orders.price",
+        "operator": "gt",
+        "values": [
+         `${priceFilter[0]}`
+       ]
+     },
+     {
+       "dimension": "Orders.price",
+       "operator": "lt",
+       "values": [
+         `${priceFilter[1]}`
+       ]
+     },
    ]
  };

  return (
    <div className={classes.root}>
      <Toolbar
+       startDate={startDate}
+       setStartDate={setStartDate}
+       finishDate={finishDate}
+       setFinishDate={setFinishDate}
+       priceFilter={priceFilter}
+       setPriceFilter={setPriceFilter}
        statusFilter={statusFilter}
        setStatusFilter={setStatusFilter}
        tabs={tabs}
      />
      <div className={classes.content}>
        <Table
+          sorting={sorting}
+          setSorting={setSorting}
          query={query}/>
      </div>
    </div>
  );
};

export default DataTablePage;
Enter fullscreen mode Exit fullscreen mode

Fantastic! πŸŽ‰ We've added some useful filters. Indeed, you can add even more filters with custom logic. See the documentation for the filter format options.

And there's one more thing! We've added sorting props to the toolbar, but we also need to pass them to the <Table /> component. To fix this, let's modify the src/components/Table.js file:

// ...

+ import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp";
+ import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
import { useCubeQuery } from "@cubejs-client/react";
import CircularProgress from "@material-ui/core/CircularProgress";

// ...

const useStyles = makeStyles(theme => ({
  // ...
  actions: {
    justifyContent: "flex-end"
  },
+ tableRow: {
+   padding: '0 5px',
+   cursor: "pointer",
+   '.MuiTableRow-root.MuiTableRow-hover&:hover': {
+   }
+ },
+ hoverable: {
+   "&:hover": {
+     cursor: `pointer`
+   }
+ },
+ arrow: {
+   fontSize: 10,
+   position: "absolute"
+ }
}));

const statusColors = {
  completed: "success",
  processing: "info",
  shipped: "danger"
};

const TableComponent = props => {
-  const { className, query, cubejsApi, ...rest } = props;
+  const { className, sorting, setSorting, query, cubejsApi, ...rest } = props;

// ...

  if (resultSet) {

//...

+     const handleSetSorting = str => {
+       setSorting([str, sorting[1] === "desc" ? "asc" : "desc"]);
+     };

    return (
                            // ...

                <TableHead className={classes.head}>
                  <TableRow>
                    {tableHeaders.map((item) => (
                      <TableCell key={item.value + Math.random()} className={classes.hoverable}
+                                 onClick={() => {
+                                 handleSetSorting(`${item.value}`);
+                                 }}
                      >
                        <span>{item.text}</span>
+                        <Typography
+                          className={classes.arrow}
+                          variant="body2"
+                          component="span"
+                        >
+                          {(sorting[0] === item.value) ? (sorting[1] === "desc" ? <KeyboardArrowUpIcon/> :
+                            <KeyboardArrowDownIcon/>) : null}
+                        </Typography>
                      </TableCell>
                    ))}
                  </TableRow>
                </TableHead>
                         // ...
Enter fullscreen mode Exit fullscreen mode

Wonderful! πŸŽ‰ Now we have the data table that fully supports filtering and sorting:

Alt Text

And that's all! πŸ˜‡ Congratulations on completing this tutorial! πŸŽ‰

Also, check the full source code available on GitHub.

Now you should be able to create custom data tables powered by Cube.js with React and Material UI to display literally any amounts of data in your applications.

Feel free to explore other examples of what can be done with Cube.js such as the Real-Time Dashboard Guide and the Open Source Web Analytics Platform Guide.

Top comments (0)