From 6deeea6cef9dbe1f05870e4c0d0ff5eb72467766 Mon Sep 17 00:00:00 2001 From: Alex Dametto <33689349+alexdametto@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:36:43 +0100 Subject: [PATCH] Prepare release 0.2.1 (#43) --- CHANGES.md | 30 ++- src/components/StatusBar/StatusBar.tsx | 4 +- src/components/StatusBar/index.ts | 3 + src/constants/defaults.ts | 2 + src/constants/routes.tsx | 8 +- src/routes/Tables/TableDetail.tsx | 139 ++++++++++ src/routes/Tables/TableList.tsx | 285 ++++++++++++++++++++ src/routes/Tables/Tables.tsx | 355 +++---------------------- 8 files changed, 493 insertions(+), 333 deletions(-) create mode 100644 src/components/StatusBar/index.ts create mode 100644 src/routes/Tables/TableDetail.tsx create mode 100644 src/routes/Tables/TableList.tsx diff --git a/CHANGES.md b/CHANGES.md index 8b7015e..c5430be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,16 +2,18 @@ ## Unreleased +- Added responsive styling to the tables page. + ## 2024-01-26 - 0.2.1 - Added type information to the result table. -- Add "Cancel" and "Save" buttons into ScheduledJobLogs component -- SQL Scheduler UI improvements +- Add "Cancel" and "Save" buttons into ScheduledJobLogs component. +- SQL Scheduler UI improvements. ## 2024-01-25 - 0.2.0 - Added a cluster overview page with overall cluster stats. -- Updated navigation to be fully responsive and branded +- Updated navigation to be fully responsive and branded. - Added charts to the cluster overview. - Improved formatting in the results table. - Added formatting of Arrays and Objects in the results table. @@ -19,18 +21,18 @@ ## 2024-01-22 - 0.1.2 -- Improved SQL Scheduler UI and add tests +- Improved SQL Scheduler UI and add tests. ## 2024-01-22 - 0.1.1 -- Initial release -- SQL Page -- Users Page -- Enterprise Wrapper for pages which require GC -- Tables Page -- Implemented navigation (Ctrl+Up/Ctrl+Down) in the SQL Editor -- Implemented SQL Scheduler UI +- Initial release. +- SQL Page. +- Users Page. +- Enterprise Wrapper for pages which require GC. +- Tables Page. +- Implemented navigation (Ctrl+Up/Ctrl+Down) in the SQL Editor. +- Implemented SQL Scheduler UI. - Added support for displaying certain types differently in the SQLResultsTable. -- Added a top bar with a logo -- Added builds for deploying as npm package -- Added a status bar and calculating cluster data status +- Added a top bar with a logo. +- Added builds for deploying as npm package. +- Added a status bar and calculating cluster data status. diff --git a/src/components/StatusBar/StatusBar.tsx b/src/components/StatusBar/StatusBar.tsx index 2f14d5e..bd696b0 100644 --- a/src/components/StatusBar/StatusBar.tsx +++ b/src/components/StatusBar/StatusBar.tsx @@ -116,7 +116,7 @@ function StatusBar() { return ( <>
setMobileVisible(true)} >
@@ -127,7 +127,7 @@ function StatusBar() {
-
+
Cluster
{spin(cluster?.name)}
diff --git a/src/components/StatusBar/index.ts b/src/components/StatusBar/index.ts new file mode 100644 index 0000000..07cdd6f --- /dev/null +++ b/src/components/StatusBar/index.ts @@ -0,0 +1,3 @@ +import StatusBar from './StatusBar'; + +export default StatusBar; diff --git a/src/constants/defaults.ts b/src/constants/defaults.ts index 5175168..554932b 100644 --- a/src/constants/defaults.ts +++ b/src/constants/defaults.ts @@ -3,3 +3,5 @@ // ------------------------------------- export const SHORT_MESSAGE_NOTIFICATION_DURATION = 7; export const LONG_MESSAGE_NOTIFICATION_DURATION = 15; + +export const TAILWIND_BREAKPOINT_MD = 768; diff --git a/src/constants/routes.tsx b/src/constants/routes.tsx index 4db494c..8392baf 100644 --- a/src/constants/routes.tsx +++ b/src/constants/routes.tsx @@ -1,7 +1,7 @@ import SQLConsole from '../routes/SQLConsole/SQLConsole'; import Users from '../routes/Users/Users'; import EnterpriseScreen from '../components/EnterpriseScreen/EnterpriseScreen'; -import Tables from '../routes/Tables/Tables'; +import Tables from '../routes/Tables'; import { Route } from '../types'; import ScheduledJobs from '../routes/JobScheduler'; import Overview from '../routes/Overview/Overview.tsx'; @@ -29,11 +29,7 @@ const routes: Route[] = [ }, { path: '/tables', - element: ( -
- -
- ), + element: , label: 'Tables', key: 'tables', }, diff --git a/src/routes/Tables/TableDetail.tsx b/src/routes/Tables/TableDetail.tsx new file mode 100644 index 0000000..615254e --- /dev/null +++ b/src/routes/Tables/TableDetail.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { LeftOutlined } from '@ant-design/icons'; +import { Button, Heading } from '@crate.io/crate-ui-components'; +import { Table, Tabs, Tag } from 'antd'; +import { useGCContext } from '../../contexts'; +import routes from '../../constants/routes'; +import { format as formatSQL } from 'sql-formatter'; +import { + getTableInformation, + showCreateTable, + TableInfo, + TableListEntry, +} from '../../utils/queries'; + +function TableDetail({ + activeTable, + setActiveTable, + systemSchemas, +}: { + activeTable: TableListEntry | undefined; + setActiveTable: (table: TableListEntry | undefined) => void; + systemSchemas: string[]; +}) { + const { sqlUrl } = useGCContext(); + const [activeTableInfo, setActiveTableInfo] = useState(); + const [createTableSQL, setCreateTableSQL] = useState(); + + useEffect(() => { + if (!activeTable) { + setActiveTableInfo(undefined); + return; + } + + getTableInformation( + sqlUrl, + activeTable.table_schema, + activeTable.table_name, + ).then(setActiveTableInfo); + + setCreateTableSQL(undefined); + if (!systemSchemas.includes(activeTable.table_schema)) { + showCreateTable(sqlUrl, activeTable.table_schema, activeTable.table_name).then( + res => setCreateTableSQL(res ? formatSQL(res) : undefined), + ); + } + }, [activeTable]); + + const columns = [ + { + title: '#', + key: 'ordinal_position', + dataIndex: 'ordinal_position', + width: '5%', + }, + { + title: 'Name', + key: 'column_name', + dataIndex: 'column_name', + width: '25%', + render: (_: string, record: TableInfo) => { + return ( + + {record.column_name}{' '} + {record.constraint_type ? {record.constraint_type} : null} + + ); + }, + }, + { + title: 'Type', + key: 'data_type', + dataIndex: 'data_type', + width: '70%', + render: (dataType: string) => { + return {dataType.toUpperCase()}; + }, + }, + ]; + + const tabs = [ + { + key: 'columns', + label: 'Columns', + children: ( + + ), + }, + ]; + + if (createTableSQL) { + tabs.push({ + key: 'SQL', + label: 'SQL', + children: ( +
+
{createTableSQL}
+
+ ), + }); + } + + return ( +
+ {activeTable && ( + <> +
setActiveTable(undefined)} + > + Back to list +
+ + {activeTable.table_schema}.{activeTable.table_name} + + +
+ r.key == 'sql')?.path, + search: `?q=SELECT * FROM "${activeTable.table_schema}"."${activeTable.table_name}" LIMIT 100;`, + }} + > + + +
+ + )} + {!activeTable &&
No table selected
} +
+ ); +} + +export default TableDetail; diff --git a/src/routes/Tables/TableList.tsx b/src/routes/Tables/TableList.tsx new file mode 100644 index 0000000..9cb1f5c --- /dev/null +++ b/src/routes/Tables/TableList.tsx @@ -0,0 +1,285 @@ +import { useEffect, useState } from 'react'; +import { StatusLight } from '@crate.io/crate-ui-components'; +import { Spin } from 'antd'; +import { + CaretDownOutlined, + CaretRightOutlined, + CloseOutlined, +} from '@ant-design/icons'; +import prettyBytes from 'pretty-bytes'; +import { useGCContext } from '../../contexts'; +import { TableListEntry } from '../../utils/queries'; +import { formatHumanReadable } from '../../utils/numbers.ts'; +import { + useGetShards, + useGetTables, + useGetAllocations, +} from '../../hooks/swrHooks.ts'; +import { + tablesWithMissingPrimaryReplicas, + tablesWithUnassignedShards, +} from '../../utils/statusChecks.ts'; + +function TableList({ + setActiveTable, + systemSchemas, +}: { + setActiveTable: (table: TableListEntry | undefined) => void; + systemSchemas: string[]; +}) { + const { sqlUrl } = useGCContext(); + const [expandedSchemas, setExpandedSchemas] = useState(); + const [schemas, setSchemas] = useState([]); + const [filter, setFilter] = useState(''); + const [filterFocused, setFilterFocused] = useState(false); + const { data: tables } = useGetTables(sqlUrl); + const { data: shards } = useGetShards(sqlUrl); + const { data: allocations } = useGetAllocations(sqlUrl); + const missingReplicasTables = tablesWithMissingPrimaryReplicas(allocations); + const unassignedShardTables = tablesWithUnassignedShards(allocations); + + // create a list of schema names + useEffect(() => { + if (!tables) { + return; + } + + const schemaNames: string[] = Array.from( + new Set(tables?.map(t => t.table_schema)), + ); + setSchemas(schemaNames); + if (expandedSchemas === undefined) { + setExpandedSchemas([schemaNames[0]]); + } + }, [tables]); + + const expandCollapseAll = () => { + if (expandedSchemas?.length === schemas.length) { + setExpandedSchemas([]); + } else { + setExpandedSchemas(schemas); + } + }; + + const expandCollapseSingle = (schema: string): void => { + if (expandedSchemas?.includes(schema)) { + setExpandedSchemas(expandedSchemas?.filter(s => s !== schema)); + } else { + setExpandedSchemas([...(expandedSchemas ?? []), schema]); + } + }; + + const getTableSize = (table: TableListEntry) => { + const tableShards = shards?.filter( + shard => + shard.schema_name == table.table_schema && + shard.table_name == table.table_name && + shard.primary, + ); + if (!tableShards || tableShards.length == 0) { + return null; + } + + return tableShards + .map(shard => { + return { + records: shard.total_docs, + bytes: shard.size_bytes, + }; + }) + .reduce((prev, current) => { + return { + records: prev.records + current.records, + bytes: prev.bytes + current.bytes, + }; + }); + }; + + const renderSchemaBadge = (schema: string) => { + if (systemSchemas.includes(schema)) { + return system; + } + + if (missingReplicasTables.filter(t => t.schema_name == schema).length > 0) { + return ( + + ); + } + + if (unassignedShardTables.filter(s => s.schema_name == schema).length > 0) { + return ( + + ); + } + + return ; + }; + + const renderSchemaList = () => { + const output = schemas + ?.map(schema => ({ + name: schema, + tables: tables + ?.filter( + table => + table.table_schema === schema && + table.table_name.toLowerCase().includes(filter.toLowerCase()), + ) + .map(table => table), + })) + .filter(schema => schema.tables && schema.tables.length > 0); + + if (output.length === 0) { + return ( +
+ No tables found +
+ ); + } + + return output.map(schema => ( +
+
expandCollapseSingle(schema.name)} + > +
+ {expandedSchemas?.includes(schema.name) ? ( + + ) : ( + + )} +
+ {schema.name} + + {schema.tables?.length} + +
+
+
+ {renderSchemaBadge(schema.name)} +
+
+ {expandedSchemas?.includes(schema.name) && ( +
+ {schema.tables?.map(table => renderTable(table))} +
+ )} +
+ )); + }; + + const renderTable = (table: TableListEntry) => { + const size = getTableSize(table); + + return ( +
setActiveTable(table)} + key={table.table_name} + > +
+ {table.table_name} + {renderTableBadge(table)} +
+ {size && ( + <> +
+ {formatHumanReadable(size.records)}{' '} + {size.records !== 1 ? 'Records' : 'Record'} ({prettyBytes(size.bytes)}) +
+
+ {table.number_of_shards} shards / {table.number_of_replicas} replicas +
+ + )} +
+ ); + }; + + const renderTableBadge = (table: TableListEntry) => { + if ( + missingReplicasTables.filter( + t => t.schema_name == table.table_schema && t.table_name == table.table_name, + ).length > 0 + ) { + return ( + + ); + } + + if ( + unassignedShardTables.filter( + s => s.schema_name == table.table_schema && s.table_name == table.table_name, + ).length > 0 + ) { + return ( + + ); + } + + return ; + }; + + if (schemas.length === 0) { + return ; + } + return ( +
+
+
+
+ setFilter(event.target.value)} + onFocus={() => setFilterFocused(true)} + onBlur={() => setFilterFocused(false)} + value={filter} + /> + +
+
+
expandCollapseAll()} + > + {(expandedSchemas?.length || 0) < schemas.length ? ( + + ) : ( + + )} +
+
+
+
{renderSchemaList()}
+
+
+ ); +} + +export default TableList; diff --git a/src/routes/Tables/Tables.tsx b/src/routes/Tables/Tables.tsx index e9fd9a3..458de79 100644 --- a/src/routes/Tables/Tables.tsx +++ b/src/routes/Tables/Tables.tsx @@ -1,332 +1,65 @@ import { useEffect, useState } from 'react'; -import { Button, Heading, StatusLight } from '@crate.io/crate-ui-components'; -import { Collapse, Input, List, Spin, Table, Tabs, Tag } from 'antd'; -import { useGCContext } from '../../contexts'; -import { format as formatSQL } from 'sql-formatter'; -import { - getTableInformation, - showCreateTable, - TableInfo, - TableListEntry, -} from '../../utils/queries'; -import { Link } from 'react-router-dom'; -import routes from '../../constants/routes'; -import prettyBytes from 'pretty-bytes'; -import { - useGetShards, - useGetTables, - useGetAllocations, -} from '../../hooks/swrHooks.ts'; -import { - tablesWithMissingPrimaryReplicas, - tablesWithUnassignedShards, -} from '../../utils/statusChecks.ts'; -import { formatHumanReadable } from '../../utils/numbers.ts'; +import { Heading } from '@crate.io/crate-ui-components'; +import TableDetail from './TableDetail'; +import TableList from './TableList'; +import { TAILWIND_BREAKPOINT_MD } from '../../constants/defaults'; +import { TableListEntry } from '../../utils/queries'; function Tables() { const systemSchemas = ['information_schema', 'sys', 'pg_catalog', 'gc']; - const { sqlUrl } = useGCContext(); - - const [schemas, setSchemas] = useState(); + const [isMobile, setIsMobile] = useState(false); const [activeTable, setActiveTable] = useState(); - const [activeTableInfo, setActiveTableInfo] = useState(); - const [createTableSQL, setCreateTableSQL] = useState(); - const [filter, setFilter] = useState(''); - - const { data: tables } = useGetTables(sqlUrl); - const { data: shards } = useGetShards(sqlUrl); - const { data: allocations } = useGetAllocations(sqlUrl); - const missingReplicasTables = tablesWithMissingPrimaryReplicas(allocations); - const unassignedShardTables = tablesWithUnassignedShards(allocations); + // get the screen width: we render the components in a different + // order depending on the screen width useEffect(() => { - if (!tables) { - return; - } - - setSchemas(Array.from(new Set(tables?.map(t => t.table_schema)))); - }, [tables]); - - useEffect(() => { - setCreateTableSQL(undefined); - if (!activeTable) { - return; - } - - getTableInformation( - sqlUrl, - activeTable.table_schema, - activeTable.table_name, - ).then(setActiveTableInfo); - - if (!systemSchemas.includes(activeTable.table_schema)) { - showCreateTable(sqlUrl, activeTable.table_schema, activeTable.table_name).then( - res => setCreateTableSQL(res ? formatSQL(res) : undefined), - ); - } - }, [activeTable]); + const handleResize = () => { + setIsMobile(window.innerWidth <= TAILWIND_BREAKPOINT_MD); + }; - const getTableSize = (schema: string, table: string) => { - const tableSizeInfo = shards?.filter( - s => s.schema_name == schema && s.table_name == table && s.primary, - ); - if (!tableSizeInfo || tableSizeInfo.length == 0) { - return null; - } - - return tableSizeInfo - .map(rec => { - return { - records: rec.total_docs, - bytes: rec.size_bytes, - }; - }) - .reduce((prev, current) => { - return { - records: prev.records + current.records, - bytes: prev.bytes + current.bytes, - }; - }); - }; - - const tableBadge = (table: TableListEntry) => { - if ( - missingReplicasTables.filter( - t => t.schema_name == table.table_schema && t.table_name == table.table_name, - ).length > 0 - ) { - return ( - - ); - } - if ( - unassignedShardTables.filter( - s => s.schema_name == table.table_schema && s.table_name == table.table_name, - ).length > 0 - ) { - return ( - - ); - } - return ; - }; - - const getTableList = (schema: string) => { - const schemaTables = tables - ?.filter(t => t.table_schema == schema) - .filter(t => t.table_name.match(filter)); - - if (!schemaTables || schemaTables.length == 0) { - return null; - } + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + // render for mobile + if (isMobile) { return ( - { - const size = getTableSize(item.table_schema, item.table_name); - return ( - { - setActiveTableInfo([]); - setActiveTable(item); - }} - > -
-
-
{item.table_name}
- {size && ( -
- {formatHumanReadable(size.records)} Records ( - {prettyBytes(size.bytes)}) -
- )} - {item.number_of_replicas && ( -
- {item.number_of_shards} Shards / {item.number_of_replicas}{' '} - Replicas -
- )} -
-
{tableBadge(item)}
-
-
- ); - }} - /> - ); - }; - - const schemaBadge = (schema: string) => { - if (systemSchemas.includes(schema)) { - return system; - } - if (missingReplicasTables.filter(t => t.schema_name == schema).length > 0) { - return ( - - ); - } - if (unassignedShardTables.filter(s => s.schema_name == schema).length > 0) { - return ( - - ); - } - return ; - }; - - const constraintBadge = (constraint: string | null) => { - if (constraint) { - return {constraint}; - } - return null; - }; - - const getSchemasSection = () => { - const items = schemas - ?.map(s => { - return { - key: s, - label: ( -
-
{s}
-
{schemaBadge(s)}
-
- ), - children: getTableList(s), - }; - }) - .filter(i => i); - - return tables ? ( - - {items - ?.filter(i => i.children) - .map(item => { - return ( - - {item.children} - - ); - })} - - ) : ( - - ); - }; - - const getActiveTableSection = () => { - if (!activeTable) { - return null; - } - const columns = [ - { - title: '#', - key: 'ordinal_position', - dataIndex: 'ordinal_position', - width: '5%', - }, - { - title: 'Name', - key: 'column_name', - dataIndex: 'column_name', - width: '25%', - render: (_: string, record: TableInfo) => { - return ( - - {record.column_name} {constraintBadge(record.constraint_type)} - - ); - }, - }, - { - title: 'Type', - key: 'data_type', - dataIndex: 'data_type', - width: '70%', - render: (_: string, record: TableInfo) => { - return {record.data_type.toUpperCase()}; - }, - }, - ]; - - const tabs = [ - { - key: `Columns`, - label: `Columns`, - children: ( -
- ), - }, - ]; - if (createTableSQL) { - tabs.push({ - key: 'SQL', - label: 'SQL', - children: ( -
-
{createTableSQL}
-
- ), - }); - } - - return ( -
- - Table: {activeTable?.table_schema}.{activeTable?.table_name} +
+ + Tables - -
- r.key == 'sql')?.path, - search: `?q=SELECT * FROM "${activeTable.table_schema}"."${activeTable.table_name}" LIMIT 100;`, - }} - > - - +
+ +
+
+
); - }; + } + // render for desktop return ( - <> - Tables -
-
-
- setFilter(v.target.value)} /> -
- {getSchemasSection()} +
+
+ +
+
+ Tables +
+
-
{getActiveTableSection()}
- +
); }