diff --git a/src/app/storybook/listview/page.tsx b/src/app/storybook/listview/page.tsx index 155e921c..cf76f93c 100644 --- a/src/app/storybook/listview/page.tsx +++ b/src/app/storybook/listview/page.tsx @@ -29,12 +29,18 @@ function Demo1() { name: "movies", key: "movies.movies", icon: LucideTable, + progressBarLabel: "1.3GB", + progressBarValue: 9050, + progressBarMax: 10000, }, { name: "users", badgeContent: "fts5", badgeClassName: "bg-red-500 text-white", key: "movies.users", + progressBarLabel: "1.5KB", + progressBarValue: 1500, + progressBarMax: 10000, icon: LucideTable, children: [ { @@ -54,7 +60,14 @@ function Demo1() { }, ], }, - { name: "reviews", key: "movies.reviews", icon: LucideTable }, + { + name: "reviews", + key: "movies.reviews", + icon: LucideTable, + progressBarLabel: "256KB", + progressBarValue: 5550, + progressBarMax: 10000, + }, ], }, { diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index 01d74bb1..d6dc0d01 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -13,8 +13,25 @@ interface SchemaListProps { search: string; } +function formatTableSize(byteCount?: number) { + const byteInKb = 1024; + const byteInMb = byteInKb * 1024; + const byteInGb = byteInMb * 1024; + + if (!byteCount) return undefined; + if (byteInMb * 999 < byteCount) + return (byteCount / byteInGb).toFixed(1) + " GB"; + if (byteInMb * 100 < byteCount) + return (byteCount / byteInMb).toFixed(0) + " MB"; + if (byteInKb * 100 < byteCount) + return (byteCount / byteInMb).toFixed(1) + " MB"; + if (byteInKb < byteCount) return Math.floor(byteCount / byteInKb) + " KB"; + return "1 KB"; +} + function prepareListViewItem( - schema: DatabaseSchemaItem[] + schema: DatabaseSchemaItem[], + maxTableSize: number ): ListViewItem[] { return schema.map((s) => { let icon = Table; @@ -34,6 +51,9 @@ function prepareListViewItem( iconColor: iconClassName, key: s.schemaName + "." + s.name, name: s.name, + progressBarMax: maxTableSize, + progressBarValue: s.tableSchema?.stats?.sizeInByte, + progressBarLabel: formatTableSize(s.tableSchema?.stats?.sizeInByte), }; }); } @@ -121,11 +141,11 @@ export default function SchemaList({ search }: Readonly) { const isTable = item?.type === "table"; return [ - item?.type === 'schema' && { - title: 'Edit', + item?.type === "schema" && { + title: "Edit", onClick: () => { setEditSchema(item.schemaName); - } + }, }, { title: "Copy Name", @@ -146,15 +166,15 @@ export default function SchemaList({ search }: Readonly) { }, isTable && databaseDriver.getFlags().supportCreateUpdateTable ? { - title: "Edit Table", - onClick: () => { - openTab({ - tableName: item?.name, - type: "schema", - schemaName: item?.schemaName ?? "", - }); - }, - } + title: "Edit Table", + onClick: () => { + openTab({ + tableName: item?.name, + type: "schema", + schemaName: item?.schemaName ?? "", + }); + }, + } : undefined, databaseDriver.getFlags().supportCreateUpdateTable ? { separator: true } @@ -168,6 +188,10 @@ export default function SchemaList({ search }: Readonly) { const listViewItems = useMemo(() => { const r = sortTable( Object.entries(schema).map(([s, tables]) => { + const maxTableSize = Math.max( + ...tables.map((t) => t.tableSchema?.stats?.sizeInByte ?? 0) + ); + return { data: { type: "schema", schemaName: s }, icon: LucideDatabase, @@ -175,7 +199,9 @@ export default function SchemaList({ search }: Readonly) { iconBadgeColor: s === currentSchemaName ? "bg-green-600" : undefined, key: s.toString(), children: sortTable( - groupByFtsTable(groupTriggerByTable(prepareListViewItem(tables))) + groupByFtsTable( + groupTriggerByTable(prepareListViewItem(tables, maxTableSize)) + ) ), } as ListViewItem; }) @@ -199,7 +225,12 @@ export default function SchemaList({ search }: Readonly) { return ( <> - {editSchema && setEditSchema(null)} />} + {editSchema && ( + setEditSchema(null)} + /> + )} { badgeContent?: string; badgeClassName?: string; children?: ListViewItem[]; + progressBarValue?: number; + progressBarMax?: number; + progressBarLabel?: string; } interface ListViewProps { @@ -224,7 +227,7 @@ function renderList(props: ListViewRendererProps): React.ReactElement { )} -
+
{item.badgeContent && ( (props: ListViewRendererProps): React.ReactElement { )}
+ + {item.progressBarValue && item.progressBarMax && ( +
+
+ + {item.progressBarLabel} + +
+ )}
{isCollapsed && diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index b7cdfa85..1104aa6e 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -144,6 +144,10 @@ export interface DatabaseTableFts5 { contentRowId?: string; } +export interface DatabaseTableSchemaStats { + sizeInByte?: number; + estimateRowCount?: number; +} export interface DatabaseTableSchema { columns: DatabaseTableColumn[]; pk: string[]; @@ -156,6 +160,7 @@ export interface DatabaseTableSchema { type?: "table" | "view"; withoutRowId?: boolean; strict?: boolean; + stats?: DatabaseTableSchemaStats } export type TriggerWhen = "BEFORE" | "AFTER" | "INSTEAD_OF"; diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index 6908403f..c2bdd4c5 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -47,6 +47,8 @@ interface MySqlTable { TABLE_SCHEMA: string; TABLE_NAME: string; TABLE_TYPE: string; + INDEX_LENGTH: number; + DATA_LENGTH: number; } interface MySQLConstraintResult { @@ -157,7 +159,7 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { .rows as unknown as MySqlDatabase[]; const tableSql = - "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')"; + "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, DATA_LENGTH, INDEX_LENGTH FROM information_schema.tables WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')"; const tableResult = (await this.query(tableSql)) .rows as unknown as MySqlTable[]; @@ -191,6 +193,9 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { tableName: t.TABLE_NAME, schemaName: t.TABLE_SCHEMA, tableSchema: { + stats: { + sizeInByte: t.DATA_LENGTH + t.INDEX_LENGTH + }, autoIncrement: false, pk: [], columns: [], @@ -345,13 +350,13 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { foreignKey: constraint.CONSTRAINT_TYPE === "FOREIGN KEY" ? { - columns: columnList.map((c) => c.COLUMN_NAME), - foreignColumns: columnList.map( - (c) => c.REFERENCED_COLUMN_NAME - ), - foreignSchemaName: columnList[0].REFERENCED_TABLE_SCHEMA, - foreignTableName: columnList[0].REFERENCED_TABLE_NAME, - } + columns: columnList.map((c) => c.COLUMN_NAME), + foreignColumns: columnList.map( + (c) => c.REFERENCED_COLUMN_NAME + ), + foreignSchemaName: columnList[0].REFERENCED_TABLE_SCHEMA, + foreignTableName: columnList[0].REFERENCED_TABLE_NAME, + } : undefined, }; } diff --git a/src/drivers/postgres/postgres-driver.ts b/src/drivers/postgres/postgres-driver.ts index 417c73d0..14887f29 100644 --- a/src/drivers/postgres/postgres-driver.ts +++ b/src/drivers/postgres/postgres-driver.ts @@ -22,6 +22,7 @@ interface PostgresTableRow { table_schema: string; table_name: string; table_type: string; + table_size: number; } interface PostgresColumnRow { @@ -92,7 +93,7 @@ export default abstract class PostgresLikeDriver extends CommonSQLImplement { const tableResult = ( await this.query( - "SELECT * FROM information_schema.tables WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND table_type = 'BASE TABLE';" + "SELECT *, pg_total_relation_size(quote_ident(table_schema) || '.' || quote_ident(table_name)) AS table_size FROM information_schema.tables WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND table_type = 'BASE TABLE';" ) ).rows as unknown as PostgresTableRow[]; @@ -145,6 +146,9 @@ WHERE type: table.table_type === "BASE TABLE" ? "table" : "view", tableName: table.table_name, tableSchema: { + stats: { + sizeInByte: table.table_size, + }, columns: [], constraints: [], pk: [],