From 2b051717b56348b389a2f38710daa9c595ab4661 Mon Sep 17 00:00:00 2001 From: Sammy Khamis Date: Tue, 20 Dec 2022 16:14:22 -1000 Subject: [PATCH] add a more robust data table that allows filtering --- data/aboutsync.css | 6 + package-lock.json | 124 ++++++++--- package.json | 3 + src/CollectionsViewer.jsx | 3 +- src/components/DynamicTableView.jsx | 317 ++++++++++++++++++++++++++++ 5 files changed, 428 insertions(+), 25 deletions(-) create mode 100644 src/components/DynamicTableView.jsx diff --git a/data/aboutsync.css b/data/aboutsync.css index 6ae9430..68c6459 100644 --- a/data/aboutsync.css +++ b/data/aboutsync.css @@ -119,6 +119,12 @@ html { cursor: pointer; } +.filterHeader { + inline-size: 100%; + padding: 4px; + font-size: 14px; +} + /* ============================================================================= Panel and PanelGroup ============================================================================= */ diff --git a/package-lock.json b/package-lock.json index 004b4ca..28fb928 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,12 @@ "version": "0.21.0", "license": "MPL-2.0", "dependencies": { + "@tanstack/react-table": "^8.7.4", "classnames": "^2.2.5", + "json-stringify-safe": "^5.0.1", "prop-types": "^15.6.0", "react": "^18.2.0", + "react-data-grid": "^7.0.0-beta.20", "react-dom": "^18.2.0", "react-dom-factories": "^1.0.2", "react-inspector": "^6.0.1", @@ -1668,12 +1671,6 @@ "regenerator-runtime": "^0.13.4" } }, - "node_modules/@babel/runtime/node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, "node_modules/@babel/template": { "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", @@ -1970,6 +1967,37 @@ "node": ">=6" } }, + "node_modules/@tanstack/react-table": { + "version": "8.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.7.4.tgz", + "integrity": "sha512-4Q1OV3kUSxbtekclS3sqjCyCDowqCPruUCfHm1nGoPSmVLRCHoQ1E2rmzTVGeocEKgV9TDnj39tOgGwcADPuPA==", + "dependencies": { + "@tanstack/table-core": "8.7.4" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.7.4.tgz", + "integrity": "sha512-OPhzca7e83KGukxLpZ+4F9OBp9CZXi9b/OGJBH8dlC3wpffcSSDczYGUGJKG7wirXQ6cUVYL+9G6UrrdnblKNQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/decompress": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", @@ -3354,6 +3382,14 @@ "mimic-response": "^1.0.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -5118,9 +5154,9 @@ } }, "node_modules/flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, "node_modules/fluent-syntax": { @@ -6287,8 +6323,7 @@ "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "node_modules/json5": { "version": "2.2.1", @@ -7979,6 +8014,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-data-grid": { + "version": "7.0.0-beta.20", + "resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-7.0.0-beta.20.tgz", + "integrity": "sha512-SJr425WjXGuiZBr3mCRSv9Fwwmd6MEBrBBtGuWPx8GS4FuhomQWgJHYsfXOGJPBmuayICJ1l/87Cjqri0E0ncA==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": "^18.0", + "react-dom": "^18.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -8063,6 +8110,12 @@ "node": ">=4" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, "node_modules/regenerator-transform": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", @@ -11231,14 +11284,6 @@ "dev": true, "requires": { "regenerator-runtime": "^0.13.4" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - } } }, "@babel/template": { @@ -11485,6 +11530,19 @@ "defer-to-connect": "^1.0.1" } }, + "@tanstack/react-table": { + "version": "8.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.7.4.tgz", + "integrity": "sha512-4Q1OV3kUSxbtekclS3sqjCyCDowqCPruUCfHm1nGoPSmVLRCHoQ1E2rmzTVGeocEKgV9TDnj39tOgGwcADPuPA==", + "requires": { + "@tanstack/table-core": "8.7.4" + } + }, + "@tanstack/table-core": { + "version": "8.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.7.4.tgz", + "integrity": "sha512-OPhzca7e83KGukxLpZ+4F9OBp9CZXi9b/OGJBH8dlC3wpffcSSDczYGUGJKG7wirXQ6cUVYL+9G6UrrdnblKNQ==" + }, "@types/decompress": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", @@ -12586,6 +12644,11 @@ "mimic-response": "^1.0.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -13950,9 +14013,9 @@ } }, "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, "fluent-syntax": { @@ -14823,8 +14886,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "json5": { "version": "2.2.1", @@ -16146,6 +16208,14 @@ "loose-envify": "^1.1.0" } }, + "react-data-grid": { + "version": "7.0.0-beta.20", + "resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-7.0.0-beta.20.tgz", + "integrity": "sha512-SJr425WjXGuiZBr3mCRSv9Fwwmd6MEBrBBtGuWPx8GS4FuhomQWgJHYsfXOGJPBmuayICJ1l/87Cjqri0E0ncA==", + "requires": { + "clsx": "^1.1.1" + } + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -16216,6 +16286,12 @@ "regenerate": "^1.4.2" } }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, "regenerator-transform": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", diff --git a/package.json b/package.json index 772ac12..219de1a 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,12 @@ ] }, "dependencies": { + "@tanstack/react-table": "^8.7.4", "classnames": "^2.2.5", + "json-stringify-safe": "^5.0.1", "prop-types": "^15.6.0", "react": "^18.2.0", + "react-data-grid": "^7.0.0-beta.20", "react-dom": "^18.2.0", "react-dom-factories": "^1.0.2", "react-inspector": "^6.0.1", diff --git a/src/CollectionsViewer.jsx b/src/CollectionsViewer.jsx index a2ddc22..b6747bb 100644 --- a/src/CollectionsViewer.jsx +++ b/src/CollectionsViewer.jsx @@ -14,6 +14,7 @@ const { PlacesSqlView, promiseSql } = require("./PlacesSqlView"); const { BookmarkValidator } = require("./bookmarkValidator"); const validation = require("./validation"); +const {default: DynamicTableView} = require("./components/DynamicTableView"); const { Weave } = importLocal("resource://services-sync/main.js"); const { AddonValidator } = importLocal("resource://services-sync/engines/addons.js"); @@ -260,7 +261,7 @@ class CollectionViewer extends React.Component { - + diff --git a/src/components/DynamicTableView.jsx b/src/components/DynamicTableView.jsx new file mode 100644 index 0000000..b8b56c6 --- /dev/null +++ b/src/components/DynamicTableView.jsx @@ -0,0 +1,317 @@ +import React, { Fragment } from "react"; +var stringify = require("json-stringify-safe"); +import styled from "styled-components"; + +import { + useReactTable, + getCoreRowModel, + getExpandedRowModel, + getFilteredRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getPaginationRowModel, + flexRender, +} from "@tanstack/react-table"; +import { ObjectInspector } from "react-inspector"; + +// styled-components allow us to create smaller customizable components +const StyledTable = styled.table` + tbody tr { + :nth-of-type(odd) { + background-color: #f0f0f0; + } + :hover { + background-color: lightgrey; + } + } + + td { + padding: 4px; + } +`; + +export default function DynamicTableView(props) { + const [columnFilters, setColumnFilters] = React.useState([]); + const [expanded, setExpanded] = React.useState({}); + const [globalFilter, setGlobalFilter] = React.useState(""); + + let records = JSON.parse(stringify(props.data)); + + let tableKeys = new Set(); + // Since each table comes with it's own schema, we just iterate over + // all the properties (that are defined) to make the columns dynamic + records.forEach((record) => { + for (let prop in record) { + if (Object.prototype.hasOwnProperty.call(record, prop)) { + tableKeys.add(prop); + } + } + }); + + const columns = React.useMemo(() => { + return [ + { + header: "Records", + columns: [...tableKeys].map((key) => { + return { + accessorFn: (row) => { + // If it's an array, we show length and later turn it into a subrow + if (Array.isArray(row[key])) { + return `${key}: ${row[key].length}`; + } + // Objects can't just be displayed by default in react, so stringify it and truncate it + if (typeof row[key] === "object") { + return `${stringify(row[key]).substring(0, 24)}...`; + } + return row[key]; + }, + id: key, + cell: (info) => { + const { row, getValue } = info; + console.log("info: ", info); + return detectIsExpandableColumn(info) ? ( +
+ +
+ ) : ( + getValue() + ); + }, + header: () => {key}, + }; + }), + }, + ]; + }); + + const [data, setData] = React.useState(() => records); + + const table = useReactTable({ + data, + columns, + state: { + columnFilters, + globalFilter, + expanded, + }, + onExpandedChange: setExpanded, + getSubRows: (row) => { + if (row.tabs) { + return row.tabs; + } + if (row.parent) { + return [row.parent]; + } + }, + renderSubComponent, + onColumnFiltersChange: setColumnFilters, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + getExpandedRowModel: getExpandedRowModel(), + }); + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : ( + <> +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {header.column.getCanFilter() ? ( +
+ +
+ ) : null} + + )} + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + + {row.getVisibleCells().map((cell) => { + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ); + })} + + {row.getIsExpanded() && ( + + {/* 2nd row is a custom 1 cell row */} + + {renderSubComponent({ row })} + + + )} + + ); + })} + +
+
+
+ + + + + +
Page
+ + {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + +
+ + | Go to page: + { + const page = e.target.value ? Number(e.target.value) - 1 : 0; + table.setPageIndex(page); + }} + className="border p-1 rounded w-16" + /> + + +
+
{table.getPrePaginationRowModel().rows.length} Rows
+ {/* Debug object, uncomment this to see all the values of the table in realtime */} + {/*
{JSON.stringify(table.getState(), null, 2)}
*/} +
+ ); +} + +function Filter({ column, table }) { + const columnFilterValue = column.getFilterValue(); + return ( + <> + column.setFilterValue(value)} + placeholder={`Search... (${column.getFacetedUniqueValues().size})`} + className="w-36 border shadow rounded" + list={column.id + "list"} + /> +
+ + ); +} + +// A debounced input react component +function DebouncedInput({ + value: initialValue, + onChange, + debounce = 500, + ...props +}) { + const [value, setValue] = React.useState(initialValue); + + React.useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + React.useEffect(() => { + const timeout = setTimeout(() => { + onChange(value); + }, debounce); + + return () => clearTimeout(timeout); + }, [value]); + + return ( + setValue(e.target.value)} + /> + ); +} + +const renderSubComponent = ({ row }) => { + console.log("rendering subrow: ", row); + return ( +
+      
+    
+ ); +}; + +function detectIsExpandableColumn(info) { + // XXX - just do this manually until we find a better way + if (info.column.id === "tabs" || info.column.id === "parent") { + return true; + } + return false; +}