feat(ui): 优化 Schema 标签与表概览显示

This commit is contained in:
kunghim
2026-08-10 17:37:44 +08:00
parent 3243082c8e
commit a84a44d2ac
4 changed files with 107 additions and 12 deletions

View File

@@ -435,6 +435,79 @@ describe('TableOverview metadata compatibility', () => {
expect(renderedText).toContain('embeddings');
});
it.each(['postgres', 'kingbase'])('shows bare table names for %s while preserving qualified operation targets', async (type) => {
storeState.appearance = { uiVersion: 'v2', tableDoubleClickAction: 'open-data' };
storeState.connections = [
{
id: 'conn-1',
config: {
type,
host: '127.0.0.1',
port: 20035,
user: 'postgres',
password: 'secret',
database: 'dbx_test',
useSSH: false,
ssh: { host: '', port: 22, user: '', password: '', keyPath: '' },
},
},
];
backendApp.DBQuery.mockResolvedValue({
success: true,
data: [
{
table_name: 'reporting.orders',
table_comment: 'Orders',
table_rows: 12,
data_length: 4096,
index_length: 1024,
},
],
});
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<TableOverview tab={{
id: 'tab-1',
title: '表概览 - dbx_test',
type: 'table-overview',
connectionId: 'conn-1',
dbName: 'dbx_test',
schemaName: 'reporting',
} as any} />);
});
await flushPromises();
expect(backendApp.DBQuery).toHaveBeenCalledOnce();
expect(String(backendApp.DBQuery.mock.calls[0]?.[2] || '')).toContain("n.nspname = 'reporting'");
const assertBareTableName = () => {
const renderedText = collectText(renderer!.toJSON());
expect(renderedText).toContain('dbx_test · reporting');
expect(renderedText).toContain('orders');
expect(renderedText).not.toContain('reporting.orders');
};
assertBareTableName();
await act(async () => {
renderer!.root.findByProps({ 'data-table-overview-view-mode': 'list' }).props.onClick();
});
assertBareTableName();
await act(async () => {
renderer!.root.findByProps({ 'data-table-overview-view-mode': 'table' }).props.onClick();
});
assertBareTableName();
storeState.addTab.mockClear();
await act(async () => {
renderer!.root.findByProps({ 'data-table-overview-row': 'reporting.orders' }).props.onDoubleClick();
});
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
type: 'table',
tableName: 'reporting.orders',
}));
});
it.each([
{ type: 'oracle', dbName: 'APP' },
{ type: 'trino', dbName: 'catalog' },

View File

@@ -34,6 +34,7 @@ import { confirmCopyTable } from './tableCopyAction';
import { APP_POPUP_Z_INDEX } from '../utils/overlayZIndex';
import { formatSidebarTableTimestamp } from './sidebar/sidebarHelpers';
import { confirmProductionMutation } from '../utils/productionRiskConfirm';
import { stripSchemaFromTabObjectLabel } from '../utils/tabDisplay';
interface TableOverviewProps {
tab: TabData;
@@ -142,6 +143,21 @@ const getMetadataDialect = (connType: string, driver?: string, oceanBaseProtocol
return type;
};
const isSchemaScopedTableOverviewDialect = (dialect: string): boolean => [
'postgres',
'kingbase',
'vastbase',
'highgo',
'opengauss',
'gaussdb',
].includes(dialect);
const getTableOverviewDisplayName = (dialect: string, tableName: string): string => {
const rawName = String(tableName || '').trim();
if (!isSchemaScopedTableOverviewDialect(dialect)) return rawName;
return stripSchemaFromTabObjectLabel(rawName) || rawName;
};
const buildTableStatusSQL = (dialect: string, dbName: string, schemaName?: string): string => {
const escapeLiteral = (s: string) => s.replace(/'/g, "''");
const iotdbDevicePattern = (name: string) => {
@@ -291,6 +307,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
[connection?.config?.driver, connection?.config?.oceanBaseProtocol, connection?.config?.type]
);
const schemaName = String((tab as any).schemaName || '').trim();
const overviewSchemaName = isSchemaScopedTableOverviewDialect(metadataDialect)
? (schemaName || 'public')
: '';
const supportsDesignWrite = !getDataSourceCapabilities(connection?.config).forceReadOnlyStructureDesigner;
const supportsCopyTable = getDataSourceCapabilities(connection?.config).supportsCopyTable;
const autoFetchVisible = useAutoFetchVisibility();
@@ -1186,9 +1205,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
>
<div className={isV2Ui ? 'gn-v2-table-card-name' : undefined} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<TableOutlined style={{ fontSize: 14, color: accentColor }} />
<Tooltip title={table.name} mouseEnterDelay={0.4}>
<Tooltip title={getTableOverviewDisplayName(metadataDialect, table.name)} mouseEnterDelay={0.4}>
<span style={{ fontSize: 13, fontWeight: 600, color: textPrimary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1, display: 'block' }}>
{table.name}
{getTableOverviewDisplayName(metadataDialect, table.name)}
</span>
</Tooltip>
</div>
@@ -1230,6 +1249,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
const renderListTable = (table: TableStatRow) => {
const combinedSize = getCombinedTableSize(table);
const displayName = getTableOverviewDisplayName(metadataDialect, table.name);
const sizeRatio = maxCombinedSize > 0 && hasKnownTableSize(table) ? combinedSize / maxCombinedSize : 0;
const fillWidth = maxCombinedSize > 0 && hasKnownTableSize(table) ? `${Math.max(10, Math.round(sizeRatio * 100))}%` : '0%';
const fillColor = isV2Ui
@@ -1285,9 +1305,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
<div style={{ minWidth: 0, flex: '1 1 320px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
<TableOutlined style={{ fontSize: 13, color: accentColor, flexShrink: 0 }} />
<Tooltip title={table.name} mouseEnterDelay={0.4}>
<Tooltip title={displayName} mouseEnterDelay={0.4}>
<span style={{ color: textPrimary, fontWeight: 600, fontSize: 13, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{table.name}
{displayName}
</span>
</Tooltip>
{table.engine && (
@@ -1423,6 +1443,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
);
const renderCompactTableRow = (table: TableStatRow) => {
const displayName = getTableOverviewDisplayName(metadataDialect, table.name);
const content = (
<div
className="gn-table-overview-compact-row"
@@ -1447,9 +1468,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
}),
}}
>
<div className="gn-table-overview-compact-name" role="cell" title={table.name}>
<div className="gn-table-overview-compact-name" role="cell" title={displayName}>
<TableOutlined aria-hidden="true" />
<span>{table.name}</span>
<span>{displayName}</span>
</div>
<div className="gn-table-overview-compact-cell" role="cell" title={table.comment || undefined}>{table.comment || '—'}</div>
<div className="gn-table-overview-compact-cell gn-table-overview-compact-number" role="cell" title={table.rows >= 0 ? String(table.rows) : undefined}>{formatRows(table.rows)}</div>
@@ -1508,7 +1529,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
<span className={isV2Ui ? 'gn-v2-table-overview-icon' : undefined}>
<DatabaseOutlined style={{ fontSize: 16, color: isV2Ui ? undefined : accentColor }} />
</span>
<span className={isV2Ui ? 'gn-v2-table-overview-title' : undefined} style={{ fontSize: 14, fontWeight: 600, color: textPrimary }}>{tab.dbName}</span>
<span className={isV2Ui ? 'gn-v2-table-overview-title' : undefined} style={{ fontSize: 14, fontWeight: 600, color: textPrimary }}>
{[tab.dbName, overviewSchemaName].filter(Boolean).join(' · ')}
</span>
<span className={isV2Ui ? 'gn-table-overview-summary gn-v2-table-overview-summary' : 'gn-table-overview-summary'} style={{ fontSize: 12, color: textMuted }}>
{renderToolbarSummary()}
</span>

View File

@@ -248,7 +248,7 @@ describe('tabDisplay', () => {
layout: 'single',
primaryElements: ['object', 'schema', 'host'],
secondaryElements: [],
})).toBe('andon_events SCHEMA:ldf_server 192.168.10.8');
})).toBe('andon_events ldf_server 192.168.10.8');
});
it('builds the default configurable model with the object on the primary line', () => {
@@ -404,8 +404,8 @@ describe('tabDisplay', () => {
expect(model.layout).toBe('double');
expect(model.primaryText).toBe('TABLE events');
expect(model.secondaryText).toBe('[PROD]·analytics·SCHEMA:reporting·10.0.0.9');
expect(model.fullTitle).toBe('TABLE events · [PROD]·analytics·SCHEMA:reporting·10.0.0.9');
expect(model.secondaryText).toBe('[PROD]·analytics·reporting·10.0.0.9');
expect(model.fullTitle).toBe('TABLE events · [PROD]·analytics·reporting·10.0.0.9');
});
it('uses explicit schema metadata for unqualified table names', () => {
@@ -426,7 +426,7 @@ describe('tabDisplay', () => {
});
expect(model.primaryText).toBe('events');
expect(model.secondaryText).toBe('SCHEMA:reporting');
expect(model.secondaryText).toBe('reporting');
});
it('sanitizes tab display settings with fallback defaults', () => {

View File

@@ -600,7 +600,6 @@ const getTabDisplayElementValue = (
const formatTabDisplayPartValue = (key: TabDisplayElementKey, value: string): string => {
if (!value) return '';
if (key === 'connection') return `[${value}]`;
if (key === 'schema') return `SCHEMA:${value}`;
return value;
};