diff --git a/package-lock.json b/package-lock.json index 77f31bc..5350459 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@chromatic-com/storybook": "^3.2.2", + "@ngneat/falso": "^6.4.0", "@percy/cli": "^1.30.2", "@percy/storybook": "^6.0.2", "@storybook/addon-a11y": "^8.4.4", @@ -1773,6 +1774,27 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@ngneat/falso": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@ngneat/falso/-/falso-6.4.0.tgz", + "integrity": "sha512-f6r036h2fX/AoHw1eV2t8+qWQwrbSrozs3zXMhhwoO7SJBc+DGMxRWEhFeYIinfwx0uhUH8ggx5+PDLzYESLOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "seedrandom": "3.0.5", + "uuid": "8.3.2" + } + }, + "node_modules/@ngneat/falso/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -11401,6 +11423,13 @@ "integrity": "sha512-HHqQ/SqbeiDfXXVKgNxTpbQTD4n7IUb4hZATvHjp03jr3TF7igehCyHdOjeYTrzIseLO93cTTfSb5f4qWcirMQ==", "license": "MIT" }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", diff --git a/package.json b/package.json index 4d7cee1..3d9a467 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "check:types": "tsc --noEmit", "lint:style": "stylelint 'src/**/*.scss'", "lint:script": "biome lint", - "lint": "npm run lint:style && npm run lint:script", + "lint": "npm run lint:style; npm run lint:script", "test:unit": "vitest run --root=.", "test": "npm run check:types; npm run lint:style; npm run test:unit", "test-ui": "vitest --ui", @@ -91,7 +91,8 @@ "lightningcss": "^1.28.1", "@types/react": "npm:types-react@rc", "@types/react-dom": "npm:types-react-dom@rc", - "@types/react-table": "^7.7.20" + "@types/react-table": "^7.7.20", + "@ngneat/falso": "^6.4.0" }, "dependencies": { "date-fns": "^4.1.0", diff --git a/package.json.js b/package.json.js index 84c49e0..d5634cf 100644 --- a/package.json.js +++ b/package.json.js @@ -142,6 +142,9 @@ const packageConfig = { '@types/react-dom': 'npm:types-react-dom@rc', '@types/react-table': '^7.7.20', + + // Fake data + "@ngneat/falso": "^6.4.0", }, // Dependencies needed when running the generated build @@ -187,4 +190,4 @@ const packageConfigWithComment = { const packageConfigFormatted = JSON.stringify(packageConfigWithComment, null, 2); // Write to `package.json` -fs.writeFileSync('./package.json', packageConfigFormatted + '\n'); +fs.writeFileSync('./package.json', `${packageConfigFormatted}\n`); diff --git a/src/components/tables/DataTable/DataTable.stories.tsx b/src/components/tables/DataTable/DataTable.stories.tsx index e7c0f78..507b87e 100644 --- a/src/components/tables/DataTable/DataTable.stories.tsx +++ b/src/components/tables/DataTable/DataTable.stories.tsx @@ -15,7 +15,7 @@ type Story = StoryObj; import * as ReactTable from 'react-table'; -import { createTableContext, TableContextState } from './DataTableContext.tsx'; +import { createTableContext, type TableContextState } from './DataTableContext.tsx'; const DataTableContext = ({ children }: React.PropsWithChildren) => { type User = { name: string }; const columns = React.useMemo>>(() => [ diff --git a/src/components/tables/DataTable/DataTableContext.tsx b/src/components/tables/DataTable/DataTableContext.tsx index 1308493..62335ea 100644 --- a/src/components/tables/DataTable/DataTableContext.tsx +++ b/src/components/tables/DataTable/DataTableContext.tsx @@ -3,7 +3,7 @@ |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import * as ReactTable from 'react-table'; +import type * as ReactTable from 'react-table'; export type DataTableStatus = { diff --git a/src/components/tables/DataTable/DataTableEager.scss b/src/components/tables/DataTable/DataTableEager.scss index 660c636..edafd0f 100644 --- a/src/components/tables/DataTable/DataTableEager.scss +++ b/src/components/tables/DataTable/DataTableEager.scss @@ -2,7 +2,7 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use '../../../style/variables.scss' as bkl; +// @use '../../../style/variables.scss' as bkl; @use './DataTableLazy.scss' as dataTableLazy; diff --git a/src/components/tables/DataTable/DataTableEager.stories.tsx b/src/components/tables/DataTable/DataTableEager.stories.tsx new file mode 100644 index 0000000..cf8c7a0 --- /dev/null +++ b/src/components/tables/DataTable/DataTableEager.stories.tsx @@ -0,0 +1,211 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import cx from 'classnames'; +import { differenceInDays } from 'date-fns'; +import React from 'react'; + +import { delay } from '../util/async_util.ts'; +import { type User, generateData } from '../util/generateData.ts'; +import { useEffectAsync } from '../util/hooks.ts'; +import * as Filtering from './filtering/Filtering.ts'; + +import { Panel } from '../../containers/Panel/Panel.tsx'; +import * as DataTablePlugins from './plugins/useRowSelectColumn.tsx'; +import * as DataTableEager from './DataTableEager.tsx'; + + +const columns = [ + { + id: 'name', + accessor: (user: User) => user.name, + Header: 'Name', + Cell: ({ value }: { value: string }) => value, + disableSortBy: false, + disableGlobalFilter: false, + }, + { + id: 'email', + accessor: (user: User) => user.email, + Header: 'Email', + disableSortBy: false, + disableGlobalFilter: false, + }, + { + id: 'company', + accessor: (user: User) => user.company, + Header: 'Company', + disableSortBy: false, + disableGlobalFilter: false, + }, + { + id: 'joinDate', + accessor: (user: User) => user.joinDate, + Header: 'Joined', + Cell: ({ value }: { value: Date }) => + value.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }), + disableSortBy: false, + disableGlobalFilter: false, + }, +]; + +const fields = { + name: { + type: 'text', + operators: ['$text'], + label: 'Name', + placeholder: 'Search name', + }, + email: { + type: 'text', + operators: ['$text'], + label: 'Email', + placeholder: 'Search email', + }, + company: { + type: 'text', + operators: ['$text'], + label: 'Company', + placeholder: 'Search company', + }, + joinDate: { + type: 'datetime', + operators: ['$lt', '$lte', '$gt', '$gte', '$range'], + label: 'Joined', + placeholder: 'Search by joined date', + }, + daysActive: { + type: 'number', + operators: ['$eq', '$ne', '$lt', '$lte', '$gt', '$gte'], + label: 'Days active', + placeholder: 'Number of days active', + accessor: (user: User) => differenceInDays(new Date(), user.joinDate), + }, +}; + +type dataTeableEagerTemplateProps = DataTableEager.TableProviderEagerProps & { delay: number }; +const DataTableEagerTemplate = (props: dataTeableEagerTemplateProps) => { + const memoizedColumns = React.useMemo(() => props.columns, [props.columns]); + const memoizedItems = React.useMemo(() => props.items, [props.items]); + + const [isReady, setIsReady] = React.useState(props.isReady ?? true); + + useEffectAsync(async () => { + if (typeof props.delay !== 'number' && isReady === false) return; + await delay(props.delay); + setIsReady(true); + }, [props.delay]); + + return ( + + item.id} + > + + + + + ); +}; + +// Template: Table with Filtering +const DataTableEagerWithFilterTemplate = (props: dataTeableEagerTemplateProps) => { + const memoizedColumns = React.useMemo(() => props.columns, [props.columns]); + + const [filters, setFilters] = React.useState([]); + const [filteredItems, setFilteredItems] = React.useState(props.items); + + React.useEffect(() => { + const filtered = Filtering.filterByQuery(fields, props.items, filters); + setFilteredItems(Object.values(filtered)); + }, [filters, props.items]); + + return ( + + item.id} + plugins={[DataTablePlugins.useRowSelectColumn]} + > + + + + ); +}; + +export default { + title: 'Components/Tables/DataTableEager', + component: DataTableEager.DataTableEager, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +}; + +// Stories +export const Empty = { + args: { + columns, + items: generateData({ numItems: 0 }), + }, + render: (args: dataTeableEagerTemplateProps) => , +}; + +export const SinglePage = { + args: { + columns, + items: generateData({ numItems: 5 }), + }, + render: (args: dataTeableEagerTemplateProps) => , +}; + +export const MultiplePagesSmall = { + args: { + columns, + items: generateData({ numItems: 45 }), + }, + render: (args: dataTeableEagerTemplateProps) => , +}; + +export const MultiplePagesLarge = { + args: { + columns, + items: generateData({ numItems: 1000 }), + }, + render: (args: dataTeableEagerTemplateProps) => , +}; + +export const SlowNetwork = { + args: { + columns, + items: generateData({ numItems: 1000 }), + delay: 1500, + isReady: false, + }, + render: (args: dataTeableEagerTemplateProps) => , +}; + +export const InfiniteDelay = { + args: { + columns, + items: generateData({ numItems: 10 }), + delay: Number.POSITIVE_INFINITY, + isReady: false, + }, + render: (args: dataTeableEagerTemplateProps) => , +}; + +export const WithFilter = { + args: { + columns, + items: generateData({ numItems: 45 }), + }, + render: (args: dataTeableEagerTemplateProps) => , +}; diff --git a/src/components/tables/DataTable/DataTableEager.tsx b/src/components/tables/DataTable/DataTableEager.tsx index 912908f..c1ad76d 100644 --- a/src/components/tables/DataTable/DataTableEager.tsx +++ b/src/components/tables/DataTable/DataTableEager.tsx @@ -3,16 +3,16 @@ |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { classNames as cx, ClassNameArgument } from '../../../util/componentUtil.ts'; +import { classNames as cx, type ClassNameArgument } from '../../../util/componentUtil.ts'; import * as ReactTable from 'react-table'; -import { TableContextState, createTableContext, useTable } from './DataTableContext.tsx'; +import { type TableContextState, createTableContext, useTable } from './DataTableContext.tsx'; import { Pagination } from './pagination/Pagination.tsx'; -import { SearchInput } from '../../../prefab/forms/SearchInput/SearchInput.tsx'; -import { MultiSearch as MultiSearchInput } from '../../../prefab/forms/MultiSearch/MultiSearch.tsx'; -import { DataTableSync } from './table/DataTable'; +import { SearchInput } from '../SearchInput/SearchInput.tsx'; +import { MultiSearch as MultiSearchInput } from '../MultiSearch/MultiSearch.tsx'; +import { DataTableSync } from './table/DataTable.tsx'; -import './DataTableEager.scss'; +// import './DataTableEager.scss'; export * from './DataTableContext'; export { Pagination }; @@ -44,15 +44,14 @@ export const TableProviderEager = (props: TableProviderEagerPr isReady = true, } = props; - const tableOptions = { + const tableOptions: ReactTable.TableOptions = { columns, data: items, - getRowId, + ...(getRowId && { getRowId }), }; const table = ReactTable.useTable( { ...tableOptions, - defaultColumn: { disableGlobalFilter: true, disableSortBy: true, diff --git a/src/components/tables/DataTable/DataTableLazy.scss b/src/components/tables/DataTable/DataTableLazy.scss index 6de6b2f..0646721 100644 --- a/src/components/tables/DataTable/DataTableLazy.scss +++ b/src/components/tables/DataTable/DataTableLazy.scss @@ -2,7 +2,7 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use '../../../style/variables.scss' as bkl; +// @use '../../../style/variables.scss' as bkl; @use '../../../style/mixins.scss' as mixins; diff --git a/src/components/tables/DataTable/DataTableLazy.stories.tsx b/src/components/tables/DataTable/DataTableLazy.stories.tsx new file mode 100644 index 0000000..6613feb --- /dev/null +++ b/src/components/tables/DataTable/DataTableLazy.stories.tsx @@ -0,0 +1,172 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState, useMemo, useCallback } from 'react'; + +import { delay } from '../util/async_util.ts'; +import { type User, generateData } from '../util/generateData.ts'; + +import { Button } from '../../actions/Button/Button.tsx'; +import { Panel } from '../../containers/Panel/Panel.tsx'; +import * as DataTableLazy from './DataTableLazy.tsx'; + +export default { + title: 'Components/Tables/DataTableLazy', + component: DataTableLazy.DataTableLazy, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +}; + +type dataTeableLazyTemplateProps = DataTableLazy.TableProviderLazyProps & { delay: number, items: Array }; +const DataTableLazyTemplate = (props: dataTeableLazyTemplateProps) => { + const columns = useMemo(() => props.columns, [props.columns]); + const items = useMemo(() => props.items, [props.items]); + const delayQuery = props.delay ?? null; + + const [itemsProcessed, setItemsProcessed] = useState>({ total: 0, itemsPage: [] }); + + const query: DataTableLazy.DataTableQuery = useCallback( + async ({ pageIndex, pageSize }) => { + if (delayQuery === Number.POSITIVE_INFINITY) return new Promise(() => {}); // Infinite delay + if (delayQuery === -1) throw new Error('Failed'); // Simulate failure + + if (delayQuery) await delay(delayQuery); + + // Simulate failure on page 4 + if (typeof delayQuery === 'number' && delayQuery > 0 && pageIndex + 1 === 4) { + throw new Error('Failed'); + } + + const itemsProcessedPage = items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); + + return { total: items.length, itemsPage: itemsProcessedPage }; + }, + [items, delayQuery] + ); + + return ( + + + + + + + + } + /> + } + /> + + + ); +}; + +// Column definitions +const columns = [ + { + id: 'name', + accessor: (user: User) => user.name, + Header: 'Name', + Cell: ({ value }: { value: string }) => value, + disableSortBy: false, + disableGlobalFilter: false, + }, + { + id: 'email', + accessor: (user: User) => user.email, + Header: 'Email', + disableSortBy: false, + disableGlobalFilter: false, + }, + { + id: 'company', + accessor: (user: User) => user.company, + Header: 'Company', + disableSortBy: false, + disableGlobalFilter: false, + }, + { + id: 'joinDate', + accessor: (user: User) => user.joinDate, + Header: 'Joined', + Cell: ({ value }: { value: Date }) => + value.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }), + disableSortBy: false, + disableGlobalFilter: false, + }, +]; + +// Stories +export const Empty = { + args: { + columns, + items: generateData({ numItems: 0 }), + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const SinglePage = { + args: { + columns, + items: generateData({ numItems: 10 }), + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const MultiplePagesSmall = { + args: { + columns, + items: generateData({ numItems: 45 }), + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const MultiplePagesLarge = { + args: { + columns, + items: generateData({ numItems: 1000 }), + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const SlowNetwork = { + args: { + columns, + items: generateData({ numItems: 1000 }), + delay: 1500, + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const InfiniteDelay = { + args: { + columns, + items: generateData({ numItems: 50 }), + delay: Number.POSITIVE_INFINITY, + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const StatusFailure = { + args: { + columns, + items: generateData({ numItems: 1000 }), + delay: -1, + }, + render: (args: dataTeableLazyTemplateProps) => , +}; diff --git a/src/components/tables/DataTable/DataTableLazy.tsx b/src/components/tables/DataTable/DataTableLazy.tsx index bb7e32b..c95ef51 100644 --- a/src/components/tables/DataTable/DataTableLazy.tsx +++ b/src/components/tables/DataTable/DataTableLazy.tsx @@ -3,20 +3,20 @@ |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { classNames as cx, ClassNameArgument } from '../../../util/component_util'; -import { useEffectAsync } from '../../../util/hooks'; +import { classNames as cx, ClassNameArgument } from '../../../util/componentUtil.ts'; +import { useEffectAsync } from '../util/hooks.ts'; -import { Loader } from '../../overlays/loader/Loader'; -import { Button } from '../../buttons/Button'; +import { Spinner } from '../../graphics/Spinner/Spinner.tsx'; +import { Button } from '../../actions/Button/Button.tsx'; import * as ReactTable from 'react-table'; -import { DataTableStatus, TableContextState, createTableContext, useTable } from './DataTableContext'; +import { type DataTableStatus, type TableContextState, createTableContext, useTable } from './DataTableContext.tsx'; import { Pagination } from './pagination/Pagination'; import { DataTablePlaceholderError } from './table/DataTablePlaceholder'; import { DataTableAsync } from './table/DataTable'; +import { Icon } from '../../graphics/Icon/Icon.tsx'; -import './DataTableLazy.scss'; -import { BaklavaIcon } from '../../icons/icon-pack-baklava/BaklavaIcon'; +// import './DataTableLazy.scss'; export * from './DataTableContext'; @@ -139,10 +139,10 @@ export const TableProviderLazy = (props: TableProviderLazyProp // Controlled table state const [pageSize, setPageSize] = React.useState(initialState?.pageSize ?? 10); - const tableOptions = { + const tableOptions: ReactTable.TableOptions = { columns, data: items.itemsPage, - getRowId, + ...(getRowId && { getRowId }), // Add `getRowId` only if it is defined }; const table = ReactTable.useTable( { @@ -280,15 +280,15 @@ export const DataTableLazy = ({ className, footer, ...propsRest }: DataTableLazy placeholderError={ { reload(); }}> - Retry + } /> } {...propsRest} > - {showLoadingIndicator && } + {showLoadingIndicator && } ); }; diff --git a/src/components/tables/DataTable/DataTableStream.scss b/src/components/tables/DataTable/DataTableStream.scss index 55eafbf..d0f2b4a 100644 --- a/src/components/tables/DataTable/DataTableStream.scss +++ b/src/components/tables/DataTable/DataTableStream.scss @@ -2,7 +2,7 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use '../../../style/variables.scss' as bkl; +// @use '../../../style/variables.scss' as bkl; @use './DataTableLazy.scss' as dataTableLazy; diff --git a/src/components/tables/DataTable/DataTableStream.stories.tsx b/src/components/tables/DataTable/DataTableStream.stories.tsx new file mode 100644 index 0000000..64852f6 --- /dev/null +++ b/src/components/tables/DataTable/DataTableStream.stories.tsx @@ -0,0 +1,220 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React, { useState, useMemo, useCallback } from 'react'; + +import { delay } from '../util/async_util.ts'; +import { generateData, type User } from '../util/generateData.ts'; + +import { Button } from '../../actions/Button/Button.tsx'; +import { Panel } from '../../containers/Panel/Panel.tsx'; +import * as DataTableStream from './DataTableStream.tsx'; + +export default { + title: 'Components/Tables/DataTableStream', + component: DataTableStream.DataTableStream, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + className: { + type: { name: 'string', required: false }, + description: 'CSS class name', + control: { type: 'text' }, + }, + columns: { + type: { name: 'array', required: true }, + description: 'Table columns', + control: { type: 'object' }, + }, + endOfStream: { + type: { name: 'boolean', required: false }, + description: 'End of stream flag', + control: { type: 'boolean' }, + }, + }, +}; +type UserPageState = { + offsetTasks: number, + offsetApprovalRequests: number, +}; +type dataTeableLazyTemplateProps = DataTableStream.TableProviderStreamProps & +{ delay: number, items: Array, endOfStream: boolean }; +const DataTableStreamTemplate = (props : dataTeableLazyTemplateProps) => { + const columns = useMemo(() => props.columns, [props.columns]); + const items = useMemo(() => props.items, [props.items]); + const delayQuery = props.delay ?? null; + + const [itemsProcessed, setItemsProcessed] = useState>([]); + + const query: DataTableStream.DataTableQuery = useCallback( + async ({ previousItem, previousPageState, limit, orderings, globalFilter }) => { + if (delayQuery === Number.POSITIVE_INFINITY) return new Promise(() => {}); // Infinite delay + if (delayQuery === -1) throw new Error('Failed'); // Simulate failure + + if (delayQuery) await delay(delayQuery); + + const previousItemIndex = items.indexOf(previousItem); + const offset = previousItemIndex === -1 ? 0 : previousItemIndex + 1; + + const filteredItems = items + .filter((row) => { + if (!globalFilter || globalFilter.trim() === '') return true; + + const columnsFilterable = columns.filter((column) => !column.disableGlobalFilter); + if (!columnsFilterable.length) return false; + + return columnsFilterable.some((column) => { + const cell = typeof column.accessor === 'function' ? column.accessor(row) : undefined; + return typeof cell === 'string' && cell.toLowerCase().includes(globalFilter.trim().toLowerCase()); + }); + }) + .sort((a, b) => { + if (!orderings.length) return 0; + const { column, direction } = orderings[0]; + const factor = direction === 'DESC' ? -1 : 1; + return a[column]?.localeCompare(b[column]) * factor || 0; + }) + .slice(offset, offset + limit); + + return { itemsPage: filteredItems, pageState: null, isEndOfStream: props.endOfStream }; + }, + [items, columns, props.endOfStream, delayQuery] + ); + + return ( + + + + + + + ); +}; + +// Column definitions +const columnDefinitions = [ + { + id: 'name', + accessor: (user: User) => user.name, + Header: 'Name', + Cell: ({ value }: { value: string }) => value, + disableSortBy: false, + disableGlobalFilter: false, + }, + { + id: 'email', + accessor: (user: User) => user.email, + Header: 'Email', + disableSortBy: false, + disableGlobalFilter: false, + }, + { + id: 'company', + accessor: (user: User) => user.company, + Header: 'Company', + disableSortBy: false, + disableGlobalFilter: false, + }, + { + id: 'joinDate', + accessor: (user: User) => user.joinDate, + Header: 'Joined', + Cell: ({ value }: { value: Date }) => + value.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }), + disableSortBy: false, + disableGlobalFilter: true, + }, +]; + +// Stories +export const Empty = { + args: { + columns: columnDefinitions, + items: generateData({ numItems: 0 }), + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const SinglePage = { + args: { + columns: columnDefinitions, + items: generateData({ numItems: 10 }), + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const MultiplePagesSmall = { + args: { + columns: columnDefinitions, + items: generateData({ numItems: 45 }), + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const MultiplePagesLarge = { + args: { + columns: columnDefinitions, + items: generateData({ numItems: 1000 }), + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const SlowNetwork = { + args: { + columns: columnDefinitions, + items: generateData({ numItems: 1000 }), + delay: 1500, + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const InfiniteDelay = { + args: { + columns: columnDefinitions, + items: generateData({ numItems: 50 }), + delay: Number.POSITIVE_INFINITY, + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const StatusFailure = { + args: { + columns: columnDefinitions, + items: generateData({ numItems: 1000 }), + delay: -1, + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const WithEndOfTablePlaceholder = { + args: { + columns: columnDefinitions, + items: generateData({ numItems: 15 }), + dataTableProps: { + placeholderEndOfTable: 'I have no idea', + }, + }, + render: (args: dataTeableLazyTemplateProps) => , +}; + +export const WithExplicitEndOfStream = { + args: { + columns: columnDefinitions, + items: generateData({ numItems: 15 }), + endOfStream: false, + }, + render: (args: dataTeableLazyTemplateProps) => , +}; diff --git a/src/components/tables/DataTable/DataTableStream.tsx b/src/components/tables/DataTable/DataTableStream.tsx index 0a9c030..e8ca23b 100644 --- a/src/components/tables/DataTable/DataTableStream.tsx +++ b/src/components/tables/DataTable/DataTableStream.tsx @@ -3,24 +3,24 @@ |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { classNames as cx } from '../../../util/component_util'; +import { classNames as cx } from '../../../util/componentUtil.ts'; -import { Loader } from '../../overlays/loader/Loader'; -import { Button } from '../../buttons/Button'; import * as ReactTable from 'react-table'; -import { DataTableStatus, TableContextState, createTableContext, useTable } from './DataTableContext'; +import { type DataTableStatus, type TableContextState, createTableContext, useTable } from './DataTableContext'; import { PaginationStream } from './pagination/PaginationStream'; import { DataTablePlaceholderError } from './table/DataTablePlaceholder'; import { DataTableAsync } from './table/DataTable'; -import type { FilterQuery } from '../../../prefab/forms/MultiSearch/MultiSearch'; +import type { FilterQuery } from '../MultiSearch/filterQuery.ts'; // Table plugins import { useCustomFilters } from './plugins/useCustomFilters'; // Styles -import './DataTableStream.scss'; +// import './DataTableStream.scss'; +import { Spinner } from '../../graphics/Spinner/Spinner.tsx'; +import { Button } from '../../actions/Button/Button.tsx'; export * from './DataTableContext'; @@ -51,9 +51,8 @@ const usePageHistory = () => { const keys = [...pageHistory.keys()]; if (keys.length === 0 || keys[keys.length - 1] === pageIndex) { return pageHistory; // Don't update if we don't need to (optimization) - } else { - return new Map([...pageHistory.entries()].filter(([pageIndexCurrent]) => pageIndexCurrent <= pageIndex)); } + return new Map([...pageHistory.entries()].filter(([pageIndexCurrent]) => pageIndexCurrent <= pageIndex)); }, [], ); @@ -75,7 +74,7 @@ const usePageHistory = () => { const lastIndex: undefined | PageIndex = indices[indices.length - 1]; // Make sure the page indices are contiguous if (pageIndex > lastIndex + 1) { - throw new Error(`Non-contiguous page indices`); // Should never happen + throw new Error('Non-contiguous page indices'); // Should never happen } const history = new Map(pageHistory).set(pageIndex, pageHistoryItem); @@ -94,7 +93,7 @@ const usePageHistory = () => { return pageHistoryApi(pageHistory); }; -export type DataTableQueryResult = { +export type DataTableQueryResult = { itemsPage: ReactTableOptions['data'], // Custom page state to be stored in page history pageState?: P, @@ -102,7 +101,7 @@ export type DataTableQueryResult = +export type DataTableQuery = (params: { previousItem: null | D, previousPageState?: undefined | P, @@ -116,7 +115,7 @@ export type DataTableQuery = customFilters: FilterQuery, }) => Promise>; -export type TableProviderStreamProps = { +export type TableProviderStreamProps = { children: React.ReactNode, columns: ReactTableOptions['columns'], getRowId: ReactTableOptions['getRowId'], @@ -131,7 +130,7 @@ export type TableProviderStreamProps) => void, }; -export const TableProviderStream = ( +export const TableProviderStream = ( props: TableProviderStreamProps, ) => { const { @@ -165,7 +164,7 @@ export const TableProviderStream = ( { @@ -440,7 +439,6 @@ export const TableProviderStream = { - return ; + return ; }; // Use `` by default, unless the table is empty (in which case there are "zero" pages) @@ -496,7 +494,7 @@ export const DataTableStream = ({ ? null : ( <>} /> ); const footerWithFallback = typeof footer === 'undefined' ? footerDefault : footer; @@ -514,7 +512,7 @@ export const DataTableStream = ({ placeholderError={ { reload(); }}> + } @@ -527,7 +525,7 @@ export const DataTableStream = ({ } {...propsRest} > - {showLoadingIndicator && } + {showLoadingIndicator && } ); }; diff --git a/src/components/tables/DataTable/filtering/Filtering.ts b/src/components/tables/DataTable/filtering/Filtering.ts index 817a873..2553ebd 100644 --- a/src/components/tables/DataTable/filtering/Filtering.ts +++ b/src/components/tables/DataTable/filtering/Filtering.ts @@ -12,7 +12,7 @@ import type { Field, TypeOfFieldSpec, TypeOfFieldsSpec, -} from '../../../../prefab/forms/MultiSearch/MultiSearch'; +} from '../../MultiSearch/filterQuery.ts'; type Primitive = string | number; type Uuid = string; @@ -24,29 +24,27 @@ const parseDateTime = (date: Date): number => { const parseStringField = (field: Primitive) => { if (typeof field === 'string') { return field.trim().toLowerCase(); - } else { - return field; } + return field; }; const numericOperation = (numericField: number, operation: FieldQuery['operation']): boolean => { if ('$eq' in operation) { return numericField === operation.$eq; - } else if ('$ne' in operation) { + } if ('$ne' in operation) { return numericField !== operation.$ne; - } else if ('$gte' in operation) { + } if ('$gte' in operation) { return numericField >= operation.$gte; - } else if ('$gt' in operation) { + } if ('$gt' in operation) { return numericField > operation.$gt; - } else if ('$lte' in operation) { + } if ('$lte' in operation) { return numericField <= operation.$lte; - } else if ('$lt' in operation) { + } if ('$lt' in operation) { return numericField < operation.$lt; - } else if ('$range' in operation) { + } if ('$range' in operation) { return numericField >= operation.$range[0] && numericField <= operation.$range[1]; - } else { - throw new TypeError(`Unknown query operator`); } + throw new TypeError('Unknown query operator'); }; const matchesFieldQuery = ( @@ -61,12 +59,11 @@ const matchesFieldQuery = ( } case 'text': { const fieldAsString = parseStringField(field as Primitive) as string; // Unsafe but guaranteed by `S` - + if ('$text' in operation) { return fieldAsString.includes(operation.$text.$search.toLowerCase()); - } else { - throw new TypeError(`Unknown query operator`); } + throw new TypeError('Unknown query operator'); } case 'datetime': { const fieldAsDate = parseDateTime(field as Date); // Unsafe but guaranteed by `TypeOfFieldSpec` @@ -74,124 +71,113 @@ const matchesFieldQuery = ( } case 'array': { const fieldAsArray = field as Array>; // Unsafe but guaranteed by `S` - + if ('$eq' in operation) { return fieldAsArray.every(field => (operation.$eq as typeof fieldAsArray).indexOf(field) >= 0); - } else if ('$ne' in operation) { + } if ('$ne' in operation) { return fieldAsArray.every(field => (operation.$ne as typeof fieldAsArray).indexOf(field) < 0); - } else if ('$all' in operation) { + } if ('$all' in operation) { const elementFieldSpec = fieldSpec.subfield; return fieldAsArray.every(element => { if ('$and' in operation.$all && Array.isArray(operation.$all.$and)) { const operations = operation.$all.$and; return operations.every(operation => matchesFieldQuery(elementFieldSpec, element, operation)); - } else if ('$or' in operation.$all && Array.isArray(operation.$all.$or)) { + } if ('$or' in operation.$all && Array.isArray(operation.$all.$or)) { const operations = operation.$all.$or; return operations.some(operation => matchesFieldQuery(elementFieldSpec, element, operation)); - } else { - throw new TypeError(`Unsupported array operation`); } + throw new TypeError('Unsupported array operation'); }); - } else if ('$any' in operation) { + } if ('$any' in operation) { return fieldAsArray.some(element => { const elementFieldSpec = fieldSpec.subfield; if ('$and' in operation.$any && Array.isArray(operation.$any.$and)) { const operations = operation.$any.$and; return operations.every(operation => matchesFieldQuery(elementFieldSpec, element, operation)); - } else if ('$or' in operation.$any && Array.isArray(operation.$any.$or)) { + } if ('$or' in operation.$any && Array.isArray(operation.$any.$or)) { const operations = operation.$any.$or; return operations.some(operation => matchesFieldQuery(elementFieldSpec, element, operation)); - } else { - throw new TypeError(`Unsupported array operation`); } + throw new TypeError('Unsupported array operation'); }); - } else { - throw new TypeError(`Unknown query operator`); } + throw new TypeError('Unknown query operator'); } case 'dictionary': { const fieldAsDictionary = field as string; // Unsafe but guaranteed by `S` - + if ('$all' in operation) { return fieldAsDictionary.includes(Object.values(operation.$all)[0]); - } else { - throw new TypeError(`Unknown query operator`); } + throw new TypeError('Unknown query operator'); } case 'enum': { const fieldAsEnum = field as string; // Unsafe but guaranteed by `S` - + if ('$in' in operation) { return operation.$in.indexOf(fieldAsEnum) !== -1; - } else if ('$nin' in operation) { + } if ('$nin' in operation) { return operation.$nin.indexOf(fieldAsEnum) === -1; - } else if ('$eq' in operation) { + } if ('$eq' in operation) { return fieldAsEnum.includes(operation.$eq as string); - } else if ('$ne' in operation) { + } if ('$ne' in operation) { return !fieldAsEnum.includes(operation.$ne as string); - } else { - throw new TypeError(`Unknown query operator`); } + throw new TypeError('Unknown query operator'); } case 'record': { const fieldAsRecord = field as TypeOfFieldsSpec; // Unsafe but guaranteed by `S` - + if ('$all' in operation) { return Object.values(fieldAsRecord).every(element => { const elementFieldSpec = Object.values(fieldSpec.fields)[0]; if ('$and' in operation.$all && Array.isArray(operation.$all.$and)) { const operations = operation.$all.$and; return operations.every(operation => matchesFieldQuery(elementFieldSpec, element, operation)); - } else if ('$or' in operation.$all && Array.isArray(operation.$all.$or)) { + } if ('$or' in operation.$all && Array.isArray(operation.$all.$or)) { const operations = operation.$all.$or; return operations.some(operation => matchesFieldQuery(elementFieldSpec, element, operation)); - } else { - const fieldName: keyof RecordFieldSpec['fields'] = Object.keys(operation.$all)[0]; - const operations: FieldQuery['operation'] = Object.values(operation.$all)[0]; - if (typeof element === 'object' && element !== null && !Array.isArray(element)) { - const item = element as Record; - return matchesFieldQuery(elementFieldSpec, item[fieldName], operations); - } else { - return matchesFieldQuery(elementFieldSpec, element, operations); - } } + const fieldName: keyof RecordFieldSpec['fields'] = Object.keys(operation.$all)[0]; + const operations: FieldQuery['operation'] = Object.values(operation.$all)[0]; + if (typeof element === 'object' && element !== null && !Array.isArray(element)) { + const item = element as Record; + return matchesFieldQuery(elementFieldSpec, item[fieldName], operations); + } + return matchesFieldQuery(elementFieldSpec, element, operations); }); - } else if ('$any' in operation) { + } if ('$any' in operation) { return Object.values(fieldAsRecord).some(element => { const elementFieldSpec = Object.values(fieldSpec.fields)[0]; if ('$and' in operation.$any && Array.isArray(operation.$any.$and)) { const operations = operation.$any.$and; return operations.every(operation => matchesFieldQuery(elementFieldSpec, element, operation)); - } else if ('$or' in operation.$any && Array.isArray(operation.$any.$or)) { + } if ('$or' in operation.$any && Array.isArray(operation.$any.$or)) { const operations = operation.$any.$or; return operations.some(operation => matchesFieldQuery(elementFieldSpec, element, operation)); - } else { - const fieldName: keyof RecordFieldSpec['fields'] = Object.keys(operation.$any)[0]; - const operations: FieldQuery['operation'] = Object.values(operation.$any)[0]; - if (typeof element === 'object' && element !== null && !Array.isArray(element)) { - const item = element as Record; - return matchesFieldQuery(elementFieldSpec, item[fieldName], operations); - } else { - return matchesFieldQuery(elementFieldSpec, element, operations); - } } + const fieldName: keyof RecordFieldSpec['fields'] = Object.keys(operation.$any)[0]; + const operations: FieldQuery['operation'] = Object.values(operation.$any)[0]; + if (typeof element === 'object' && element !== null && !Array.isArray(element)) { + const item = element as Record; + return matchesFieldQuery(elementFieldSpec, item[fieldName], operations); + } + return matchesFieldQuery(elementFieldSpec, element, operations); }); - } else { - throw new TypeError(`Unknown query operator`); } + throw new TypeError('Unknown query operator'); } - default: throw new TypeError(`Unknown field type`); + default: throw new TypeError('Unknown field type'); } }; const getFieldValue = (fieldSpec: Field, item: TypeOfFieldsSpec, fieldName: string) => { if (fieldSpec.accessor) { return fieldSpec.accessor(item); - } else if (fieldName !== '') { + } if (fieldName !== '') { return item[fieldName]; - } else { - throw new TypeError('Unable to get field value, expected either `accessor` or `fieldName` to be configured'); } + throw new TypeError('Unable to get field value, expected either `accessor` or `fieldName` to be configured'); }; // Take some data that corresponds to the given spec (`Fields`), and return that data filtered through the given query @@ -221,7 +207,6 @@ export const filterByQuery = ( {} as Record, ); return itemsFiltered; - } else { - return items; } + return items; }; diff --git a/src/components/tables/DataTable/pagination/Pagination.scss b/src/components/tables/DataTable/pagination/Pagination.scss index 011aa53..f5c1543 100644 --- a/src/components/tables/DataTable/pagination/Pagination.scss +++ b/src/components/tables/DataTable/pagination/Pagination.scss @@ -2,8 +2,8 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use '../../../../style/variables.scss' as bkl; -@use '../../../../style/mixins.scss' as mixins; +// @use '../../../../style/variables.scss' as bkl; +// @use '../../../../style/mixins.scss' as mixins; .pagination { diff --git a/src/components/tables/DataTable/pagination/Pagination.tsx b/src/components/tables/DataTable/pagination/Pagination.tsx index c771338..307db42 100644 --- a/src/components/tables/DataTable/pagination/Pagination.tsx +++ b/src/components/tables/DataTable/pagination/Pagination.tsx @@ -4,37 +4,43 @@ import cx from 'classnames'; import * as React from 'react'; -import { joinElements } from '../../../../util/component_util'; +import { joinElements } from '../../../../util/componentUtil.ts'; -import { SpriteIcon as Icon } from '../../../icons/Icon'; +import { Icon } from '../../../graphics/Icon/Icon.tsx'; -import { PageSizeOption, PaginationSizeSelector } from './PaginationSizeSelector'; -import { useTable } from '../DataTableContext'; +import { type PageSizeOption, PaginationSizeSelector } from './PaginationSizeSelector.tsx'; +import { useTable } from '../DataTableContext.tsx'; -import './Pagination.scss'; +// import './Pagination.scss'; type PageOptionsSegment = Array; // Consecutive list of page indices (e.g. `[5, 6, 7]`) type PageOptions = Array; // List of segments (e.g. `[[1], [49, 50, 51], [100]]`) const combineSegments = (segment1: PageOptionsSegment, segment2: PageOptionsSegment): PageOptions => { - if (segment1.length === 0 || segment2.length === 0) { return [[...segment1, ...segment2]]; } - - const gapLeft = segment1.slice(-1)[0]; // Element to the left side of the gap (i.e. the last element of `segment1`) - const gapRight = segment2[0]; // Element to the right of the gap (i.e. the first element of `segment2`) - const gapSize = gapRight - gapLeft - 1; // Calculate the gap (if 0, then the segments are consecutive, e.g. 3 to 4) - + if (segment1.length === 0 || segment2.length === 0) { + return [[...segment1, ...segment2]]; + } + + const gapLeft = segment1[segment1.length - 1]; // Last element of `segment1` + const gapRight = segment2[0]; // First element of `segment2` + + // Ensure `gapLeft` and `gapRight` are defined before proceeding + if (gapLeft === undefined || gapRight === undefined) { + return [segment1, segment2]; + } + + const gapSize = gapRight - gapLeft - 1; // Calculate the gap if (gapSize > 1) { // If there is a gap between the segments larger than one, leave unmerged return [segment1, segment2]; - } else if (gapSize === 1) { + } if (gapSize === 1) { // If the gap is 1 (i.e. there is only one element "missing" in between), fill it in and merge // Motivation: there will be a separator between gaps (e.g. `4 5 ... 7 8`), so if there is only element in between, // then it makes sense to replace the separator with the missing element explicitly. return [[...segment1, gapLeft + 1, ...segment2]]; - } else { - // If there is no gap, combine the two segments (removing any overlapping elements) - return [[...segment1, ...segment2.filter((pageIndex: number) => pageIndex > gapLeft)]]; } + // If there is no gap, combine the two segments (removing any overlapping elements) + return [[...segment1, ...segment2.filter((pageIndex) => pageIndex > gapLeft)]]; }; const getPageOptions = ({ pageCount, pageIndex }: { pageCount: number, pageIndex: number }): PageOptions => { const pageIndexFirst = 0; @@ -54,11 +60,13 @@ const getPageOptions = ({ pageCount, pageIndex }: { pageCount: number, pageIndex return pageIndex >= pageIndexFirst && pageIndex <= pageIndexLast; }); - if (pageOptions.length === 0) { return [segment]; } + if (pageOptions.length === 0) { + return [segment]; + } // Split `pageOptions` into its last segment, and everything before: `[...pageOptionsBase, segmentPrior]` const pageOptionsBase: PageOptions = pageOptions.slice(0, -1); - const segmentPrior: PageOptionsSegment = pageOptions.slice(-1)[0]; + const segmentPrior: PageOptionsSegment = pageOptions.slice(-1)[0] || []; // Attempt to combine `segmentPrior` and `segment` into one consecutive segment (if there's no gap in between) return [...pageOptionsBase, ...combineSegments(segmentPrior, segment)]; @@ -78,7 +86,6 @@ export const Pagination = ({ pageSizeOptions }: PaginationProps) => { - table.state.pageIndex - table.state.pageSize - table.canPreviousPage - - table.canPreviousPage - table.canNextPage - table.pageOptions - table.pageCount @@ -94,11 +101,13 @@ export const Pagination = ({ pageSizeOptions }: PaginationProps) => { return (
- +
- @@ -106,22 +115,31 @@ export const Pagination = ({ pageSizeOptions }: PaginationProps) => { {joinElements( // Join the segments together with separator
  • elements inserted in between
  • , pageOptions - .map((pageOptionsSegment: PageOptionsSegment) => - <> - {pageOptionsSegment.map((pageIndex: number) => -
  • { table.gotoPage(pageIndex); }} - > - {pageIndex + 1} -
  • , - )} - , - ), + .map((pageOptionsSegment, segmentIndex) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + {pageOptionsSegment.map((pageIndex) => ( +
  • { + table.gotoPage(pageIndex); + }} + onKeyDown={() => { + table.gotoPage(pageIndex); + }} + > + {pageIndex + 1} +
  • + ))} +
    + )) )} - diff --git a/src/components/tables/DataTable/pagination/PaginationSizeSelector.scss b/src/components/tables/DataTable/pagination/PaginationSizeSelector.scss index d987609..1e8f932 100644 --- a/src/components/tables/DataTable/pagination/PaginationSizeSelector.scss +++ b/src/components/tables/DataTable/pagination/PaginationSizeSelector.scss @@ -2,8 +2,8 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use '../../../../style/variables.scss' as bkl; -@use '../../../../style/mixins.scss' as mixins; +// @use '../../../../style/variables.scss' as bkl; +// @use '../../../../style/mixins.scss' as mixins; .page-size-selector { diff --git a/src/components/tables/DataTable/pagination/PaginationSizeSelector.tsx b/src/components/tables/DataTable/pagination/PaginationSizeSelector.tsx index 877875c..3867b5f 100644 --- a/src/components/tables/DataTable/pagination/PaginationSizeSelector.tsx +++ b/src/components/tables/DataTable/pagination/PaginationSizeSelector.tsx @@ -2,55 +2,59 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import cx from 'classnames'; -import * as React from 'react'; +import type React from 'react'; -import { SpriteIcon as Icon } from '../../../icons/Icon'; -import { Button } from '../../../buttons/Button'; -import { Dropdown } from '../../../overlays/dropdown/Dropdown'; +import { Icon } from '../../../graphics/Icon/Icon.tsx'; +import { Button } from '../../../actions/Button/Button.tsx'; +import { DropdownMenuProvider } from '../../../overlays/DropdownMenu/DropdownMenuProvider.tsx'; -import { useTable } from '../DataTableContext'; +import { useTable } from '../DataTableContext.tsx'; -import './PaginationSizeSelector.scss'; +// import './PaginationSizeSelector.scss'; export type PageSizeOption = number; export const defaultPageSizeOptions: Array = [10, 25, 50, 100]; type PaginationSizeSelectorProps = { - pageSizeOptions?: Array, - pageSizeLabel?: string; + pageSizeOptions?: Array | undefined, + pageSizeLabel?: string | undefined, }; export const PaginationSizeSelector = (props: PaginationSizeSelectorProps) => { const { pageSizeOptions = defaultPageSizeOptions, pageSizeLabel = 'Items per page' } = props; - + const { table } = useTable(); - + return (
    {pageSizeLabel}: - - + + ( + { + table.setPageSize(pageSize); + context.close(); + }} + /> + ))} + > + {({ props }) => ( + - } - > - {({ close }) => - pageSizeOptions.map(pageSize => - { table.setPageSize(pageSize); close(); }} - > - {pageSize} - , - ) - } - + )} +
    ); -}; +}; \ No newline at end of file diff --git a/src/components/tables/DataTable/pagination/PaginationStream.scss b/src/components/tables/DataTable/pagination/PaginationStream.scss index bf9ead0..acff7cf 100644 --- a/src/components/tables/DataTable/pagination/PaginationStream.scss +++ b/src/components/tables/DataTable/pagination/PaginationStream.scss @@ -2,8 +2,8 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use '../../../../style/variables.scss' as bkl; -@use '../../../../style/mixins.scss' as mixins; +// @use '../../../../style/variables.scss' as bkl; +// @use '../../../../style/mixins.scss' as mixins; @use './Pagination.scss'; diff --git a/src/components/tables/DataTable/pagination/PaginationStream.tsx b/src/components/tables/DataTable/pagination/PaginationStream.tsx index d1fbd00..110b289 100644 --- a/src/components/tables/DataTable/pagination/PaginationStream.tsx +++ b/src/components/tables/DataTable/pagination/PaginationStream.tsx @@ -3,15 +3,15 @@ |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import cx from 'classnames'; -import * as React from 'react'; +import type * as React from 'react'; -import { SpriteIcon as Icon } from '../../../icons/Icon'; -import { Button } from '../../../buttons/Button'; +import { Icon } from '../../../graphics/Icon/Icon.tsx'; +import { Button } from '../../../actions/Button/Button.tsx'; -import { PageSizeOption, PaginationSizeSelector } from './PaginationSizeSelector'; -import { useTable } from '../DataTableContext'; +import { type PageSizeOption, PaginationSizeSelector } from './PaginationSizeSelector.tsx'; +import { useTable } from '../DataTableContext.tsx'; -import './PaginationStream.scss'; +// import './PaginationStream.scss'; type IconDoubleChevronLeftProps = React.ComponentPropsWithoutRef<'span'> & { @@ -20,10 +20,10 @@ type IconDoubleChevronLeftProps = React.ComponentPropsWithoutRef<'span'> & { const IconDoubleChevronLeft = ({ iconProps = {}, ...props }: IconDoubleChevronLeftProps) => { return ( - - @@ -38,27 +38,30 @@ export const PaginationStreamPager = ({ pageSizeOptions }: PaginationStreamPager return (
    - - -
    ); diff --git a/src/components/tables/DataTable/plugins/useCustomFilters.tsx b/src/components/tables/DataTable/plugins/useCustomFilters.tsx index c74c527..0e5266e 100644 --- a/src/components/tables/DataTable/plugins/useCustomFilters.tsx +++ b/src/components/tables/DataTable/plugins/useCustomFilters.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import * as ReactTable from 'react-table'; -import { type FilterQuery } from '../../MultiSearch/MultiSearch.tsx'; +import type { FilterQuery } from '../../MultiSearch/filterQuery.ts'; // Actions diff --git a/src/components/tables/DataTable/plugins/useRowSelectColumn.scss b/src/components/tables/DataTable/plugins/useRowSelectColumn.scss index ef3aec8..b77f469 100644 --- a/src/components/tables/DataTable/plugins/useRowSelectColumn.scss +++ b/src/components/tables/DataTable/plugins/useRowSelectColumn.scss @@ -2,7 +2,7 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use '../../../../style/variables.scss' as bkl; +// @use '../../../../style/variables.scss' as bkl; .bkl-data-table-row-select { width: bkl.$sizing-7 + bkl.$sizing-1; diff --git a/src/components/tables/DataTable/plugins/useRowSelectColumn.tsx b/src/components/tables/DataTable/plugins/useRowSelectColumn.tsx index be78f0f..71061e8 100644 --- a/src/components/tables/DataTable/plugins/useRowSelectColumn.tsx +++ b/src/components/tables/DataTable/plugins/useRowSelectColumn.tsx @@ -3,11 +3,11 @@ |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import * as ReactTable from 'react-table'; +import type * as ReactTable from 'react-table'; import { Checkbox } from '../../../forms/controls/Checkbox/Checkbox.tsx'; -import './useRowSelectColumn.scss'; +// import './useRowSelectColumn.scss'; // `react-table` plugin for row selection column. Note: depends on `react-table`'s `useRowSelect` plugin. diff --git a/src/components/tables/DataTable/table/DataTable.scss b/src/components/tables/DataTable/table/DataTable.scss index a8307cb..638471e 100644 --- a/src/components/tables/DataTable/table/DataTable.scss +++ b/src/components/tables/DataTable/table/DataTable.scss @@ -2,8 +2,8 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use '../../../../style/variables.scss' as bkl; -@use '../../../../style/mixins.scss' as mixins; +// @use '../../../../style/variables.scss' as bkl; +// @use '../../../../style/mixins.scss' as mixins; .bkl-data-table { diff --git a/src/components/tables/DataTable/table/DataTable.tsx b/src/components/tables/DataTable/table/DataTable.tsx index dada864..673e428 100644 --- a/src/components/tables/DataTable/table/DataTable.tsx +++ b/src/components/tables/DataTable/table/DataTable.tsx @@ -2,9 +2,9 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as React from 'react'; +import type * as React from 'react'; import { classNames as cx, type ClassNameArgument, type ComponentProps } from '../../../../util/componentUtil.ts'; -import * as ReactTable from 'react-table'; +import type * as ReactTable from 'react-table'; import { Icon } from '../../../graphics/Icon/Icon.tsx'; diff --git a/src/components/tables/DataTable/table/DataTablePlaceholder.scss b/src/components/tables/DataTable/table/DataTablePlaceholder.scss index 76e7a62..936f962 100644 --- a/src/components/tables/DataTable/table/DataTablePlaceholder.scss +++ b/src/components/tables/DataTable/table/DataTablePlaceholder.scss @@ -2,8 +2,8 @@ |* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@use '../../../../style/variables.scss' as bkl; -@use '../../../../style/mixins.scss' as mixins; +// @use '../../../../style/variables.scss' as bkl; +// @use '../../../../style/mixins.scss' as mixins; .bk-table-placeholder { diff --git a/src/components/tables/MultiSearch/MultiSearch.stories.tsx b/src/components/tables/MultiSearch/MultiSearch.stories.tsx index 1fa02cc..1fc540a 100644 --- a/src/components/tables/MultiSearch/MultiSearch.stories.tsx +++ b/src/components/tables/MultiSearch/MultiSearch.stories.tsx @@ -5,25 +5,20 @@ import { getDay as dateGetDay, startOfDay as dateStartOfDay, endOfDay as dateEndOfDay, sub as dateSub } from 'date-fns'; import * as React from 'react'; -import * as StorybookKnobs from '@storybook/addon-knobs'; -import { StoryMetadata } from '../../../types/storyMetadata'; - -import { Panel } from '../../../components/containers/panel/Panel'; -import * as MultiSearch from './MultiSearch'; +import type * as FQ from './filterQuery.ts'; +import * as MultiSearch from './MultiSearch.tsx'; export default { - title: 'Prefab/Forms/MultiSearch', - decorators: [ - StorybookKnobs.withKnobs, - renderStory => {renderStory()}, - ], - component: MultiSearch, -} as StoryMetadata; - + component: MultiSearch, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +}; export const Standard = () => { - const severityFieldSpec: MultiSearch.EnumFieldSpec = { + const severityFieldSpec: FQ.EnumFieldSpec = { type: 'enum', operators: ['$eq', '$ne', '$in', '$nin'], label: 'Severity', @@ -35,7 +30,7 @@ export const Standard = () => { }, }; - const keyOpsFieldSpec: MultiSearch.ArrayFieldSpec = { + const keyOpsFieldSpec: FQ.ArrayFieldSpec = { type: 'array', operators: ['$eq', '$ne', '$any', '$all'], label: 'Key Ops', @@ -56,21 +51,21 @@ export const Standard = () => { }, }; - const initiatorFieldSpec: MultiSearch.TextFieldSpec = { + const initiatorFieldSpec: FQ.TextFieldSpec = { type: 'text', operators: ['$text'], label: 'Initiator', placeholder: 'Search initiator', }; - const countFieldSpec: MultiSearch.Field = { + const countFieldSpec: FQ.Field = { type: 'number', operators: ['$eq', '$lt', '$lte', '$gt', '$gte', '$ne'], label: 'Count', placeholder: 'Search ip-address', }; - const customAttributesFieldSpec: MultiSearch.DictionaryFieldSpec = { + const customAttributesFieldSpec: FQ.DictionaryFieldSpec = { type: 'dictionary', operators: ['$all'], label: 'Custom Attributes', @@ -80,7 +75,7 @@ export const Standard = () => { }, }; - const createdAtFieldSpec: MultiSearch.DateTimeFieldSpec = { + const createdAtFieldSpec: FQ.DateTimeFieldSpec = { type: 'datetime', operators: ['$gt', '$range'], label: 'Created', @@ -107,9 +102,9 @@ export const Standard = () => { }, }]; - const [filters, setFilters] = React.useState(defaultFilters); + const [filters, setFilters] = React.useState(defaultFilters); - const query = (filter: MultiSearch.FilterQuery) => setFilters(filter); + const query = (filter: FQ.FilterQuery) => setFilters(filter); return ( @@ -117,7 +112,7 @@ export const Standard = () => { }; export const WithValidation = () => { - const uuidFieldSpec: MultiSearch.TextFieldSpec = { + const uuidFieldSpec: FQ.TextFieldSpec = { type: 'text', operators: ['$text'], label: 'UUID', @@ -131,7 +126,7 @@ export const WithValidation = () => { }, }; - const severityFieldSpec: MultiSearch.EnumFieldSpec = { + const severityFieldSpec: FQ.EnumFieldSpec = { type: 'enum', operators: ['$eq', '$ne', '$in', '$nin'], label: 'Severity', @@ -150,7 +145,7 @@ export const WithValidation = () => { }, }; - const keyOpsFieldSpec: MultiSearch.ArrayFieldSpec = { + const keyOpsFieldSpec: FQ.ArrayFieldSpec = { type: 'array', operators: ['$eq', '$ne', '$any', '$all'], label: 'Key Ops', @@ -178,7 +173,7 @@ export const WithValidation = () => { }, }; - const countFieldSpec: MultiSearch.Field = { + const countFieldSpec: FQ.Field = { type: 'number', operators: ['$eq', '$lt', '$lte', '$gt', '$gte', '$ne'], label: 'Count', @@ -192,7 +187,7 @@ export const WithValidation = () => { }, }; - const customAttributesFieldSpec: MultiSearch.DictionaryFieldSpec = { + const customAttributesFieldSpec: FQ.DictionaryFieldSpec = { type: 'dictionary', operators: ['$all'], label: 'Custom attributes', @@ -209,7 +204,7 @@ export const WithValidation = () => { }, }; - const createdAtFieldSpec: MultiSearch.DateTimeFieldSpec = { + const createdAtFieldSpec: FQ.DateTimeFieldSpec = { type: 'datetime', operators: ['$gt', '$range'], label: 'Created', @@ -231,9 +226,9 @@ export const WithValidation = () => { createdAt: createdAtFieldSpec, }; - const [filters, setFilters] = React.useState([]); + const [filters, setFilters] = React.useState([]); - const query = (filter: MultiSearch.FilterQuery) => setFilters(filter); + const query = (filter: FQ.FilterQuery) => setFilters(filter); return ( diff --git a/src/components/tables/MultiSearch/MultiSearch.tsx b/src/components/tables/MultiSearch/MultiSearch.tsx index 8bc608c..a3a3f54 100644 --- a/src/components/tables/MultiSearch/MultiSearch.tsx +++ b/src/components/tables/MultiSearch/MultiSearch.tsx @@ -18,7 +18,7 @@ import { import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { classNames as cx, type ClassNameArgument, type ComponentProps } from '../../../util/componentUtil.ts'; -import * as Popper from 'react-popper'; +// import * as Popper from 'react-popper'; import { mergeRefs } from '../../../util/reactUtil.ts'; import { useOutsideClickHandler } from '../../../util/hooks/useOutsideClickHandler.ts'; import { useFocus } from '../../../util/hooks/useFocus.ts'; @@ -28,8 +28,9 @@ import { Tag } from '../../text/Tag/Tag.tsx'; import { Button } from '../../actions/Button/Button.tsx'; import { Input } from '../../forms/controls/Input/Input.tsx'; import { CheckboxGroup } from '../../forms/fields/CheckboxGroup/CheckboxGroup.tsx'; -import * as Dropdown from '../../overlays/dropdown/Dropdown.tsx'; -import { DateTimePicker } from '../../forms/datetime/DateTimePicker.tsx'; +// import * as Dropdown from '../../overlays/dropdown/Dropdown.tsx'; +import { DropdownMenu } from '../../overlays/DropdownMenu/DropdownMenu.tsx'; +// import { DateTimePicker } from '../../forms/datetime/DateTimePicker.tsx'; import * as FQ from './filterQuery.ts'; @@ -176,8 +177,8 @@ export const Filters = (props: FiltersProps) => { onRemoveAllFilters, } = props; - const renderDateTimeFilter = (filter: FieldQuery, index: number) => { - const { fieldName, operatorSymbol, operand } = decodeFieldQuery(filter, fields); + const renderDateTimeFilter = (filter: FQ.FieldQuery, index: number) => { + const { fieldName, operatorSymbol, operand } = FQ.decodeFieldQuery(filter, fields); const field = fieldName ? fields[fieldName] : null; const fieldNameLabel = typeof field?.label === 'string' ? field?.label : ''; let symbol = ':'; @@ -194,7 +195,7 @@ export const Filters = (props: FiltersProps) => { if (field && field.type === 'datetime') { if (operatorSymbol === 'Range') { - if (isRangeOperationValue(operand)) { + if (FQ.isRangeOperationValue(operand)) { const startDateTime = dateFormat(operand[0] * 1000, 'MMMM do yyyy HH:mm'); const endDateTime = dateFormat(operand[1] * 1000, 'MMMM do yyyy HH:mm'); operandLabel = { from: startDateTime, to: endDateTime }; @@ -233,7 +234,7 @@ export const Filters = (props: FiltersProps) => { }; const renderArrayFilter = (filter: FQ.FieldQuery, index: number) => { - const { fieldName, operatorSymbol, operand, subOperatorSymbol = '' } = decodeFieldQuery(filter, fields); + const { fieldName, operatorSymbol, operand, subOperatorSymbol = '' } = FQ.decodeFieldQuery(filter, fields); const field = fieldName ? fields[fieldName] : null; const subField = field && field.type === 'array' ? field.subfield : null; const fieldNameLabel = typeof field?.label === 'string' ? field?.label : ''; @@ -274,14 +275,15 @@ export const Filters = (props: FiltersProps) => { ); }; - const renderFilter = (filter: FieldQuery, index: number) => { - const { fieldName, operatorSymbol, operand } = decodeFieldQuery(filter, fields); + const renderFilter = (filter: FQ.FieldQuery, index: number) => { + const { fieldName, operatorSymbol, operand } = FQ.decodeFieldQuery(filter, fields); const field = fieldName ? fields[fieldName] : null; if (field) { if (field.type === 'datetime') { return renderDateTimeFilter(filter, index); - } else if (field.type === 'array') { + } + if (field.type === 'array') { return renderArrayFilter(filter, index); } } @@ -334,6 +336,7 @@ export const Filters = (props: FiltersProps) => { return filters.length > 0 && (
    role="button" tabIndex={0} className="clear-all" @@ -365,7 +368,7 @@ export const Filters = (props: FiltersProps) => { // Suggestions dropdown // -const SuggestionItem = Dropdown.Item; +const SuggestionItem = DropdownMenu.Action; export type SuggestionProps = Omit, 'children'> & { children: React.ReactNode | ((props: { close: () => void }) => React.ReactNode), @@ -375,7 +378,7 @@ export type SuggestionProps = Omit, 'children'> & { primary?: undefined | boolean, secondary?: undefined | boolean, basic?: undefined | boolean, - popperOptions?: undefined | Dropdown.PopperOptions, + // popperOptions?: undefined | Dropdown.PopperOptions, onOutsideClick?: undefined | (() => void), containerRef?: undefined | React.RefObject, }; @@ -389,7 +392,7 @@ export const Suggestions = (props: SuggestionProps) => { basic = false, children = '', elementRef, - popperOptions = {}, + // popperOptions = {}, onOutsideClick, containerRef, } = props; @@ -471,7 +474,7 @@ export const Suggestions = (props: SuggestionProps) => { }; export type SearchInputProps = ComponentProps & { - fields: Fields, + fields: FQ.Fields, fieldQueryBuffer: FieldQueryBuffer, inputRef: React.RefObject, }; @@ -549,6 +552,7 @@ export const SearchInput = (props: SearchInputProps) => { return (
    role="button" tabIndex={0} className={cx('bkl-search-input', className, { 'bkl-search-input--active': isFocused })} @@ -577,8 +581,8 @@ export const SearchInput = (props: SearchInputProps) => { type FieldsDropdownProps = { inputRef?: React.RefObject, isActive?: boolean, - fields?: Fields, - popperOptions?: Dropdown.PopperOptions, + fields?: FQ.Fields, + // popperOptions?: Dropdown.PopperOptions, onClick: (fieldName?: string) => void, onOutsideClick?: () => void, }; @@ -600,7 +604,7 @@ const FieldsDropdown = (props: FieldsDropdownProps) => { return ( { type AlternativesDropdownProps = { inputRef?: React.RefObject, isActive?: boolean, - operators?: EnumFieldOperator[] | ArrayFieldOperator[], - alternatives?: Alternatives, - popperOptions?: Dropdown.PopperOptions, - selectedOperator: Operator, + operators?: FQ.EnumFieldOperator[] | FQ.ArrayFieldOperator[], + alternatives?: FQ.Alternatives, + // popperOptions?: Dropdown.PopperOptions, + selectedOperator: FQ.Operator, onChange: (value: Primitive[]) => void, onOutsideClick?: () => void, - validator?: ArrayValidator, + validator?: FQ.ArrayValidator, }; const AlternativesDropdown = (props: AlternativesDropdownProps) => { @@ -722,7 +726,7 @@ const AlternativesDropdown = (props: AlternativesDropdownProps) => { { type DateTimeDropdownProps = { inputRef?: React.RefObject, isActive?: boolean, - popperOptions?: Dropdown.PopperOptions, + // popperOptions?: Dropdown.PopperOptions, onChange: (value: number | [number, number]) => void, onOutsideClick?: () => void, maxDate?: Date | number, minDate?: Date | number, - selectedDate?: SelectedDate, + selectedDate?: FQ.SelectedDate, canSelectDateTimeRange?: boolean, - validator?: DateTimeValidator, + validator?: FQ.DateTimeValidator, }; const DateTimeDropdown = (props: DateTimeDropdownProps) => { @@ -766,7 +770,7 @@ const DateTimeDropdown = (props: DateTimeDropdownProps) => { return !!(date && typeof date === 'number' || date instanceof Date); }; - const isValidSelectedDate = (selectedDate: SelectedDate | undefined) => { + const isValidSelectedDate = (selectedDate: FQ.SelectedDate | undefined) => { if (Array.isArray(selectedDate) && selectedDate.length === 2) { return isValidDateParamType(selectedDate[0]) && isValidDateParamType(selectedDate[1]); } @@ -780,7 +784,7 @@ const DateTimeDropdown = (props: DateTimeDropdownProps) => { : date; }; - const initDateTime = (selectedDate: SelectedDate | undefined, range: 'start' | 'end') => { + const initDateTime = (selectedDate: FQ.SelectedDate | undefined, range: 'start' | 'end') => { const defaultDate = setDate(new Date(), { seconds: 0, milliseconds: 0 }); if (!selectedDate) { return defaultDate; @@ -886,28 +890,27 @@ const DateTimeDropdown = (props: DateTimeDropdownProps) => { <>
    Start Date
    - + /> */}
    End Date
    - + /> */}
    - <> - {!dateTimeRangeValidation.isValid + {!dateTimeRangeValidation.isValid && dateTimeRangeValidation.message && ( @@ -915,7 +918,6 @@ const DateTimeDropdown = (props: DateTimeDropdownProps) => { ) } -