mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-24 15:20:03 +08:00
25
frontend/src/components/SidebarDatabaseMenu.order.test.ts
Normal file
25
frontend/src/components/SidebarDatabaseMenu.order.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user