使用 Material UI 和 Spark of Joy 来 React 数据表 ⚛️ 😛 如何构建数据表 🤔 准备数据库中的数据 💾 启动 API 来处理数据 🚀 使用 React 创建应用程序 ⚛️ 构建基本数据表 🏗

2025-06-04

使用 Material UI 和一丝喜悦来响应数据表⚛️

如何构建数据表

准备数据库中的数据💾

启动 API 来处理数据🚀

使用 React ⚛️ 创建应用程序

构建基本数据表🏗

当你想要可视化大量统一的数据时,图表效果不佳,因为它们会有效地隐藏单个数据项的信息。然而,数据表就派上用场了! 😇

在本教程中,我们将学习如何在 React 中从头构建的数据表中显示海量数据。我们将探索如何通过 API 从数据库获取数据,并使用过滤、排序等基本功能在数据表中进行可视化。

我们将使用Material UI,因为它是 React 最流行的 UI 框架。它的设计灵感源自 Google 的Material Design,提供了许多组件,可用于创建美观的用户界面。

如何构建数据表

这是我们今天的计划!

听起来不错吧?走吧!

在深入研究之前,请先查看我们将要构建的数据表的屏幕截图。此外,请查看GitHub 上提供的完整源代码。

替代文本

准备数据库中的数据💾

我想,我们会使用最流行的 SQL 数据存储之一——PostgreSQL数据库。请确保你已安装 PostgreSQL。(否则,它可能有一天会变得不再流行😛。)

现在,我们可以下载并导入一个精心准备的 PostgreSQL 示例电商数据集。该数据集与一家虚拟电商公司相关,该公司希望跟踪其订单及其状态:



$ 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

好了,数据库已经准备好了!让我们继续……

启动 API 来处理数据🚀

我们将使用Cube.js作为 API。Cube.js 是一个开源分析 API 平台,可帮助您创建用于 SQL 数据存储的 API 并构建分析应用程序。它免除了构建 API 层、生成 SQL 和查询数据库的所有繁琐步骤。它还提供了许多生产级功能,例如用于优化性能的多级缓存、多租户、安全性等等。

因此,让我们通过几个简单的步骤使用 Cube.js 在数据库上启动 API。

首先,我们需要安装 Cube.js 命令行工具(CLI)。为了方便起见,我们先在机器上全局安装它。

$ npm install -g cubejs-cli

然后,安装 CLI 后,我们可以通过运行单个命令创建一个基本的后端。Cube.js 支持所有流行的数据库,因此我们可以预先配置后端以使用 PostgreSQL:

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

要创建后端,我们运行以下命令:

$ cubejs create react-data-table -d postgres

现在我们需要将它连接到数据库。为了实现这一点,我们通过.envCube.js 项目根目录下的文件 ( react-data-table) 提供了一些选项:



CUBEJS_DB_NAME=ecom
CUBEJS_DB_TYPE=postgres
CUBEJS_API_SECRET=secret


Enter fullscreen mode Exit fullscreen mode

现在我们可以运行后端了!

在开发模式下,后端还会运行 Cube.js Playground。这真的很酷。🤘 Cube.js Playground 是一款省时的 Web 应用程序,可帮助您创建数据模式、测试查询并生成 React 仪表板样板。在 Cube.js 项目文件夹中运行以下命令:

$ node index.js

接下来,在浏览器中打开http://localhost:4000 。

我们将使用 Cube.js Playground 创建数据模式。它本质上是一段 JavaScript 代码,以声明式的方式描述数据,定义度量和维度等分析实体,并将它们映射到 SQL 查询。以下是可用于描述产品数据的模式示例。



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 可以根据数据库表生成简单的数据模式。如果您的数据库中已经有一组重要的表,可以考虑使用数据模式生成功能,因为它可以节省大量时间。

因此,让我们导航到 Cube.js Playground 的 Schema 选项卡,public在树视图中选择组,选择line_itemsordersproductsusers表,然后单击“生成 Schema”。结果,我们将在schema文件夹中生成 4 个文件 - 每个表恰好一个模式文件。

生成模式后,我们就可以通过 Cube.js Playground 查询数据了。具体操作如下:导航到“Build”选项卡,从模式中选择一些度量和维度。看起来很神奇,不是吗?

在“构建”选项卡中,您可以使用不同的可视化库构建示例图表,并检查图表创建的各个方面,从生成的 SQL 一直到用于渲染图表的 JavaScript 代码。您还可以检查发送到 Cube.js 后端的 JSON 编码的 Cube.js 查询。

好的,一切就绪。API 已准备就绪,现在我们……

使用 React ⚛️ 创建应用程序

大新闻!😛

Cube.js 园地可以为你生成任何所选前端框架和图表库的模板。要为我们的应用程序创建模板,请导航到“仪表板应用”并使用以下选项:

  • 框架:React
  • 主模板:React Material UI Static
  • 图表库:Chart.js

恭喜!现在我们的项目中有了dashboard-app文件夹。此文件夹包含我们要扩展的所有前端代码。

