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!
- Prepare the data in the database β lots of data!
- Launch an API to work with that data, fast and easy
- Create an application with React and Material UI
- Build a basic data table
- Extend the data table with various features, step by step
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.
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
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
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`
}
}
});
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>
// ...
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
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`
+ },
// ...
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`
}
}
});
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;
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;
Finally! Here's the data table we were waiting for:
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;
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>
// ...
Nice! π Now we have a table which displays information about all orders and has some colorful touch:
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
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;
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;
Perfect! π Now the data table has a filter which switches between different types of orders:
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;
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;
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>
// ...
Wonderful! π Now we have the data table that fully supports filtering and sorting:
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)