From 99942e37d6063feb7200a4298beabfd9ba277dd6 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 29 Jun 2026 20:35:59 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(database-ui):=20=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E8=A1=A8=E6=A6=82=E8=A7=88=E5=85=83=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=B9=B6=E8=B0=83=E6=95=B4=E6=95=B0=E6=8D=AE=E5=BA=93=E8=8F=9C?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #395 --- .../SidebarDatabaseMenu.order.test.ts | 25 +++++++ .../src/components/TableOverview.i18n.test.ts | 15 ++++- .../TableOverview.tdengine.test.tsx | 67 +++++++++++++++++++ frontend/src/components/TableOverview.tsx | 54 +++++++++++++++ .../sidebar/sidebarLegacyNodeMenu.tsx | 27 ++++---- shared/i18n/de-DE.json | 2 + shared/i18n/en-US.json | 2 + shared/i18n/ja-JP.json | 2 + shared/i18n/ru-RU.json | 2 + shared/i18n/zh-CN.json | 2 + shared/i18n/zh-TW.json | 2 + 11 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/SidebarDatabaseMenu.order.test.ts diff --git a/frontend/src/components/SidebarDatabaseMenu.order.test.ts b/frontend/src/components/SidebarDatabaseMenu.order.test.ts new file mode 100644 index 00000000..4bcf6419 --- /dev/null +++ b/frontend/src/components/SidebarDatabaseMenu.order.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8'); + +describe('Sidebar legacy database menu order', () => { + it('keeps new query and run-sql ahead of close database', () => { + const databaseMenuStart = source.indexOf("} else if (node.type === 'database') {"); + const databaseMenuEnd = source.indexOf("} else if (node.type === 'view') {", databaseMenuStart); + + expect(databaseMenuStart).toBeGreaterThanOrEqual(0); + expect(databaseMenuEnd).toBeGreaterThan(databaseMenuStart); + + const databaseMenuSource = source.slice(databaseMenuStart, databaseMenuEnd); + const newQueryIndex = databaseMenuSource.indexOf("key: 'new-query'"); + const runSqlIndex = databaseMenuSource.indexOf("key: 'run-sql'"); + const disconnectIndex = databaseMenuSource.indexOf("key: 'disconnect-db'"); + + expect(newQueryIndex).toBeGreaterThanOrEqual(0); + expect(runSqlIndex).toBeGreaterThanOrEqual(0); + expect(disconnectIndex).toBeGreaterThanOrEqual(0); + expect(newQueryIndex).toBeLessThan(disconnectIndex); + expect(runSqlIndex).toBeLessThan(disconnectIndex); + }); +}); diff --git a/frontend/src/components/TableOverview.i18n.test.ts b/frontend/src/components/TableOverview.i18n.test.ts index 9d1f3a7d..7e0421ce 100644 --- a/frontend/src/components/TableOverview.i18n.test.ts +++ b/frontend/src/components/TableOverview.i18n.test.ts @@ -37,6 +37,10 @@ const normalizedVisibleTableSectionsSource = visibleTableSectionsSource.replace( const renderOverviewSectionTitleSource = source.slice( source.indexOf('const renderOverviewSectionTitle = (section: OverviewTableSection) => {'), + source.indexOf('const renderTableOverviewMetaBadges = useCallback((table: TableStatRow, compact = false) => {'), +); +const metaBadgesSource = source.slice( + source.indexOf('const renderTableOverviewMetaBadges = useCallback((table: TableStatRow, compact = false) => {'), source.indexOf('const renderCardTableContent = (table: TableStatRow) => ('), ); const normalizedRenderOverviewSectionTitleSource = renderOverviewSectionTitleSource.replace(/\s+/g, ' ').trim(); @@ -59,6 +63,8 @@ const aiPromptSource = source.slice( ); const requiredTableOperationKeys = [ + 'table_overview.metric.created_at', + 'table_overview.metric.updated_at', 'table_overview.tab.design_table_title', 'table_overview.tab.table_structure_title', 'table_overview.message.load_tables_failed', @@ -121,18 +127,26 @@ describe('TableOverview i18n', () => { expect(cardSource).not.toContain('title="行数"'); expect(cardSource).not.toContain('title="数据大小"'); expect(cardSource).not.toContain('title="引擎"'); + expect(cardSource).not.toContain('最近修改'); + expect(cardSource).not.toContain('创建时间'); expect(cardSource).toContain("title={t('table_overview.sort.rows')}"); expect(cardSource).toContain("title={t('table_overview.metric.data_size')}"); expect(cardSource).toContain("title={t('table_overview.metric.engine')}"); + expect(cardSource).toContain('{renderTableOverviewMetaBadges(table)}'); + expect(metaBadgesSource).toContain("t('table_overview.metric.updated_at')"); + expect(metaBadgesSource).toContain("t('table_overview.metric.created_at')"); expect(listSource).not.toContain('`${table.engine} 表`'); expect(listSource).not.toContain("'双击打开数据,右键查看更多操作'"); + expect(listSource).not.toContain('最近修改'); + expect(listSource).not.toContain('创建时间'); expect(listSource).not.toContain("
行数
"); expect(listSource).not.toContain("
数据大小
"); expect(listSource).not.toContain("
索引大小
"); expect(listSource).not.toContain("
相对大小
"); expect(listSource).toContain("t('table_overview.row.engine_table', { engine: table.engine })"); expect(listSource).toContain("t('table_overview.row.open_hint')"); + expect(listSource).toContain('{renderTableOverviewMetaBadges(table, true)}'); expect(listSource).toContain("t('table_overview.sort.rows')"); expect(listSource).toContain("t('table_overview.metric.data_size')"); expect(listSource).toContain("t('table_overview.metric.index_size')"); @@ -239,7 +253,6 @@ describe('TableOverview i18n', () => { "t('table_overview.message.rename_table_failed'", "t('table_overview.menu.copy_table_name')", "t('table_overview.menu.table_structure')", - "t('table_overview.menu.export_xlsx')", ].forEach(text => { expect(tableOperationSource).toContain(text); }); diff --git a/frontend/src/components/TableOverview.tdengine.test.tsx b/frontend/src/components/TableOverview.tdengine.test.tsx index a3bfd2aa..3a8da13e 100644 --- a/frontend/src/components/TableOverview.tdengine.test.tsx +++ b/frontend/src/components/TableOverview.tdengine.test.tsx @@ -142,6 +142,21 @@ describe('TableOverview tdengine compatibility', () => { beforeEach(() => { vi.clearAllMocks(); storeState.appearance = { uiVersion: 'legacy', tableDoubleClickAction: 'open-data' }; + storeState.connections = [ + { + id: 'conn-1', + config: { + type: 'tdengine', + host: '127.0.0.1', + port: 6041, + user: 'root', + password: 'taosdata', + database: 'metrics', + useSSH: false, + ssh: { host: '', port: 22, user: '', password: '', keyPath: '' }, + }, + }, + ]; backendApp.DBGetTables.mockResolvedValue({ success: true, data: [ @@ -249,4 +264,56 @@ describe('TableOverview tdengine compatibility', () => { tableName: 'd001', })); }); + + it('renders comment and temporal metadata in the legacy table list when available', async () => { + storeState.connections = [ + { + id: 'conn-1', + config: { + type: 'mysql', + host: '127.0.0.1', + port: 3306, + user: 'root', + password: 'secret', + database: 'app_db', + useSSH: false, + ssh: { host: '', port: 22, user: '', password: '', keyPath: '' }, + }, + }, + ]; + backendApp.DBQuery.mockResolvedValue({ + success: true, + data: [ + { + TABLE_NAME: 'orders', + TABLE_COMMENT: '订单表', + TABLE_ROWS: 128, + DATA_LENGTH: 2048, + INDEX_LENGTH: 1024, + ENGINE: 'InnoDB', + CREATE_TIME: '2026-05-01 09:00:00', + UPDATE_TIME: '2026-06-02 10:30:00', + }, + ], + }); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + await flushPromises(); + + expect(backendApp.DBQuery).toHaveBeenCalled(); + const renderedText = collectText(renderer!.toJSON()); + expect(renderedText).toContain('orders'); + expect(renderedText).toContain('订单表'); + expect(renderedText).toContain('2026-06-02 10:30:00'); + expect(renderedText).toContain('2026-05-01 09:00:00'); + }); }); diff --git a/frontend/src/components/TableOverview.tsx b/frontend/src/components/TableOverview.tsx index f5928be3..13dc3479 100644 --- a/frontend/src/components/TableOverview.tsx +++ b/frontend/src/components/TableOverview.tsx @@ -1012,6 +1012,58 @@ const TableOverview: React.FC = ({ tab }) => { ); }; + const renderTableOverviewMetaBadges = useCallback((table: TableStatRow, compact = false) => { + const items = [ + ...(table.updateTime ? [{ + key: 'updated', + label: t('table_overview.metric.updated_at'), + value: table.updateTime, + }] : []), + ...(table.createTime ? [{ + key: 'created', + label: t('table_overview.metric.created_at'), + value: table.createTime, + }] : []), + ]; + if (items.length === 0) return null; + return ( +
+ {items.map((item) => ( + + + {item.label} + {item.value} + + + ))} +
+ ); + }, [darkMode, t, textPrimary, textSecondary]); + const renderCardTableContent = (table: TableStatRow) => (
= ({ tab }) => {
)} + {renderTableOverviewMetaBadges(table)}
📊 {formatRows(table.rows)} 💾 {formatSize(table.dataSize)} @@ -1150,6 +1203,7 @@ const TableOverview: React.FC = ({ tab }) => { {rowSecondary}
+ {renderTableOverviewMetaBadges(table, true)}
diff --git a/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx index 8b03e1b5..fa99d3a1 100644 --- a/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx +++ b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx @@ -649,6 +649,19 @@ export const buildSidebarLegacyNodeMenuItems = ( onClick: () => openCreateStarRocksExternalCatalog(node) }, ] : []), + { + key: 'new-query', + label: t('sidebar.menu.new_query'), + icon: , + onClick: () => handleV2DatabaseContextMenuAction(node, 'new-query') + }, + { + key: 'run-sql', + label: t('sidebar.sql_file_exec.title'), + icon: , + onClick: () => handleV2DatabaseContextMenuAction(node, 'run-sql') + }, + { type: 'divider' }, ...(capabilities.supportsRenameDatabase ? [{ key: 'rename-db', label: t('sidebar.menu.rename_database'), @@ -693,19 +706,7 @@ export const buildSidebarLegacyNodeMenuItems = ( label: t('sidebar.menu.close_database'), icon: , onClick: () => handleV2DatabaseContextMenuAction(node, 'disconnect-db') - }, - { - key: 'new-query', - label: t('sidebar.menu.new_query'), - icon: , - onClick: () => handleV2DatabaseContextMenuAction(node, 'new-query') - }, - { - key: 'run-sql', - label: t('sidebar.sql_file_exec.title'), - icon: , - onClick: () => handleV2DatabaseContextMenuAction(node, 'run-sql') - } + } ]; } else if (node.type === 'view') { return [ diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index d5ce38ee..eca35794 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -7629,10 +7629,12 @@ "table_overview.message.table_data_action_success": "{{action}} erfolgreich", "table_overview.message.unknown_error": "Unbekannter Fehler", "table_overview.message.unpinned": "Anheften der Tabelle aufgehoben", + "table_overview.metric.created_at": "Erstellt", "table_overview.metric.data_size": "Datengröße", "table_overview.metric.engine": "Engine", "table_overview.metric.index_size": "Indexgröße", "table_overview.metric.relative_size": "Relative Größe", + "table_overview.metric.updated_at": "Zuletzt geändert", "table_overview.modal.delete_table.content": "Tabelle \"{{table}}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden.", "table_overview.modal.delete_table.title": "Tabelle löschen", "table_overview.modal.rename_table.placeholder": "Neuen Tabellennamen eingeben", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index ad0eb5a7..78559f9f 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -7629,10 +7629,12 @@ "table_overview.message.table_data_action_success": "{{action}} succeeded", "table_overview.message.unknown_error": "Unknown error", "table_overview.message.unpinned": "Table unpinned", + "table_overview.metric.created_at": "Created", "table_overview.metric.data_size": "Data size", "table_overview.metric.engine": "Engine", "table_overview.metric.index_size": "Index size", "table_overview.metric.relative_size": "Relative size", + "table_overview.metric.updated_at": "Last updated", "table_overview.modal.delete_table.content": "Delete table \"{{table}}\"? This action cannot be undone.", "table_overview.modal.delete_table.title": "Delete Table", "table_overview.modal.rename_table.placeholder": "Enter a new table name", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index 17305779..4defc880 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -7629,10 +7629,12 @@ "table_overview.message.table_data_action_success": "{{action}} が成功しました", "table_overview.message.unknown_error": "不明なエラー", "table_overview.message.unpinned": "テーブルのピン留めを解除しました", + "table_overview.metric.created_at": "作成日時", "table_overview.metric.data_size": "データサイズ", "table_overview.metric.engine": "エンジン", "table_overview.metric.index_size": "インデックスサイズ", "table_overview.metric.relative_size": "相対サイズ", + "table_overview.metric.updated_at": "最終更新", "table_overview.modal.delete_table.content": "テーブル \"{{table}}\" を削除しますか?この操作は元に戻せません。", "table_overview.modal.delete_table.title": "テーブルを削除", "table_overview.modal.rename_table.placeholder": "新しいテーブル名を入力", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index b36b57c8..366713e0 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -7629,10 +7629,12 @@ "table_overview.message.table_data_action_success": "{{action}} выполнено", "table_overview.message.unknown_error": "Неизвестная ошибка", "table_overview.message.unpinned": "Закрепление таблицы снято", + "table_overview.metric.created_at": "Создано", "table_overview.metric.data_size": "Размер данных", "table_overview.metric.engine": "Движок", "table_overview.metric.index_size": "Размер индекса", "table_overview.metric.relative_size": "Относительный размер", + "table_overview.metric.updated_at": "Последнее изменение", "table_overview.modal.delete_table.content": "Удалить таблицу \"{{table}}\"? Это действие нельзя отменить.", "table_overview.modal.delete_table.title": "Удалить таблицу", "table_overview.modal.rename_table.placeholder": "Введите новое имя таблицы", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 159bb8ee..12bfed3c 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -7629,10 +7629,12 @@ "table_overview.message.table_data_action_success": "{{action}}成功", "table_overview.message.unknown_error": "未知错误", "table_overview.message.unpinned": "已取消置顶", + "table_overview.metric.created_at": "创建时间", "table_overview.metric.data_size": "数据大小", "table_overview.metric.engine": "引擎", "table_overview.metric.index_size": "索引大小", "table_overview.metric.relative_size": "相对大小", + "table_overview.metric.updated_at": "最近修改", "table_overview.modal.delete_table.content": "删除表 \"{{table}}\"?此操作无法撤销。", "table_overview.modal.delete_table.title": "删除表", "table_overview.modal.rename_table.placeholder": "输入新表名", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index 23a194e4..95691d19 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -7629,10 +7629,12 @@ "table_overview.message.table_data_action_success": "{{action}}成功", "table_overview.message.unknown_error": "未知錯誤", "table_overview.message.unpinned": "已取消置頂", + "table_overview.metric.created_at": "建立時間", "table_overview.metric.data_size": "資料大小", "table_overview.metric.engine": "引擎", "table_overview.metric.index_size": "索引大小", "table_overview.metric.relative_size": "相對大小", + "table_overview.metric.updated_at": "最近修改", "table_overview.modal.delete_table.content": "刪除資料表 \"{{table}}\"?此操作無法復原。", "table_overview.modal.delete_table.title": "刪除資料表", "table_overview.modal.rename_table.placeholder": "輸入新的資料表名稱",