feat(database-ui): 补充表概览元数据并调整数据库菜单

Fixes #395
This commit is contained in:
Syngnat
2026-06-29 20:35:59 +08:00
parent d2a535e6ff
commit 99942e37d6
11 changed files with 186 additions and 14 deletions

View File

@@ -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);
});
});

View File

@@ -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("<div style={{ color: textMuted }}>行数</div>");
expect(listSource).not.toContain("<div style={{ color: textMuted }}>数据大小</div>");
expect(listSource).not.toContain("<div style={{ color: textMuted }}>索引大小</div>");
expect(listSource).not.toContain("<div style={{ color: textMuted }}>相对大小</div>");
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);
});

View File

@@ -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(<TableOverview tab={{
id: 'tab-1',
title: '表概览 - app_db',
type: 'table-overview',
connectionId: 'conn-1',
dbName: 'app_db',
} as any} />);
});
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');
});
});

View File

@@ -1012,6 +1012,58 @@ const TableOverview: React.FC<TableOverviewProps> = ({ 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 (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 8,
marginTop: compact ? 8 : 0,
marginBottom: compact ? 0 : 10,
}}
>
{items.map((item) => (
<Tooltip key={item.key} title={`${item.label}: ${item.value}`} mouseEnterDelay={0.4}>
<span
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
maxWidth: '100%',
padding: compact ? '1px 7px' : '2px 8px',
borderRadius: 999,
background: darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
color: textSecondary,
fontSize: compact ? 10 : 11,
lineHeight: compact ? '16px' : '18px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
<span style={{ opacity: 0.72 }}>{item.label}</span>
<span style={{ color: textPrimary, fontVariantNumeric: 'tabular-nums' }}>{item.value}</span>
</span>
</Tooltip>
))}
</div>
);
}, [darkMode, t, textPrimary, textSecondary]);
const renderCardTableContent = (table: TableStatRow) => (
<div
className={isV2Ui ? 'gn-v2-table-card' : undefined}
@@ -1044,6 +1096,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
</div>
</Tooltip>
)}
{renderTableOverviewMetaBadges(table)}
<div className={isV2Ui ? 'gn-v2-table-card-meta' : undefined} style={{ display: 'flex', gap: 16, fontSize: 12, color: textMuted }}>
<span title={t('table_overview.sort.rows')} style={{ minWidth: 52 }}>📊 {formatRows(table.rows)}</span>
<span title={t('table_overview.metric.data_size')} style={{ minWidth: 72 }}>💾 {formatSize(table.dataSize)}</span>
@@ -1150,6 +1203,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
{rowSecondary}
</div>
</Tooltip>
{renderTableOverviewMetaBadges(table, true)}
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 12, flexWrap: 'wrap', fontSize: 12 }}>
<div style={{ minWidth: 96, textAlign: 'right' }}>

View File

@@ -649,6 +649,19 @@ export const buildSidebarLegacyNodeMenuItems = (
onClick: () => openCreateStarRocksExternalCatalog(node)
},
] : []),
{
key: 'new-query',
label: t('sidebar.menu.new_query'),
icon: <ConsoleSqlOutlined />,
onClick: () => handleV2DatabaseContextMenuAction(node, 'new-query')
},
{
key: 'run-sql',
label: t('sidebar.sql_file_exec.title'),
icon: <FileAddOutlined />,
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: <DisconnectOutlined />,
onClick: () => handleV2DatabaseContextMenuAction(node, 'disconnect-db')
},
{
key: 'new-query',
label: t('sidebar.menu.new_query'),
icon: <ConsoleSqlOutlined />,
onClick: () => handleV2DatabaseContextMenuAction(node, 'new-query')
},
{
key: 'run-sql',
label: t('sidebar.sql_file_exec.title'),
icon: <FileAddOutlined />,
onClick: () => handleV2DatabaseContextMenuAction(node, 'run-sql')
}
}
];
} else if (node.type === 'view') {
return [

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "新しいテーブル名を入力",

View File

@@ -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": "Введите новое имя таблицы",

View File

@@ -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": "输入新表名",

View File

@@ -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": "輸入新的資料表名稱",