在继续之前,让我们做出最重要的改变——在标题中显示我们正在构建数据表。😝

为此,请更改public/index.html文件中的几行dashboard-app,如下所示:



// ...

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


Enter fullscreen mode Exit fullscreen mode

此外,让我们安装一些依赖项,dashboard-app这将使我们构建数据表的任务更容易:



$ npm install --save react-perfect-scrollbar @material-ui/pickers


Enter fullscreen mode Exit fullscreen mode

那么,现在我们准备好了...

构建基本数据表🏗

表中有大量数据真是太棒了,对吧?那么,让我们通过 API 获取它们。

为此,我们将定义一些新指标:订单中的商品数量(即订单大小)、订单价格以及用户的全名。使用 Cube.js,这非常简单:

首先,让我们在schema/Users.js文件的“Users”模式中添加全名。要创建全名,我们使用 SQL 函数连接名字和姓氏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

然后,让我们向文件中的“订单”模式添加其他措施schema/Orders.js

对于这些度量,我们将使用Cube.js 的子查询功能。您可以使用子查询维度来引用维度内其他多维数据集的度量。定义此类维度的方法如下:



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

快完成了!为了显示数据表,我们将src/pages/DashboardPage.js文件替换为以下内容:



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

请注意,现在该文件包含一个 Cube.js 查询,该查询非常不言自明:我们只是要求 API 返回一些维度,然后它就会执行该操作。

然而,对的变化Dashboard很小,渲染数据表的所有神奇之处都发生在<Table />组件内部,并且查询结果的变化反映在表中。

让我们在文件<Table />中创建这个组件src/components/Table.js,其内容如下:



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

终于!这是我们期待已久的数据表:

替代文本

看起来很棒,是吗?

请注意,它实际上并不是那么基础!😜 你有一个内置的分页功能,可以显示和浏览大量数据。

然而,它看起来有点灰暗。所以,让我们添加一些颜色,并扩展表格……

自定义单元格格式

该表包含订单状态,目前以文本形式显示。让我们用自定义组件替换它们!

我们的想法是用一个彩色圆点来可视化订单状态。为此,我们将创建一个自定义<StatusBullet />组件。让我们在src/components/StatusBullet.js文件中创建此组件,其内容如下:



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

为了使其正常工作,我们需要对数据表进行一些细微的更改。让我们修改src/components/Table.js如下:



// ...

} 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

太棒了!🎉 现在我们有了一个表格,它显示所有订单的信息,并且带有一些彩色元素:

替代文本

过滤数据

然而,仅使用提供的控件很难探索这些订单。为了解决这个问题,我们将添加一个带有过滤器的综合工具栏,并使表格具有交互性。

首先,让我们添加一些依赖项。在文件夹中运行以下命令dashboard-app



npm install --save @date-io/date-fns@1.x date-fns @date-io/moment@1.x moment


Enter fullscreen mode Exit fullscreen mode

然后,<Toolbar />src/components/Toolbar.js文件中创建具有以下内容的组件:



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

让我们修改这个src/pages/DashboardPage文件:



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

完美!🎉 现在数据表新增了一个过滤器,可以在不同类型的订单之间切换:

替代文本

但是,订单还有其他参数,例如价格和日期。让我们为这些参数创建过滤器。为此,请修改src/components/Toolbar.js文件:



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

为了使这些过滤器正常工作,我们需要将它们连接到父组件:添加状态、修改查询,并向<Toolbar />组件添加新的 props。此外,我们还需要为数据表添加排序功能。因此,请src/pages/DashboardPage.js按如下方式修改文件:



// ...

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

太棒了!🎉 我们添加了一些实用的过滤器。事实上,您还可以添加更多带有自定义逻辑的过滤器。请参阅文档,了解过滤器格式选项。

还有一件事!我们已经向工具栏添加了排序属性,但我们还需要将它们传递给<Table />组件。为了解决这个问题,让我们修改src/components/Table.js文件:



// ...

+ 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

太棒了!🎉 现在我们有了完全支持过滤和排序的数据表:

替代文本

就这样!😇 恭喜你完成本教程!🎉

另外,请检查GitHub 上提供的完整源代码。

现在,您应该能够使用 React 和 Material UI 创建由 Cube.js 提供支持的自定义数据表,以在应用程序中显示任意数量的数据。

欢迎随意探索使用 Cube.js 可以实现的其他示例,例如实时仪表板指南开源 Web 分析平台指南

文章来源:https://dev.to/cubejs/react-data-table-with-material-ui-and-a-spark-of-joy-50o1
PREV
“防御性编程”真的健康吗?AWS GenAI 上线!
NEXT
Multi-Tenant Analytics with Auth0 and Cube.js 🔐 — the Complete Guide Security... Why bother? 🤔 Step 0. Openly accessible analytical app Step 1. Authentication with JWTs Step 2. Authorization with JWTs Step 3. Identification via Auth0 Step 4. Accountability with audit logs