mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-12 01:24:12 +08:00
- 读取 PostgreSQL 分区父子关系并隐藏分区主表行数 - 在主表下增加 Partitions 分组并保留分区表操作能力 - 兼容多级分区、跨 schema 显隐及异常元数据 - 补充分区树接入测试与多语言文案 Refs #750
1178 lines
60 KiB
TypeScript
1178 lines
60 KiB
TypeScript
import React, { useRef } from 'react';
|
||
import { message } from 'antd';
|
||
import {
|
||
CodeOutlined,
|
||
ClockCircleOutlined,
|
||
DashboardOutlined,
|
||
DatabaseOutlined,
|
||
EyeOutlined,
|
||
FileTextOutlined,
|
||
FolderOpenOutlined,
|
||
FunctionOutlined,
|
||
HddOutlined,
|
||
KeyOutlined,
|
||
LinkOutlined,
|
||
TableOutlined,
|
||
ThunderboltOutlined,
|
||
UnorderedListOutlined,
|
||
} from '@ant-design/icons';
|
||
import type { SavedConnection, SavedQuery, JVMCapability, JVMResourceSummary } from '../../types';
|
||
import { useStore } from '../../store';
|
||
import { t } from '../../i18n';
|
||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
|
||
import { buildRedisDbNodeLabel, getRedisDbAlias } from '../../utils/redisDbAlias';
|
||
import { buildJVMMonitoringActionDescriptors } from '../../utils/jvmSidebarActions';
|
||
import { getSchemaVisibilityRule, isSchemaVisible } from '../../utils/schemaVisibility';
|
||
import { type SidebarViewMetadataEntry } from '../../utils/sidebarMetadata';
|
||
import {
|
||
buildQualifiedName,
|
||
buildSidebarObjectKeyName,
|
||
buildSidebarTableStatusSQL,
|
||
getCaseInsensitiveValue,
|
||
getMetadataDialect,
|
||
getMySQLShowTablesName,
|
||
getSidebarTableName,
|
||
getSidebarTableDisplayName,
|
||
isSphinxConnection,
|
||
loadDatabaseEvents,
|
||
loadDatabaseTriggers,
|
||
loadFunctions,
|
||
loadPackages,
|
||
loadSchemas,
|
||
loadSequences,
|
||
loadStarRocksMaterializedViews,
|
||
loadViews,
|
||
parseMetadataRowCount,
|
||
shouldHideSchemaPrefix,
|
||
splitQualifiedName,
|
||
supportsDatabaseEvents,
|
||
supportsDatabaseSequences,
|
||
} from './sidebarMetadataLoaders';
|
||
import {
|
||
buildSidebarTableChildrenForUi,
|
||
isSidebarTablePinned,
|
||
sortSidebarTableEntries,
|
||
type SidebarConnectionState,
|
||
type SidebarTreeNode as TreeNode,
|
||
} from '../sidebarV2Utils';
|
||
import {
|
||
groupSidebarPartitionTableEntries,
|
||
} from './sidebarPartitions';
|
||
import { DBGetDatabases, DBGetTables, DBQuery, GetDriverStatusList, JVMProbeCapabilities } from '../../../wailsjs/go/app/App';
|
||
import type { SidebarTableMetadataSnapshot } from '../../utils/sidebarTableMetadata';
|
||
|
||
type DriverStatusSnapshot = {
|
||
type: string;
|
||
name: string;
|
||
connectable: boolean;
|
||
expectedRevision?: string;
|
||
needsUpdate?: boolean;
|
||
updateReason?: string;
|
||
message?: string;
|
||
};
|
||
|
||
type SidebarLoadedTableMetadata = SidebarTableMetadataSnapshot & {
|
||
schemaName?: string;
|
||
partitionParentTableName?: string;
|
||
};
|
||
|
||
type SidebarLoadedTableEntry = {
|
||
tableName: string;
|
||
schemaName: string;
|
||
displayName: string;
|
||
rowCount?: number;
|
||
tableSize?: number;
|
||
createdAt?: string;
|
||
updatedAt?: string;
|
||
tableComment?: string;
|
||
partitionParentTableName?: string;
|
||
partitionTables?: SidebarLoadedTableEntry[];
|
||
};
|
||
|
||
export const formatSidebarDriverAgentUpdateWarning = (
|
||
driverName: string,
|
||
status: Pick<DriverStatusSnapshot, 'message' | 'updateReason'>,
|
||
): string => {
|
||
const rawMessage = String(status.message || '').trim();
|
||
if (rawMessage) {
|
||
return rawMessage;
|
||
}
|
||
const rawUpdateReason = String(status.updateReason || '').trim();
|
||
if (rawUpdateReason) {
|
||
return rawUpdateReason;
|
||
}
|
||
return t('connection.modal.driver.updateFallback', { name: driverName });
|
||
};
|
||
|
||
const buildConnectionReloadSignature = (conn?: SavedConnection | null): string => {
|
||
if (!conn) return '';
|
||
return JSON.stringify({
|
||
config: conn.config || {},
|
||
includeDatabases: conn.includeDatabases || [],
|
||
includeRedisDatabases: conn.includeRedisDatabases || [],
|
||
schemaVisibilityByDatabase: conn.schemaVisibilityByDatabase || {},
|
||
});
|
||
};
|
||
|
||
const isConnectionTreeKey = (key: React.Key, connectionId: string): boolean => {
|
||
const text = String(key);
|
||
return text === connectionId || text.startsWith(`${connectionId}-`);
|
||
};
|
||
|
||
const DRIVER_STATUS_CACHE_TTL_MS = 30_000;
|
||
|
||
export const normalizeDriverType = (value: string): string => {
|
||
const normalized = String(value || '').trim().toLowerCase();
|
||
if (normalized === 'postgresql' || normalized === 'pg' || normalized === 'pq' || normalized === 'pgx') return 'postgres';
|
||
if (normalized === 'doris') return 'diros';
|
||
if (
|
||
normalized === 'open_gauss' ||
|
||
normalized === 'open-gauss' ||
|
||
normalized === 'opengauss'
|
||
) return 'opengauss';
|
||
if (
|
||
normalized === 'intersystems' ||
|
||
normalized === 'intersystemsiris' ||
|
||
normalized === 'inter-systems' ||
|
||
normalized === 'inter-systems-iris'
|
||
) return 'iris';
|
||
return normalized;
|
||
};
|
||
|
||
const resolveSavedConnectionDriverType = (conn: SavedConnection | undefined): string => {
|
||
const type = normalizeDriverType(conn?.config?.type || '');
|
||
if (type !== 'custom') {
|
||
return type;
|
||
}
|
||
return normalizeDriverType(conn?.config?.driver || '');
|
||
};
|
||
|
||
|
||
type UseSidebarTreeLoadersOptions = {
|
||
savedQueries: SavedQuery[];
|
||
tableSortPreference: Record<string, any>;
|
||
tableAccessCount: Record<string, any>;
|
||
pinnedSidebarTables: any[];
|
||
isV2Ui: boolean;
|
||
loadingNodesRef: React.MutableRefObject<Set<string>>;
|
||
setConnectionStates: React.Dispatch<React.SetStateAction<Record<string, SidebarConnectionState>>>;
|
||
setLoadedKeys: React.Dispatch<React.SetStateAction<React.Key[]>>;
|
||
replaceTreeNodeChildren: (key: React.Key, children: TreeNode[] | undefined, dataRef?: unknown) => TreeNode[];
|
||
buildRuntimeConfig: (conn: any, overrideDatabase?: string, clearDatabase?: boolean) => any;
|
||
buildJVMRuntimeConfig: (conn: SavedConnection & { dbName?: string }, providerMode: string) => any;
|
||
buildJVMDiagnosticTreeNodes: (conn: SavedConnection) => TreeNode[];
|
||
resolveSavedQueryDisplayName: (name: string | null | undefined) => string;
|
||
onDatabaseTreeLoaded?: (databaseKey: string) => void;
|
||
};
|
||
|
||
export const useSidebarTreeLoaders = ({
|
||
savedQueries,
|
||
tableSortPreference,
|
||
tableAccessCount,
|
||
pinnedSidebarTables,
|
||
isV2Ui,
|
||
loadingNodesRef,
|
||
setConnectionStates,
|
||
setLoadedKeys,
|
||
replaceTreeNodeChildren,
|
||
buildRuntimeConfig,
|
||
buildJVMRuntimeConfig,
|
||
buildJVMDiagnosticTreeNodes,
|
||
resolveSavedQueryDisplayName,
|
||
onDatabaseTreeLoaded,
|
||
}: UseSidebarTreeLoadersOptions) => {
|
||
const driverStatusCacheRef = useRef<{
|
||
fetchedAt: number;
|
||
items: Record<string, DriverStatusSnapshot>;
|
||
} | null>(null);
|
||
const driverUpdateWarningKeysRef = useRef<Set<string>>(new Set());
|
||
|
||
const fetchDriverStatusMap = async (): Promise<Record<string, DriverStatusSnapshot>> => {
|
||
const cached = driverStatusCacheRef.current;
|
||
if (cached && Date.now() - cached.fetchedAt < DRIVER_STATUS_CACHE_TTL_MS) {
|
||
return cached.items;
|
||
}
|
||
const result: Record<string, DriverStatusSnapshot> = {};
|
||
const res = await GetDriverStatusList('', '');
|
||
if (!res?.success) {
|
||
return result;
|
||
}
|
||
const data = (res.data || {}) as any;
|
||
const drivers = Array.isArray(data.drivers) ? data.drivers : [];
|
||
drivers.forEach((item: any) => {
|
||
const type = normalizeDriverType(String(item.type || '').trim());
|
||
if (!type) return;
|
||
result[type] = {
|
||
type,
|
||
name: String(item.name || item.type || type).trim(),
|
||
connectable: !!item.connectable,
|
||
expectedRevision: String(item.expectedRevision || '').trim() || undefined,
|
||
needsUpdate: !!item.needsUpdate,
|
||
updateReason: String(item.updateReason || '').trim() || undefined,
|
||
message: String(item.message || '').trim() || undefined,
|
||
};
|
||
});
|
||
driverStatusCacheRef.current = { fetchedAt: Date.now(), items: result };
|
||
return result;
|
||
};
|
||
|
||
const warnIfConnectionDriverAgentNeedsUpdate = async (conn: SavedConnection) => {
|
||
try {
|
||
const driverType = resolveSavedConnectionDriverType(conn);
|
||
if (!driverType || driverType === 'custom') {
|
||
return;
|
||
}
|
||
const statusMap = await fetchDriverStatusMap();
|
||
const status = statusMap[driverType];
|
||
if (!status?.connectable || !status.needsUpdate) {
|
||
return;
|
||
}
|
||
const revisionKey = status.expectedRevision || status.updateReason || status.message || 'unknown';
|
||
const warningKey = `${conn.id}:${driverType}:${revisionKey}`;
|
||
if (driverUpdateWarningKeysRef.current.has(warningKey)) {
|
||
return;
|
||
}
|
||
driverUpdateWarningKeysRef.current.add(warningKey);
|
||
const driverName = status.name || driverType;
|
||
message.warning({
|
||
content: formatSidebarDriverAgentUpdateWarning(driverName, status),
|
||
key: `driver-agent-update-${conn.id}`,
|
||
duration: 10,
|
||
});
|
||
} catch (error) {
|
||
console.warn('检查驱动代理更新状态失败', error);
|
||
}
|
||
};
|
||
const loadDatabases = async (node: any) => {
|
||
const conn = node.dataRef as SavedConnection;
|
||
const loadKey = `dbs-${conn.id}`;
|
||
if (loadingNodesRef.current.has(loadKey)) return;
|
||
loadingNodesRef.current.add(loadKey);
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'loading' }));
|
||
let shouldMarkConnectionSuccess = false;
|
||
const config = {
|
||
...conn.config,
|
||
port: Number(conn.config.port),
|
||
password: conn.config.password || "",
|
||
database: conn.config.database || "",
|
||
useSSH: conn.config.useSSH || false,
|
||
ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" }
|
||
};
|
||
|
||
if (conn.config.type === 'jvm') {
|
||
try {
|
||
const res = await JVMProbeCapabilities(buildRuntimeConfig(conn) as any);
|
||
if (res.success) {
|
||
const capabilities: JVMCapability[] = Array.isArray(res.data) ? res.data as JVMCapability[] : [];
|
||
const modeNodes: TreeNode[] = capabilities.map((capability) => ({
|
||
title: capability.displayLabel || capability.mode,
|
||
key: `${conn.id}-jvm-mode-${capability.mode}`,
|
||
icon: <HddOutlined />,
|
||
type: 'jvm-mode',
|
||
dataRef: {
|
||
...conn,
|
||
providerMode: capability.mode,
|
||
canBrowse: capability.canBrowse,
|
||
canWrite: capability.canWrite,
|
||
reason: capability.reason,
|
||
displayLabel: capability.displayLabel,
|
||
},
|
||
isLeaf: capability.canBrowse !== true,
|
||
}));
|
||
const monitoringNodes: TreeNode[] = buildJVMMonitoringActionDescriptors(conn.id, capabilities).map((item) => ({
|
||
title: item.title,
|
||
key: item.key,
|
||
icon: <DashboardOutlined />,
|
||
type: 'jvm-monitoring',
|
||
dataRef: {
|
||
...conn,
|
||
providerMode: item.providerMode,
|
||
},
|
||
isLeaf: true,
|
||
}));
|
||
const diagnosticNode = buildJVMDiagnosticTreeNodes(conn);
|
||
replaceTreeNodeChildren(node.key, [...monitoringNodes, ...modeNodes, ...diagnosticNode]);
|
||
shouldMarkConnectionSuccess = true;
|
||
} else {
|
||
const diagnosticNode = buildJVMDiagnosticTreeNodes(conn);
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
|
||
if (diagnosticNode.length > 0) {
|
||
replaceTreeNodeChildren(node.key, diagnosticNode);
|
||
message.warning({
|
||
content: t('sidebar.message.jvm_provider_probe_failed_with_diagnostic', {
|
||
error: res.message || t('sidebar.error.unknown'),
|
||
}),
|
||
key: `conn-${conn.id}-jvm-caps`,
|
||
});
|
||
} else {
|
||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||
message.error({ content: res.message, key: `conn-${conn.id}-jvm-caps` });
|
||
}
|
||
}
|
||
} catch (e: any) {
|
||
const diagnosticNode = buildJVMDiagnosticTreeNodes(conn);
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
|
||
if (diagnosticNode.length > 0) {
|
||
replaceTreeNodeChildren(node.key, diagnosticNode);
|
||
message.warning({
|
||
content: t('sidebar.message.jvm_provider_probe_exception_with_diagnostic', {
|
||
error: e?.message || String(e),
|
||
}),
|
||
key: `conn-${conn.id}-jvm-caps`,
|
||
});
|
||
} else {
|
||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||
message.error({
|
||
content: t('sidebar.message.connection_failed', { error: e?.message || String(e) }),
|
||
key: `conn-${conn.id}-jvm-caps`,
|
||
});
|
||
}
|
||
} finally {
|
||
loadingNodesRef.current.delete(loadKey);
|
||
if (shouldMarkConnectionSuccess) {
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Handle Redis connections differently
|
||
if (conn.config.type === 'redis') {
|
||
try {
|
||
const res = await (window as any).go.app.App.RedisGetDatabases(buildRpcConnectionConfig(config));
|
||
if (res.success) {
|
||
const redisRows: any[] = Array.isArray(res.data) ? res.data : [];
|
||
const redisDbAliases = useStore.getState().appearance.redisDbAliases;
|
||
let dbs = redisRows.map((db: any) => {
|
||
const keyCount = Number(db.keys) > 0 ? Number(db.keys) : 0;
|
||
const alias = getRedisDbAlias(redisDbAliases, conn.id, db.index);
|
||
return {
|
||
title: buildRedisDbNodeLabel(
|
||
db.index,
|
||
alias,
|
||
),
|
||
key: `${conn.id}-db${db.index}`,
|
||
icon: <DatabaseOutlined style={{ color: '#DC382D' }} />,
|
||
type: 'redis-db' as const,
|
||
dataRef: { ...conn, redisDB: db.index, redisKeyCount: keyCount, redisDbAlias: alias },
|
||
isLeaf: true,
|
||
dbIndex: db.index,
|
||
};
|
||
});
|
||
// Filter Redis databases if configured
|
||
if (conn.includeRedisDatabases && conn.includeRedisDatabases.length > 0) {
|
||
dbs = dbs.filter(db => conn.includeRedisDatabases!.includes(db.dbIndex));
|
||
}
|
||
replaceTreeNodeChildren(node.key, dbs, conn);
|
||
shouldMarkConnectionSuccess = true;
|
||
} else {
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
|
||
message.error({ content: res.message, key: `conn-${conn.id}-dbs` });
|
||
}
|
||
} catch (e: any) {
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
|
||
message.error({
|
||
content: t('sidebar.message.connection_failed', { error: e?.message || String(e) }),
|
||
key: `conn-${conn.id}-dbs`,
|
||
});
|
||
} finally {
|
||
loadingNodesRef.current.delete(loadKey);
|
||
if (shouldMarkConnectionSuccess) {
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await DBGetDatabases(buildRpcConnectionConfig(config) as any);
|
||
if (res.success) {
|
||
const dbRows: any[] = Array.isArray(res.data) ? res.data : [];
|
||
let dbs = dbRows.map((row: any) => ({
|
||
title: row.Database || row.database,
|
||
key: `${conn.id}-${row.Database || row.database}`,
|
||
icon: <DatabaseOutlined />,
|
||
type: 'database' as const,
|
||
dataRef: { ...conn, dbName: row.Database || row.database },
|
||
isLeaf: false,
|
||
}));
|
||
|
||
// Filter databases if configured
|
||
if (conn.includeDatabases && conn.includeDatabases.length > 0) {
|
||
dbs = dbs.filter(db => conn.includeDatabases!.includes(db.title));
|
||
}
|
||
|
||
if (dbs.length > 0) {
|
||
replaceTreeNodeChildren(node.key, dbs, conn);
|
||
} else {
|
||
// 空列表:清理 loadedKeys 以允许重新加载,不设置 children = []
|
||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||
message.warning({ content: t('sidebar.message.no_visible_databases'), key: `conn-${conn.id}-dbs` });
|
||
}
|
||
shouldMarkConnectionSuccess = true;
|
||
} else {
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
|
||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||
message.error({ content: res.message, key: `conn-${conn.id}-dbs` });
|
||
}
|
||
} catch (e: any) {
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
|
||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||
message.error({
|
||
content: t('sidebar.message.connection_failed', { error: e?.message || String(e) }),
|
||
key: `conn-${conn.id}-dbs`,
|
||
});
|
||
} finally {
|
||
loadingNodesRef.current.delete(loadKey);
|
||
if (shouldMarkConnectionSuccess) {
|
||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
|
||
}
|
||
}
|
||
};
|
||
|
||
const loadJVMResources = async (node: any) => {
|
||
const conn = node.dataRef as SavedConnection & { providerMode?: string; resourcePath?: string };
|
||
const providerMode = String(conn.providerMode || '').trim().toLowerCase();
|
||
const parentPath = String(conn.resourcePath || '').trim();
|
||
const loadKey = `jvm-resources-${conn.id}-${providerMode}-${parentPath}`;
|
||
if (loadingNodesRef.current.has(loadKey)) return;
|
||
loadingNodesRef.current.add(loadKey);
|
||
|
||
try {
|
||
const backendApp = (window as any).go?.app?.App;
|
||
if (typeof backendApp?.JVMListResources !== 'function') {
|
||
throw new Error(t('sidebar.message.jvm_resources_backend_unavailable'));
|
||
}
|
||
|
||
const res = await backendApp.JVMListResources(buildJVMRuntimeConfig(conn, providerMode), parentPath);
|
||
if (res.success) {
|
||
const resourceRows: JVMResourceSummary[] = Array.isArray(res.data) ? res.data as JVMResourceSummary[] : [];
|
||
const resourceNodes: TreeNode[] = resourceRows.map((item) => ({
|
||
title: item.name || item.path || item.id,
|
||
key: `${conn.id}-jvm-resource-${providerMode}-${item.path}`,
|
||
icon: item.hasChildren ? <FolderOpenOutlined /> : <HddOutlined />,
|
||
type: 'jvm-resource',
|
||
dataRef: {
|
||
...conn,
|
||
providerMode: item.providerMode || providerMode,
|
||
resourcePath: item.path,
|
||
resourceKind: item.kind,
|
||
canRead: item.canRead,
|
||
canWrite: item.canWrite,
|
||
hasChildren: item.hasChildren,
|
||
sensitive: item.sensitive,
|
||
},
|
||
isLeaf: item.hasChildren !== true,
|
||
}));
|
||
replaceTreeNodeChildren(node.key, resourceNodes);
|
||
} else {
|
||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||
message.error({ content: res.message, key: `jvm-resource-${node.key}` });
|
||
}
|
||
} catch (e: any) {
|
||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||
message.error({
|
||
content: t('sidebar.message.load_jvm_resources_failed', { error: e?.message || String(e) }),
|
||
key: `jvm-resource-${node.key}`,
|
||
});
|
||
} finally {
|
||
loadingNodesRef.current.delete(loadKey);
|
||
}
|
||
};
|
||
|
||
const loadTables = async (node: any) => {
|
||
const conn = node.dataRef; // has dbName
|
||
const dbName = conn.dbName;
|
||
const key = node.key;
|
||
const loadKey = `tables-${conn.id}-${dbName}`;
|
||
if (loadingNodesRef.current.has(loadKey)) return;
|
||
loadingNodesRef.current.add(loadKey);
|
||
setConnectionStates(prev => ({ ...prev, [key as string]: 'loading' }));
|
||
let shouldMarkDatabaseSuccess = false;
|
||
|
||
const dbQueries = savedQueries.filter(q => q.connectionId === conn.id && q.dbName === dbName);
|
||
const queriesNode: TreeNode = {
|
||
title: t('sidebar.tree.saved_queries'),
|
||
key: `${key}-queries`,
|
||
icon: <FolderOpenOutlined />,
|
||
type: 'queries-folder',
|
||
isLeaf: dbQueries.length === 0,
|
||
children: dbQueries.map(q => ({
|
||
title: resolveSavedQueryDisplayName(q.name),
|
||
key: q.id,
|
||
icon: <FileTextOutlined />,
|
||
type: 'saved-query',
|
||
dataRef: q,
|
||
isLeaf: true
|
||
}))
|
||
};
|
||
|
||
const config = {
|
||
...conn.config,
|
||
port: Number(conn.config.port),
|
||
password: conn.config.password || "",
|
||
database: conn.config.database || "",
|
||
useSSH: conn.config.useSSH || false,
|
||
ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" }
|
||
};
|
||
try {
|
||
const res = await DBGetTables(buildRpcConnectionConfig(config) as any, conn.dbName);
|
||
if (res.success) {
|
||
const tableRows: any[] = Array.isArray(res.data) ? res.data : [];
|
||
const tableStatusSql = buildSidebarTableStatusSQL(conn as SavedConnection, conn.dbName);
|
||
const tableStatsResult = tableStatusSql
|
||
? await DBQuery(buildRpcConnectionConfig(config) as any, conn.dbName, tableStatusSql).catch(() => ({ success: false, data: [] as any[] }))
|
||
: { success: false, data: [] as any[] };
|
||
const tableMetadataMap = new Map<string, SidebarLoadedTableMetadata>();
|
||
const buildTableMetadataKeys = (rawTableName: string, rawSchemaName = ''): string[] => {
|
||
const tableName = String(rawTableName || '').trim();
|
||
if (!tableName) return [];
|
||
const parsed = splitQualifiedName(tableName);
|
||
const schemaName = String(rawSchemaName || parsed.schemaName || '').trim();
|
||
const objectName = String(parsed.objectName || tableName).trim();
|
||
const keys = new Set<string>([tableName.toLowerCase()]);
|
||
if (objectName) keys.add(objectName.toLowerCase());
|
||
const qualifiedName = buildQualifiedName(schemaName, objectName || tableName);
|
||
if (qualifiedName) keys.add(qualifiedName.toLowerCase());
|
||
return Array.from(keys);
|
||
};
|
||
const readNumericMetadataValue = (row: Record<string, any>, keys: string[]): number | undefined => {
|
||
const rawValue = getCaseInsensitiveValue(row, keys);
|
||
if (rawValue === undefined || rawValue === null || rawValue === '') return undefined;
|
||
const numericValue = Number(String(rawValue).replace(/,/g, ''));
|
||
return Number.isFinite(numericValue) ? numericValue : undefined;
|
||
};
|
||
const normalizeMetadataTimestamp = (rawValue: unknown): string | undefined => {
|
||
if (rawValue === undefined || rawValue === null) return undefined;
|
||
const normalized = String(rawValue).trim();
|
||
return normalized ? normalized : undefined;
|
||
};
|
||
const mergeTableMetadata = (
|
||
rawTableName: string,
|
||
patch: SidebarLoadedTableMetadata,
|
||
rawSchemaName = '',
|
||
) => {
|
||
buildTableMetadataKeys(rawTableName, rawSchemaName).forEach((metadataKey) => {
|
||
const current = tableMetadataMap.get(metadataKey) || {};
|
||
tableMetadataMap.set(metadataKey, {
|
||
...current,
|
||
...(patch.schemaName ? { schemaName: patch.schemaName } : {}),
|
||
...(patch.partitionParentTableName ? { partitionParentTableName: patch.partitionParentTableName } : {}),
|
||
...(patch.tableComment ? { tableComment: patch.tableComment } : {}),
|
||
...(patch.rowCount !== undefined ? { rowCount: patch.rowCount } : {}),
|
||
...(patch.tableSize !== undefined ? { tableSize: patch.tableSize } : {}),
|
||
...(patch.createdAt ? { createdAt: patch.createdAt } : {}),
|
||
...(patch.updatedAt ? { updatedAt: patch.updatedAt } : {}),
|
||
});
|
||
});
|
||
};
|
||
tableRows.forEach((row: Record<string, any>) => {
|
||
const tableName = getSidebarTableName(row);
|
||
const rowCount = parseMetadataRowCount(row);
|
||
if (tableName && rowCount !== undefined) {
|
||
mergeTableMetadata(tableName, { rowCount });
|
||
}
|
||
});
|
||
if (tableStatsResult?.success && Array.isArray(tableStatsResult.data)) {
|
||
tableStatsResult.data.forEach((row: Record<string, any>) => {
|
||
const rawTableName = String(
|
||
getCaseInsensitiveValue(row, ['table_name', 'TABLE_NAME', 'Name', 'name'])
|
||
|| getMySQLShowTablesName(row)
|
||
|| ''
|
||
).trim();
|
||
if (!rawTableName) return;
|
||
const rawSchemaName = getCaseInsensitiveValue(row, ['schema_name', 'SCHEMA_NAME', 'owner', 'OWNER']);
|
||
const partitionParentTableName = String(getCaseInsensitiveValue(row, [
|
||
'partition_parent_table',
|
||
'PARTITION_PARENT_TABLE',
|
||
]) || '').trim();
|
||
const tableComment = String(getCaseInsensitiveValue(row, [
|
||
'table_comment',
|
||
'TABLE_COMMENT',
|
||
'comment',
|
||
'Comment',
|
||
'comments',
|
||
'COMMENTS',
|
||
'MS_Description',
|
||
]) || '').trim();
|
||
const rowCount = parseMetadataRowCount(row);
|
||
const tableSize = readNumericMetadataValue(row, [
|
||
'table_size',
|
||
'TABLE_SIZE',
|
||
'data_length',
|
||
'DATA_LENGTH',
|
||
'total_bytes',
|
||
'TOTAL_BYTES',
|
||
]);
|
||
const createdAt = normalizeMetadataTimestamp(getCaseInsensitiveValue(row, [
|
||
'create_time',
|
||
'CREATE_TIME',
|
||
'created_at',
|
||
'CREATED_AT',
|
||
'create_date',
|
||
'CREATE_DATE',
|
||
]));
|
||
const updatedAt = normalizeMetadataTimestamp(getCaseInsensitiveValue(row, [
|
||
'update_time',
|
||
'UPDATE_TIME',
|
||
'updated_at',
|
||
'UPDATED_AT',
|
||
'modify_date',
|
||
'MODIFY_DATE',
|
||
'last_ddl_time',
|
||
'LAST_DDL_TIME',
|
||
]));
|
||
mergeTableMetadata(rawTableName, {
|
||
schemaName: rawSchemaName ? String(rawSchemaName).trim() : undefined,
|
||
...(partitionParentTableName ? { partitionParentTableName } : {}),
|
||
...(tableComment ? { tableComment } : {}),
|
||
...(rowCount !== undefined ? { rowCount } : {}),
|
||
...(tableSize !== undefined ? { tableSize } : {}),
|
||
...(createdAt ? { createdAt } : {}),
|
||
...(updatedAt ? { updatedAt } : {}),
|
||
}, rawSchemaName);
|
||
});
|
||
}
|
||
const tableEntries = tableRows.map((row: any) => {
|
||
const tableName = getSidebarTableName(row as Record<string, any>);
|
||
const parsed = splitQualifiedName(tableName);
|
||
const metadataKeys = buildTableMetadataKeys(tableName);
|
||
const resolvedMetadata = metadataKeys
|
||
.map((metadataKey) => tableMetadataMap.get(metadataKey))
|
||
.find((value): value is SidebarLoadedTableMetadata => !!value);
|
||
const rowSchemaName = getCaseInsensitiveValue(row, ['schema_name', 'SCHEMA_NAME', 'owner', 'OWNER']);
|
||
const mappedSchemaName = rowSchemaName
|
||
|| resolvedMetadata?.schemaName
|
||
|| parsed.schemaName;
|
||
const rowComment = getCaseInsensitiveValue(row, [
|
||
'table_comment',
|
||
'TABLE_COMMENT',
|
||
'comment',
|
||
'Comment',
|
||
'comments',
|
||
'COMMENTS',
|
||
]);
|
||
return {
|
||
tableName,
|
||
schemaName: String(mappedSchemaName || '').trim(),
|
||
displayName: getSidebarTableDisplayName(conn, tableName),
|
||
rowCount: parseMetadataRowCount(row) ?? resolvedMetadata?.rowCount,
|
||
tableSize: resolvedMetadata?.tableSize,
|
||
createdAt: resolvedMetadata?.createdAt,
|
||
updatedAt: resolvedMetadata?.updatedAt,
|
||
tableComment: rowComment
|
||
|| resolvedMetadata?.tableComment
|
||
|| '',
|
||
partitionParentTableName: resolvedMetadata?.partitionParentTableName,
|
||
};
|
||
}) as SidebarLoadedTableEntry[];
|
||
|
||
const [schemasResult, viewsResult, materializedViewsResult, triggersResult, routinesResult, sequencesResult, packagesResult, eventsResult] = await Promise.all([
|
||
loadSchemas(conn, conn.dbName),
|
||
loadViews(conn, conn.dbName),
|
||
loadStarRocksMaterializedViews(conn, conn.dbName),
|
||
loadDatabaseTriggers(conn, conn.dbName),
|
||
loadFunctions(conn, conn.dbName),
|
||
loadSequences(conn, conn.dbName),
|
||
loadPackages(conn, conn.dbName),
|
||
loadDatabaseEvents(conn, conn.dbName),
|
||
]);
|
||
const viewRows: SidebarViewMetadataEntry[] = Array.isArray(viewsResult.views) ? viewsResult.views : [];
|
||
const materializedViewRows: SidebarViewMetadataEntry[] = Array.isArray(materializedViewsResult.views) ? materializedViewsResult.views : [];
|
||
const triggerRows: any[] = Array.isArray(triggersResult.triggers) ? triggersResult.triggers : [];
|
||
const routineRows: any[] = Array.isArray(routinesResult.routines) ? routinesResult.routines : [];
|
||
const sequenceRows: any[] = Array.isArray(sequencesResult.sequences) ? sequencesResult.sequences : [];
|
||
const packageRows: any[] = Array.isArray(packagesResult.packages) ? packagesResult.packages : [];
|
||
const eventRows: any[] = Array.isArray(eventsResult.events) ? eventsResult.events : [];
|
||
const schemaRows: string[] = Array.isArray(schemasResult.schemas) ? schemasResult.schemas : [];
|
||
const normalizedSchemaRows = schemaRows
|
||
.map((schemaName) => String(schemaName || '').trim())
|
||
.filter((schemaName) => schemaName !== '');
|
||
const normalizedTableEntries = tableEntries.map((entry) => {
|
||
if (entry.schemaName || normalizedSchemaRows.length !== 1) {
|
||
return entry;
|
||
}
|
||
return {
|
||
...entry,
|
||
schemaName: normalizedSchemaRows[0],
|
||
};
|
||
});
|
||
|
||
const viewEntries = viewRows.map((entry: SidebarViewMetadataEntry) => {
|
||
const parsed = splitQualifiedName(entry.viewName);
|
||
return {
|
||
viewName: entry.viewName,
|
||
schemaName: entry.schemaName || parsed.schemaName,
|
||
displayName: getSidebarTableDisplayName(conn, entry.viewName),
|
||
};
|
||
});
|
||
|
||
const materializedViewEntries = materializedViewRows.map((entry: SidebarViewMetadataEntry) => {
|
||
const parsed = splitQualifiedName(entry.viewName);
|
||
return {
|
||
viewName: entry.viewName,
|
||
schemaName: entry.schemaName || parsed.schemaName,
|
||
displayName: getSidebarTableDisplayName(conn, entry.viewName),
|
||
};
|
||
});
|
||
|
||
const triggerEntries = (() => {
|
||
const deduped: Array<{ displayName: string; triggerName: string; tableName: string; schemaName: string }> = [];
|
||
const triggerSeen = new Set<string>();
|
||
const metadataDialect = getMetadataDialect(conn as SavedConnection);
|
||
|
||
triggerRows.forEach((trigger: any) => {
|
||
const triggerParsed = splitQualifiedName(trigger.triggerName);
|
||
const tableParsed = splitQualifiedName(trigger.tableName);
|
||
const schemaName = tableParsed.schemaName || triggerParsed.schemaName || String(conn.dbName || '').trim();
|
||
const triggerObjectName = (triggerParsed.objectName || trigger.triggerName).trim();
|
||
const tableObjectName = (tableParsed.objectName || trigger.tableName).trim();
|
||
const displayName = tableObjectName ? `${triggerObjectName} (${tableObjectName})` : triggerObjectName;
|
||
const dedupeKey = metadataDialect === 'mysql'
|
||
? `${schemaName.toLowerCase()}@@${triggerObjectName.toLowerCase()}`
|
||
: `${schemaName.toLowerCase()}@@${triggerObjectName.toLowerCase()}@@${tableObjectName.toLowerCase()}`;
|
||
|
||
if (triggerSeen.has(dedupeKey)) return;
|
||
triggerSeen.add(dedupeKey);
|
||
deduped.push({
|
||
...trigger,
|
||
schemaName,
|
||
triggerName: triggerObjectName,
|
||
tableName: buildQualifiedName(schemaName, tableObjectName) || tableObjectName,
|
||
displayName,
|
||
});
|
||
});
|
||
|
||
return deduped;
|
||
})();
|
||
|
||
const routineEntries = (() => {
|
||
const deduped: Array<{ routineName: string; routineType: string; schemaName: string; displayName: string }> = [];
|
||
const routineSeen = new Set<string>();
|
||
routineRows.forEach((routine: any) => {
|
||
const parsed = splitQualifiedName(routine.routineName);
|
||
const routineType = String(routine.routineType || 'FUNCTION').toUpperCase().includes('PROC')
|
||
? 'PROCEDURE'
|
||
: 'FUNCTION';
|
||
const schemaName = String(parsed.schemaName || routine.schemaName || '').trim();
|
||
const objectName = String(parsed.objectName || routine.routineName || '').trim();
|
||
if (!objectName) return;
|
||
const routineName = String(routine.routineName || objectName).trim();
|
||
const typeLabel = routineType === 'PROCEDURE' ? 'P' : 'F';
|
||
const dedupeKey = `${schemaName.toLowerCase()}@@${objectName.toLowerCase()}@@${routineType}`;
|
||
if (routineSeen.has(dedupeKey)) return;
|
||
routineSeen.add(dedupeKey);
|
||
deduped.push({
|
||
routineName,
|
||
routineType,
|
||
schemaName,
|
||
displayName: `${objectName} [${typeLabel}]`,
|
||
});
|
||
});
|
||
return deduped;
|
||
})();
|
||
|
||
const sequenceEntries = sequenceRows.map((sequence: any) => {
|
||
const parsed = splitQualifiedName(sequence.sequenceName);
|
||
return {
|
||
...sequence,
|
||
schemaName: sequence.schemaName || parsed.schemaName,
|
||
displayName: parsed.objectName || sequence.sequenceName,
|
||
};
|
||
});
|
||
|
||
const packageEntries = packageRows.map((packageEntry: any) => {
|
||
const parsed = splitQualifiedName(packageEntry.packageName);
|
||
return {
|
||
...packageEntry,
|
||
schemaName: packageEntry.schemaName || parsed.schemaName,
|
||
displayName: parsed.objectName || packageEntry.packageName,
|
||
};
|
||
});
|
||
|
||
const eventEntries = eventRows.map((event: any) => ({
|
||
...event,
|
||
schemaName: String(event.schemaName || conn.dbName || '').trim(),
|
||
displayName: String(event.displayName || event.eventName || '').trim(),
|
||
})).filter((event: any) => event.eventName && event.displayName);
|
||
|
||
if (isSphinxConnection(conn as SavedConnection)) {
|
||
const unsupportedObjects: string[] = [];
|
||
if (!viewsResult.supported) unsupportedObjects.push(t('sidebar.object_group.views'));
|
||
if (!routinesResult.supported) unsupportedObjects.push(t('sidebar.object_group.routines'));
|
||
if (!triggersResult.supported) unsupportedObjects.push(t('sidebar.object_group.triggers'));
|
||
if (unsupportedObjects.length > 0) {
|
||
message.info({
|
||
key: `sphinx-capability-${conn.id}-${conn.dbName}`,
|
||
content: t('sidebar.message.sphinx_unsupported_objects', {
|
||
objects: unsupportedObjects.join(t('sidebar.punctuation.list_separator')),
|
||
}),
|
||
});
|
||
}
|
||
}
|
||
|
||
const currentStoreState = useStore.getState();
|
||
const currentTableSortPreference = currentStoreState.tableSortPreference || tableSortPreference;
|
||
const currentTableAccessCount = currentStoreState.tableAccessCount || tableAccessCount;
|
||
const currentPinnedSidebarTables = currentStoreState.pinnedSidebarTables || pinnedSidebarTables;
|
||
// Metadata loading can overlap with a schema visibility save. Build partition
|
||
// relationships from the newest visible table set so hidden schemas cannot leak
|
||
// through a visible parent, and visible children do not disappear with a hidden parent.
|
||
const latestConnection = useStore.getState().connections.find(
|
||
(candidate) => candidate.id === conn.id,
|
||
) || conn;
|
||
const latestDatabaseConnection = { ...latestConnection, dbName };
|
||
const shouldGroupBySchema = shouldHideSchemaPrefix(latestDatabaseConnection as SavedConnection);
|
||
const schemaVisibilityRule = getSchemaVisibilityRule(latestDatabaseConnection, dbName);
|
||
|
||
// 获取当前数据库的排序偏好
|
||
const sortPreferenceKey = `${conn.id}-${conn.dbName}`;
|
||
const sortBy = currentTableSortPreference[sortPreferenceKey] || 'name';
|
||
|
||
const sortedTableEntries = groupSidebarPartitionTableEntries(sortSidebarTableEntries(normalizedTableEntries, {
|
||
connectionId: conn.id,
|
||
dbName: conn.dbName,
|
||
sortBy,
|
||
tableAccessCount: currentTableAccessCount,
|
||
pinnedSidebarTables: isV2Ui ? currentPinnedSidebarTables : [],
|
||
}), {
|
||
isEntryVisible: (entry) => !shouldGroupBySchema
|
||
|| isSchemaVisible(schemaVisibilityRule, entry.schemaName),
|
||
}) as SidebarLoadedTableEntry[];
|
||
|
||
// Sort views by name (case-insensitive)
|
||
viewEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||
|
||
materializedViewEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||
|
||
// Sort triggers by display name (case-insensitive)
|
||
triggerEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||
|
||
// Sort routines by display name (case-insensitive)
|
||
routineEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||
|
||
sequenceEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||
|
||
packageEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||
|
||
eventEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||
|
||
const buildTableNode = (entry: SidebarLoadedTableEntry): TreeNode => {
|
||
const isPinned = isV2Ui && isSidebarTablePinned(
|
||
currentPinnedSidebarTables,
|
||
conn.id,
|
||
conn.dbName,
|
||
entry.tableName,
|
||
entry.schemaName,
|
||
);
|
||
const nodeKey = `${conn.id}-${conn.dbName}-${entry.tableName}`;
|
||
const tableDataRef = {
|
||
...conn,
|
||
tableName: entry.tableName,
|
||
schemaName: entry.schemaName,
|
||
...(entry.rowCount !== undefined ? { rowCount: entry.rowCount } : {}),
|
||
tableSize: entry.tableSize,
|
||
createdAt: entry.createdAt,
|
||
updatedAt: entry.updatedAt,
|
||
tableComment: entry.tableComment,
|
||
...(isPinned ? { pinnedSidebarTable: true } : {}),
|
||
};
|
||
const partitionNodes = (entry.partitionTables || []).map(buildTableNode);
|
||
const children: TreeNode[] | undefined = partitionNodes.length > 0
|
||
? [
|
||
{
|
||
title: t('sidebar.table_folder.columns'),
|
||
key: `${nodeKey}-columns`,
|
||
icon: <UnorderedListOutlined />,
|
||
type: 'folder-columns',
|
||
isLeaf: true,
|
||
dataRef: tableDataRef,
|
||
},
|
||
{
|
||
title: t('sidebar.table_folder.indexes'),
|
||
key: `${nodeKey}-indexes`,
|
||
icon: <KeyOutlined style={{ transform: 'rotate(45deg)' }} />,
|
||
type: 'folder-indexes',
|
||
isLeaf: true,
|
||
dataRef: tableDataRef,
|
||
},
|
||
{
|
||
title: t('sidebar.table_folder.foreign_keys'),
|
||
key: `${nodeKey}-fks`,
|
||
icon: <LinkOutlined />,
|
||
type: 'folder-fks',
|
||
isLeaf: true,
|
||
dataRef: tableDataRef,
|
||
},
|
||
{
|
||
title: t('sidebar.table_folder.triggers'),
|
||
key: `${nodeKey}-triggers`,
|
||
icon: <ThunderboltOutlined />,
|
||
type: 'folder-triggers',
|
||
isLeaf: true,
|
||
dataRef: tableDataRef,
|
||
},
|
||
{
|
||
title: t('sidebar.table_folder.partitions'),
|
||
key: `${nodeKey}-partitions`,
|
||
icon: <FolderOpenOutlined />,
|
||
type: 'object-group',
|
||
isLeaf: false,
|
||
selectable: false,
|
||
children: partitionNodes,
|
||
dataRef: {
|
||
...tableDataRef,
|
||
groupKey: 'partitions',
|
||
partitionCount: partitionNodes.length,
|
||
},
|
||
},
|
||
]
|
||
: undefined;
|
||
return {
|
||
title: entry.displayName,
|
||
key: nodeKey,
|
||
icon: <TableOutlined />,
|
||
type: 'table',
|
||
dataRef: tableDataRef,
|
||
...(children ? { children } : {}),
|
||
isLeaf: false,
|
||
};
|
||
};
|
||
|
||
const buildViewNode = (entry: { viewName: string; schemaName: string; displayName: string }): TreeNode => {
|
||
const keyName = buildSidebarObjectKeyName(conn.dbName, entry.schemaName, entry.viewName);
|
||
return {
|
||
title: entry.displayName,
|
||
key: `${conn.id}-${conn.dbName}-view-${keyName}`,
|
||
icon: <EyeOutlined />,
|
||
type: 'view',
|
||
dataRef: { ...conn, viewName: entry.viewName, tableName: entry.viewName, schemaName: entry.schemaName },
|
||
isLeaf: true,
|
||
};
|
||
};
|
||
|
||
const buildMaterializedViewNode = (entry: { viewName: string; schemaName: string; displayName: string }): TreeNode => {
|
||
const keyName = buildSidebarObjectKeyName(conn.dbName, entry.schemaName, entry.viewName);
|
||
return {
|
||
title: entry.displayName,
|
||
key: `${conn.id}-${conn.dbName}-materialized-view-${keyName}`,
|
||
icon: <ThunderboltOutlined />,
|
||
type: 'materialized-view',
|
||
dataRef: { ...conn, viewName: entry.viewName, tableName: entry.viewName, schemaName: entry.schemaName, objectKind: 'materialized-view' },
|
||
isLeaf: true,
|
||
};
|
||
};
|
||
|
||
const buildTriggerNode = (entry: { triggerName: string; tableName: string; schemaName: string; displayName: string }): TreeNode => ({
|
||
title: entry.displayName,
|
||
key: `${conn.id}-${conn.dbName}-trigger-${entry.triggerName}-${entry.tableName}`,
|
||
icon: <FunctionOutlined />,
|
||
type: 'db-trigger',
|
||
dataRef: { ...conn, triggerName: entry.triggerName, triggerTableName: entry.tableName, tableName: entry.tableName, schemaName: entry.schemaName },
|
||
isLeaf: true,
|
||
});
|
||
|
||
const buildRoutineNode = (entry: { routineName: string; routineType: string; schemaName: string; displayName: string }): TreeNode => {
|
||
const typeToken = entry.routineType === 'PROCEDURE' ? 'proc' : 'func';
|
||
const keyName = buildSidebarObjectKeyName(conn.dbName, entry.schemaName, entry.routineName);
|
||
return {
|
||
title: entry.displayName,
|
||
// 必须带 routineType:同名函数/过程否则 key 冲突,虚拟列表会叠成“同一函数无限重复”
|
||
key: `${conn.id}-${conn.dbName}-routine-${typeToken}-${keyName}`,
|
||
icon: <CodeOutlined />,
|
||
type: 'routine',
|
||
dataRef: { ...conn, routineName: entry.routineName, routineType: entry.routineType, schemaName: entry.schemaName },
|
||
isLeaf: true,
|
||
};
|
||
};
|
||
|
||
const buildSequenceNode = (entry: { sequenceName: string; schemaName: string; displayName: string }): TreeNode => {
|
||
const keyName = buildSidebarObjectKeyName(conn.dbName, entry.schemaName, entry.sequenceName);
|
||
return {
|
||
title: entry.displayName,
|
||
key: `${conn.id}-${conn.dbName}-sequence-${keyName}`,
|
||
icon: <KeyOutlined />,
|
||
type: 'sequence',
|
||
dataRef: { ...conn, sequenceName: entry.sequenceName, schemaName: entry.schemaName },
|
||
isLeaf: true,
|
||
};
|
||
};
|
||
|
||
const buildPackageNode = (entry: { packageName: string; schemaName: string; displayName: string }): TreeNode => {
|
||
const keyName = buildSidebarObjectKeyName(conn.dbName, entry.schemaName, entry.packageName);
|
||
return {
|
||
title: entry.displayName,
|
||
key: `${conn.id}-${conn.dbName}-package-${keyName}`,
|
||
icon: <CodeOutlined />,
|
||
type: 'package',
|
||
dataRef: { ...conn, packageName: entry.packageName, schemaName: entry.schemaName },
|
||
isLeaf: true,
|
||
};
|
||
};
|
||
|
||
const buildEventNode = (entry: { eventName: string; schemaName: string; displayName: string; eventType?: string; status?: string }): TreeNode => ({
|
||
title: entry.displayName,
|
||
key: `${conn.id}-${conn.dbName}-event-${entry.schemaName}-${entry.eventName}`,
|
||
icon: <ClockCircleOutlined />,
|
||
type: 'db-event',
|
||
dataRef: { ...conn, eventName: entry.eventName, schemaName: entry.schemaName, eventType: entry.eventType, eventStatus: entry.status },
|
||
isLeaf: true,
|
||
});
|
||
|
||
const buildObjectGroup = (
|
||
parentKey: string,
|
||
groupKey: string,
|
||
groupTitle: string,
|
||
groupIcon: React.ReactNode,
|
||
children: TreeNode[],
|
||
extraData: Record<string, any> = {}
|
||
): TreeNode => {
|
||
const groupNodeKey = `${parentKey}-${groupKey}`;
|
||
const groupedChildren = groupKey === 'tables'
|
||
? buildSidebarTableChildrenForUi(groupNodeKey, children, isV2Ui)
|
||
: children;
|
||
return {
|
||
title: groupTitle,
|
||
key: groupNodeKey,
|
||
icon: groupIcon,
|
||
type: 'object-group',
|
||
isLeaf: children.length === 0,
|
||
children: groupedChildren.length > 0 ? groupedChildren : undefined,
|
||
dataRef: { ...conn, dbName: conn.dbName, groupKey, ...extraData }
|
||
};
|
||
};
|
||
|
||
if (shouldGroupBySchema) {
|
||
type SchemaBucket = {
|
||
schemaName: string;
|
||
tables: TreeNode[];
|
||
views: TreeNode[];
|
||
materializedViews: TreeNode[];
|
||
routines: TreeNode[];
|
||
sequences: TreeNode[];
|
||
packages: TreeNode[];
|
||
triggers: TreeNode[];
|
||
events: TreeNode[];
|
||
};
|
||
|
||
const schemaMap = new Map<string, SchemaBucket>();
|
||
const getSchemaBucket = (rawSchemaName: string): SchemaBucket => {
|
||
const schemaName = String(rawSchemaName || '').trim();
|
||
const schemaKey = schemaName || '__default__';
|
||
let bucket = schemaMap.get(schemaKey);
|
||
if (!bucket) {
|
||
bucket = {
|
||
schemaName,
|
||
tables: [],
|
||
views: [],
|
||
materializedViews: [],
|
||
routines: [],
|
||
sequences: [],
|
||
packages: [],
|
||
triggers: [],
|
||
events: [],
|
||
};
|
||
schemaMap.set(schemaKey, bucket);
|
||
}
|
||
return bucket;
|
||
};
|
||
|
||
schemaRows.forEach((schemaName) => getSchemaBucket(schemaName));
|
||
sortedTableEntries.forEach((entry) => getSchemaBucket(entry.schemaName).tables.push(buildTableNode(entry)));
|
||
viewEntries.forEach((entry) => getSchemaBucket(entry.schemaName).views.push(buildViewNode(entry)));
|
||
materializedViewEntries.forEach((entry) => getSchemaBucket(entry.schemaName).materializedViews.push(buildMaterializedViewNode(entry)));
|
||
routineEntries.forEach((entry) => getSchemaBucket(entry.schemaName).routines.push(buildRoutineNode(entry)));
|
||
sequenceEntries.forEach((entry) => getSchemaBucket(entry.schemaName).sequences.push(buildSequenceNode(entry)));
|
||
packageEntries.forEach((entry) => getSchemaBucket(entry.schemaName).packages.push(buildPackageNode(entry)));
|
||
triggerEntries.forEach((entry) => getSchemaBucket(entry.schemaName).triggers.push(buildTriggerNode(entry)));
|
||
eventEntries.forEach((entry) => getSchemaBucket(entry.schemaName).events.push(buildEventNode(entry)));
|
||
|
||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||
const isOracleLike = (dialect === 'oracle' || dialect === 'dm');
|
||
const includeMaterializedViews = dialect === 'starrocks';
|
||
const includeOracleObjects = isOracleLike;
|
||
const includeSequences = supportsDatabaseSequences(conn as SavedConnection);
|
||
const includeEvents = supportsDatabaseEvents(conn as SavedConnection);
|
||
|
||
const schemaNodes: TreeNode[] = Array.from(schemaMap.values())
|
||
.filter((bucket) => !(isOracleLike && !bucket.schemaName))
|
||
.filter((bucket) => isSchemaVisible(schemaVisibilityRule, bucket.schemaName))
|
||
.sort((a, b) => {
|
||
if (!a.schemaName && !b.schemaName) return 0;
|
||
if (!a.schemaName) return -1;
|
||
if (!b.schemaName) return 1;
|
||
return a.schemaName.toLowerCase().localeCompare(b.schemaName.toLowerCase());
|
||
})
|
||
.map((bucket) => {
|
||
const schemaNodeKey = `${key}-schema-${bucket.schemaName || 'default'}`;
|
||
const schemaTitle = bucket.schemaName || t('sidebar.tree.default_schema');
|
||
const groupedNodes: TreeNode[] = [
|
||
buildObjectGroup(schemaNodeKey, 'tables', t('sidebar.object_group.tables'), <TableOutlined />, bucket.tables, { schemaName: bucket.schemaName }),
|
||
buildObjectGroup(schemaNodeKey, 'views', t('sidebar.object_group.views'), <EyeOutlined />, bucket.views, { schemaName: bucket.schemaName }),
|
||
...(includeMaterializedViews ? [buildObjectGroup(schemaNodeKey, 'materializedViews', t('sidebar.object_group.materialized_views'), <ThunderboltOutlined />, bucket.materializedViews, { schemaName: bucket.schemaName })] : []),
|
||
...(includeSequences ? [buildObjectGroup(schemaNodeKey, 'sequences', t('sidebar.object_group.sequences'), <KeyOutlined />, bucket.sequences, { schemaName: bucket.schemaName })] : []),
|
||
buildObjectGroup(schemaNodeKey, 'routines', t('sidebar.object_group.routines'), <CodeOutlined />, bucket.routines, { schemaName: bucket.schemaName }),
|
||
...(includeOracleObjects ? [buildObjectGroup(schemaNodeKey, 'packages', t('sidebar.object_group.packages'), <CodeOutlined />, bucket.packages, { schemaName: bucket.schemaName })] : []),
|
||
buildObjectGroup(schemaNodeKey, 'triggers', t('sidebar.object_group.triggers'), <FunctionOutlined />, bucket.triggers, { schemaName: bucket.schemaName }),
|
||
...(includeEvents ? [buildObjectGroup(schemaNodeKey, 'events', t('sidebar.object_group.events'), <ClockCircleOutlined />, bucket.events, { schemaName: bucket.schemaName })] : []),
|
||
];
|
||
|
||
return {
|
||
title: schemaTitle,
|
||
key: schemaNodeKey,
|
||
icon: <FolderOpenOutlined />,
|
||
type: 'object-group' as const,
|
||
isLeaf: groupedNodes.length === 0,
|
||
children: groupedNodes,
|
||
dataRef: { ...conn, dbName: conn.dbName, groupKey: 'schema', schemaName: bucket.schemaName }
|
||
};
|
||
});
|
||
|
||
replaceTreeNodeChildren(key, [queriesNode, ...schemaNodes], latestDatabaseConnection);
|
||
} else {
|
||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||
const includeMaterializedViews = dialect === 'starrocks';
|
||
const includeOracleObjects = dialect === 'oracle' || dialect === 'dm';
|
||
const includeSequences = supportsDatabaseSequences(conn as SavedConnection);
|
||
const includeEvents = supportsDatabaseEvents(conn as SavedConnection);
|
||
const groupedNodes: TreeNode[] = [
|
||
buildObjectGroup(key as string, 'tables', t('sidebar.object_group.tables'), <TableOutlined />, sortedTableEntries.map(buildTableNode)),
|
||
buildObjectGroup(key as string, 'views', t('sidebar.object_group.views'), <EyeOutlined />, viewEntries.map(buildViewNode)),
|
||
...(includeMaterializedViews ? [buildObjectGroup(key as string, 'materializedViews', t('sidebar.object_group.materialized_views'), <ThunderboltOutlined />, materializedViewEntries.map(buildMaterializedViewNode))] : []),
|
||
...(includeSequences ? [buildObjectGroup(key as string, 'sequences', t('sidebar.object_group.sequences'), <KeyOutlined />, sequenceEntries.map(buildSequenceNode))] : []),
|
||
buildObjectGroup(key as string, 'routines', t('sidebar.object_group.routines'), <CodeOutlined />, routineEntries.map(buildRoutineNode)),
|
||
...(includeOracleObjects ? [buildObjectGroup(key as string, 'packages', t('sidebar.object_group.packages'), <CodeOutlined />, packageEntries.map(buildPackageNode))] : []),
|
||
buildObjectGroup(key as string, 'triggers', t('sidebar.object_group.triggers'), <FunctionOutlined />, triggerEntries.map(buildTriggerNode)),
|
||
...(includeEvents ? [buildObjectGroup(key as string, 'events', t('sidebar.object_group.events'), <ClockCircleOutlined />, eventEntries.map(buildEventNode))] : []),
|
||
];
|
||
|
||
replaceTreeNodeChildren(key, [queriesNode, ...groupedNodes], latestDatabaseConnection);
|
||
}
|
||
onDatabaseTreeLoaded?.(String(key));
|
||
shouldMarkDatabaseSuccess = true;
|
||
} else {
|
||
setConnectionStates(prev => ({ ...prev, [key as string]: 'error' }));
|
||
message.error({ content: res.message, key: `db-${key}-tables` });
|
||
}
|
||
} catch (e: any) {
|
||
setConnectionStates(prev => ({ ...prev, [key as string]: 'error' }));
|
||
message.error({
|
||
content: t('sidebar.message.load_table_list_failed', { error: e?.message || String(e) }),
|
||
key: `db-${key}-tables`,
|
||
});
|
||
} finally {
|
||
loadingNodesRef.current.delete(loadKey);
|
||
if (shouldMarkDatabaseSuccess) {
|
||
setConnectionStates(prev => ({ ...prev, [key as string]: 'success' }));
|
||
}
|
||
}
|
||
};
|
||
|
||
|
||
return {
|
||
loadDatabases,
|
||
loadJVMResources,
|
||
loadTables,
|
||
};
|
||
};
|