mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-15 11:14:31 +08:00
✨ feat(ui): 优化 Schema 标签与表概览显示
This commit is contained in:
@@ -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' },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user