From d95149c5de10a3cd4ed1976819cff3ae026376bf Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Mon, 6 Jan 2025 18:28:35 -0500 Subject: [PATCH] vtadmin: enable sorting in all tables Signed-off-by: c-r-dev --- .../components/dataTable/SortedDataTable.tsx | 142 ++++++++++++++++++ web/vtadmin/src/components/routes/Schemas.tsx | 20 +-- 2 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 web/vtadmin/src/components/dataTable/SortedDataTable.tsx diff --git a/web/vtadmin/src/components/dataTable/SortedDataTable.tsx b/web/vtadmin/src/components/dataTable/SortedDataTable.tsx new file mode 100644 index 00000000000..f74e81c9c3c --- /dev/null +++ b/web/vtadmin/src/components/dataTable/SortedDataTable.tsx @@ -0,0 +1,142 @@ +/** + * Copyright 2025 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from 'react'; +import { useLocation } from 'react-router-dom'; + +import { useURLPagination } from '../../hooks/useURLPagination'; +import { useURLQuery } from '../../hooks/useURLQuery'; +import { stringify } from '../../util/queryString'; +import { PaginationNav } from './PaginationNav'; +import { useCallback, useMemo, useState } from 'react'; + +export interface ColumnProps { + // Coulmn display name string | JSX.Element + display: string| JSX.Element, + // Column data accessor + accessor: string +} + +interface Props { + // When passing a JSX.Element, note that the column element + // will be rendered *inside* a tag. (Note: I don't love this + // abstraction + we'll likely want to revisit this when we add + // table sorting.) + columns: Array; + data: T[]; + pageSize?: number; + renderRows: (rows: T[]) => JSX.Element[]; + title?: string; + // Pass a unique `pageKey` for each DataTable, in case multiple + // DataTables access the same URL. This will be used to + // access page number from the URL. + pageKey?: string; +} + +// Generally, page sizes of ~100 rows are fine in terms of performance, +// but anything over ~50 feels unwieldy in terms of UX. +const DEFAULT_PAGE_SIZE = 50; + +export const SortedDataTable = ({ + columns, + data, + pageSize = DEFAULT_PAGE_SIZE, + renderRows, + title, + pageKey = '', +}: Props) => { + const { pathname } = useLocation(); + const urlQuery = useURLQuery(); + + const pageQueryKey = `${pageKey}page`; + + const totalPages = Math.ceil(data.length / pageSize); + const { page } = useURLPagination({ totalPages, pageQueryKey }); + + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + + const startRow = startIndex + 1; + const lastRow = Math.min(data.length, startIndex + pageSize); + + const formatPageLink = (p: number) => ({ + pathname, + search: stringify({ ...urlQuery.query, [pageQueryKey]: p === 1 ? undefined : p }), + }); + + const [sortColumn, setSortColumn] = useState(null); + const [sortOrder, setSortOrder] = useState('asc'); + + const handleSort = useCallback((column: any) => { + if (sortColumn === column) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortColumn(column); + setSortOrder('asc'); + } + }, [sortColumn, sortOrder]); + + const sortedData = useMemo(() => { + if (!sortColumn) return data; + + const compare = (a: { [x: string]: any; }, b: { [x: string]: any; }) => { + const valueA = a[sortColumn]; + const valueB = b[sortColumn]; + + if (valueA < valueB) { + return sortOrder === 'asc' ? -1 : 1; + } else if (valueA > valueB) { + return sortOrder === 'asc' ? 1 : -1; + } else { + return 0; + } + }; + + return [...data].sort(compare); + }, [data, sortColumn, sortOrder]); + + const dataPage = sortedData.slice(startIndex, endIndex); + + + return ( +
+ + {title && } + + + {columns.map((col, cdx) => ( + + ))} + + + {renderRows(dataPage)} +
{title}
handleSort(col.accessor)}> +
+ {col.display} + {sortColumn === col.accessor && ( + {sortOrder === 'asc' ? '▲' : '▼'} + )} +
+
+ + + {!!data.length && ( +

+ Showing {startRow} {lastRow > startRow ? `- ${lastRow}` : null} of {data.length} +

+ )} +
+ ); +}; diff --git a/web/vtadmin/src/components/routes/Schemas.tsx b/web/vtadmin/src/components/routes/Schemas.tsx index fff3bb8b2db..d7ac9f4a708 100644 --- a/web/vtadmin/src/components/routes/Schemas.tsx +++ b/web/vtadmin/src/components/routes/Schemas.tsx @@ -25,7 +25,7 @@ import { formatBytes } from '../../util/formatBytes'; import { getTableDefinitions } from '../../util/tableDefinitions'; import { DataCell } from '../dataTable/DataCell'; import { DataFilter } from '../dataTable/DataFilter'; -import { DataTable } from '../dataTable/DataTable'; +import { ColumnProps, SortedDataTable } from '../dataTable/SortedDataTable'; import { ContentContainer } from '../layout/ContentContainer'; import { WorkspaceHeader } from '../layout/WorkspaceHeader'; import { WorkspaceTitle } from '../layout/WorkspaceTitle'; @@ -33,10 +33,10 @@ import { KeyspaceLink } from '../links/KeyspaceLink'; import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; import { HelpTooltip } from '../tooltip/HelpTooltip'; -const TABLE_COLUMNS = [ - 'Keyspace', - 'Table', -
+const TABLE_COLUMNS : Array = [ + {display: 'Keyspace', accessor : 'keyspace'}, + {display: 'Table' , accessor : 'table'}, + {display :
Approx. Size{' '} } /> -
, -
+
, accessor : '_tableSize'}, + {display:
Approx. Rows{' '} } /> -
, +
, accessor : '_tableRowCount'}, ]; export const Schemas = () => { @@ -74,6 +74,8 @@ export const Schemas = () => { clusterID: d.cluster?.id, keyspace: d.keyspace, table: d.tableDefinition?.name, + _tableSize: d.tableSize?.data_length || 0, + _tableRowCount: d.tableSize?.row_count || 0, _raw: d, })); @@ -120,7 +122,7 @@ export const Schemas = () => { placeholder="Filter schemas" value={filter || ''} /> - +