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: (
+
+ ),
+ });
+ }
+
+ 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 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
+
+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: (
-
- ),
- });
- }
-
- 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()}
- >
+
);
}