mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-31 10:29:39 +08:00
✨ feat(mysql): 新增左侧事件对象展示
- 加载 MySQL 事件元数据并展示事件分组 - 支持双击事件查看定义 - 兼容旧版侧边栏与新版 UI 筛选 Refs #411
This commit is contained in:
@@ -225,6 +225,27 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const buildShowEventQueries = (dialect: string, eventName: string, dbName: string): string[] => {
|
||||
const { schema, name } = parseSchemaAndName(eventName);
|
||||
const safeName = escapeSQLLiteral(name);
|
||||
const safeSchema = escapeSQLLiteral(schema || dbName);
|
||||
const eventRef = schema
|
||||
? `\`${schema.replace(/`/g, '``')}\`.\`${name.replace(/`/g, '``')}\``
|
||||
: `\`${name.replace(/`/g, '``')}\``;
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
return [
|
||||
`SHOW CREATE EVENT ${eventRef}`,
|
||||
safeSchema
|
||||
? `SELECT EVENT_SCHEMA AS schema_name, EVENT_NAME AS event_name, EVENT_DEFINITION AS event_definition, EVENT_TYPE AS event_type, EXECUTE_AT AS execute_at, INTERVAL_VALUE AS interval_value, INTERVAL_FIELD AS interval_field, STARTS AS starts, ENDS AS ends, STATUS AS status, ON_COMPLETION AS on_completion, EVENT_COMMENT AS event_comment FROM information_schema.events WHERE event_schema = '${safeSchema}' AND event_name = '${safeName}' LIMIT 1`
|
||||
: '',
|
||||
].filter(Boolean);
|
||||
default:
|
||||
return [`-- 暂不支持该数据库类型的事件定义查看`];
|
||||
}
|
||||
};
|
||||
|
||||
const runQueryCandidates = async (
|
||||
config: Record<string, any>,
|
||||
dbName: string,
|
||||
@@ -366,6 +387,30 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const extractEventDefinition = (dialect: string, data: any[]): string => {
|
||||
if (!data || data.length === 0) return '-- 未找到事件定义';
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql': {
|
||||
const row = data[0];
|
||||
const keys = Object.keys(row);
|
||||
const sqlKey = keys.find(k => k.toLowerCase().includes('create event'));
|
||||
if (sqlKey && row[sqlKey]) return String(row[sqlKey]);
|
||||
|
||||
const definition = row.event_definition || row.EVENT_DEFINITION;
|
||||
const eventName = row.event_name || row.EVENT_NAME || row.Name || row.name;
|
||||
if (definition && eventName) {
|
||||
return `-- 当前数据源未返回完整 CREATE EVENT 语句,已返回事件定义片段\n-- 名称: ${eventName}\n${String(definition)}`;
|
||||
}
|
||||
return JSON.stringify(row, null, 2);
|
||||
}
|
||||
default: {
|
||||
const row = data[0];
|
||||
return row.event_definition || row.EVENT_DEFINITION || Object.values(row)[0] || '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadDefinition = async () => {
|
||||
setLoading(true);
|
||||
@@ -396,6 +441,16 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
queries = buildShowViewQueries(dialect, viewName, dbName, tab.viewKind);
|
||||
extractFn = extractViewDefinition;
|
||||
objectLabel = tab.viewKind === 'materialized' ? '物化视图' : '视图';
|
||||
} else if (tab.type === 'event-def') {
|
||||
const eventName = tab.eventName || '';
|
||||
if (!eventName) {
|
||||
setError('事件名称为空');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
queries = buildShowEventQueries(dialect, eventName, dbName);
|
||||
extractFn = extractEventDefinition;
|
||||
objectLabel = '事件';
|
||||
} else {
|
||||
const routineName = tab.routineName || '';
|
||||
const routineType = tab.routineType || 'FUNCTION';
|
||||
@@ -456,10 +511,14 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
};
|
||||
|
||||
loadDefinition();
|
||||
}, [tab.connectionId, tab.dbName, tab.viewName, tab.viewKind, tab.routineName, tab.routineType, tab.type, connections]);
|
||||
}, [tab.connectionId, tab.dbName, tab.viewName, tab.viewKind, tab.eventName, tab.routineName, tab.routineType, tab.type, connections]);
|
||||
|
||||
const objectLabel = tab.type === 'view-def' ? (tab.viewKind === 'materialized' ? '物化视图' : '视图') : '函数/存储过程';
|
||||
const objectName = tab.type === 'view-def' ? tab.viewName : tab.routineName;
|
||||
const objectLabel = tab.type === 'view-def'
|
||||
? (tab.viewKind === 'materialized' ? '物化视图' : '视图')
|
||||
: (tab.type === 'event-def' ? '事件' : '函数/存储过程');
|
||||
const objectName = tab.type === 'view-def'
|
||||
? tab.viewName
|
||||
: (tab.type === 'event-def' ? tab.eventName : tab.routineName);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
@@ -520,6 +520,13 @@ describe('Sidebar locate toolbar', () => {
|
||||
dataRef: { groupKey: 'routines' },
|
||||
children: [{ title: 'calc_total', key: 'calc_total', type: 'routine' as const }],
|
||||
},
|
||||
{
|
||||
title: '事件',
|
||||
key: 'conn-main-events',
|
||||
type: 'object-group' as const,
|
||||
dataRef: { groupKey: 'events' },
|
||||
children: [{ title: 'daily_cleanup', key: 'daily_cleanup', type: 'db-event' as const }],
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
@@ -528,10 +535,12 @@ describe('Sidebar locate toolbar', () => {
|
||||
'conn-main-tables',
|
||||
'conn-main-views',
|
||||
'conn-main-routines',
|
||||
'conn-main-events',
|
||||
]);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'tables')[0].children?.map((node) => node.key)).toEqual(['conn-main-tables']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'views')[0].children?.map((node) => node.key)).toEqual(['conn-main-views']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'routines')[0].children?.map((node) => node.key)).toEqual(['conn-main-routines']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'events')[0].children?.map((node) => node.key)).toEqual(['conn-main-events']);
|
||||
});
|
||||
|
||||
it('renders the v2 table context menu with the redesigned table layout', () => {
|
||||
|
||||
@@ -106,7 +106,7 @@ interface TreeNode {
|
||||
children?: TreeNode[];
|
||||
icon?: React.ReactNode;
|
||||
dataRef?: any;
|
||||
type?: 'connection' | 'database' | 'table' | 'view' | 'materialized-view' | 'db-trigger' | 'routine' | 'object-group' | 'queries-folder' | 'saved-query' | 'external-sql-root' | 'external-sql-directory' | 'external-sql-folder' | 'external-sql-file' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers' | 'redis-db' | 'tag' | 'jvm-mode' | 'jvm-resource' | 'jvm-diagnostic' | 'jvm-monitoring';
|
||||
type?: 'connection' | 'database' | 'table' | 'view' | 'materialized-view' | 'db-trigger' | 'db-event' | 'routine' | 'object-group' | 'queries-folder' | 'saved-query' | 'external-sql-root' | 'external-sql-directory' | 'external-sql-folder' | 'external-sql-file' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers' | 'redis-db' | 'tag' | 'jvm-mode' | 'jvm-resource' | 'jvm-diagnostic' | 'jvm-monitoring';
|
||||
}
|
||||
|
||||
const isV2SidebarObjectNode = (node: Pick<TreeNode, 'type'> | null | undefined): boolean => {
|
||||
@@ -114,6 +114,7 @@ const isV2SidebarObjectNode = (node: Pick<TreeNode, 'type'> | null | undefined):
|
||||
|| node?.type === 'view'
|
||||
|| node?.type === 'materialized-view'
|
||||
|| node?.type === 'db-trigger'
|
||||
|| node?.type === 'db-event'
|
||||
|| node?.type === 'routine';
|
||||
};
|
||||
|
||||
@@ -196,7 +197,7 @@ type BatchObjectType = 'table' | 'view';
|
||||
type BatchObjectFilterType = 'all' | BatchObjectType;
|
||||
type BatchSelectionScope = 'filtered' | 'all';
|
||||
type SearchScope = 'smart' | 'object' | 'database' | 'host' | 'tag';
|
||||
type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines';
|
||||
type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines' | 'events';
|
||||
|
||||
export const V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID = '__gonavi-v2-ungrouped-connections__';
|
||||
|
||||
@@ -257,12 +258,14 @@ const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; label: string }
|
||||
{ key: 'tables', label: '表' },
|
||||
{ key: 'views', label: '视图' },
|
||||
{ key: 'routines', label: '函数' },
|
||||
{ key: 'events', label: '事件' },
|
||||
];
|
||||
|
||||
const V2_EXPLORER_FILTER_GROUP_KEYS: Record<Exclude<V2ExplorerFilter, 'all'>, string[]> = {
|
||||
tables: ['tables'],
|
||||
views: ['views', 'materializedViews'],
|
||||
routines: ['routines'],
|
||||
events: ['events'],
|
||||
};
|
||||
|
||||
export const filterV2ExplorerTreeByKind = (
|
||||
@@ -275,6 +278,7 @@ export const filterV2ExplorerTreeByKind = (
|
||||
if (filter === 'tables') return node.type === 'table';
|
||||
if (filter === 'views') return node.type === 'view' || node.type === 'materialized-view';
|
||||
if (filter === 'routines') return node.type === 'routine';
|
||||
if (filter === 'events') return node.type === 'db-event';
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1130,6 +1134,10 @@ const Sidebar: React.FC<{
|
||||
return type;
|
||||
};
|
||||
|
||||
const supportsDatabaseEvents = (conn: SavedConnection | undefined): boolean => {
|
||||
return getMetadataDialect(conn) === 'mysql';
|
||||
};
|
||||
|
||||
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
|
||||
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;
|
||||
|
||||
@@ -1426,6 +1434,23 @@ const Sidebar: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const buildEventsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
if (dialect !== 'mysql') {
|
||||
return [];
|
||||
}
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT EVENT_SCHEMA AS schema_name, EVENT_NAME AS event_name, EVENT_TYPE AS event_type, STATUS AS status FROM information_schema.events WHERE event_schema = '${safeDbName}' ORDER BY EVENT_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW EVENTS FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW EVENTS` },
|
||||
]);
|
||||
};
|
||||
|
||||
const buildSchemasMetadataQuerySpecs = (dialect: string): MetadataQuerySpec[] => {
|
||||
if (!isPostgresSchemaDialect(dialect)) {
|
||||
return [];
|
||||
@@ -1604,6 +1629,46 @@ const Sidebar: React.FC<{
|
||||
return { routines, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabaseEvents = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ events: Array<{ displayName: string; eventName: string; schemaName: string; eventType: string; status: string }>; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildEventsMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const events: Array<{ displayName: string; eventName: string; schemaName: string; eventType: string; status: string }> = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rawEventName = getCaseInsensitiveValue(row, ['event_name', 'eventname', 'name', 'event']);
|
||||
if (!rawEventName) return;
|
||||
|
||||
const rawSchemaName = getCaseInsensitiveValue(row, ['schema_name', 'event_schema', 'db', 'database']);
|
||||
const parsed = splitQualifiedName(rawEventName);
|
||||
const schemaName = (rawSchemaName || parsed.schemaName || dbName).trim();
|
||||
const eventName = (parsed.objectName || rawEventName).trim();
|
||||
if (!eventName) return;
|
||||
|
||||
const uniqueKey = `${schemaName.toLowerCase()}@@${eventName.toLowerCase()}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
|
||||
const eventType = getCaseInsensitiveValue(row, ['event_type', 'type']);
|
||||
const status = getCaseInsensitiveValue(row, ['status']);
|
||||
events.push({
|
||||
displayName: eventName,
|
||||
eventName,
|
||||
schemaName,
|
||||
eventType,
|
||||
status,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { events, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadSchemas = async (conn: any, dbName: string): Promise<{ schemas: string[]; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildSchemasMetadataQuerySpecs(dialect);
|
||||
@@ -1931,12 +1996,13 @@ const Sidebar: React.FC<{
|
||||
};
|
||||
});
|
||||
|
||||
const [schemasResult, viewsResult, materializedViewsResult, triggersResult, routinesResult] = await Promise.all([
|
||||
const [schemasResult, viewsResult, materializedViewsResult, triggersResult, routinesResult, eventsResult] = await Promise.all([
|
||||
loadSchemas(conn, conn.dbName),
|
||||
loadViews(conn, conn.dbName),
|
||||
loadStarRocksMaterializedViews(conn, conn.dbName),
|
||||
loadDatabaseTriggers(conn, conn.dbName),
|
||||
loadFunctions(conn, conn.dbName),
|
||||
loadDatabaseEvents(conn, conn.dbName),
|
||||
]);
|
||||
const externalSQLDirectoryResults = await Promise.all(
|
||||
dbExternalSQLDirectories.map(async (directory) => {
|
||||
@@ -1970,6 +2036,7 @@ const Sidebar: React.FC<{
|
||||
const materializedViewRows: string[] = 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 eventRows: any[] = Array.isArray(eventsResult.events) ? eventsResult.events : [];
|
||||
const schemaRows: string[] = Array.isArray(schemasResult.schemas) ? schemasResult.schemas : [];
|
||||
|
||||
const viewEntries = viewRows.map((viewName: string) => {
|
||||
@@ -2030,6 +2097,12 @@ const Sidebar: React.FC<{
|
||||
};
|
||||
});
|
||||
|
||||
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('视图');
|
||||
@@ -2071,6 +2144,8 @@ const Sidebar: React.FC<{
|
||||
// Sort routines by display name (case-insensitive)
|
||||
routineEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||||
|
||||
eventEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||||
|
||||
const buildTableNode = (entry: { tableName: string; schemaName: string; displayName: string }): TreeNode => {
|
||||
const isPinned = isSidebarTablePinned(currentPinnedSidebarTables, conn.id, conn.dbName, entry.tableName, entry.schemaName);
|
||||
return {
|
||||
@@ -2119,6 +2194,15 @@ const Sidebar: React.FC<{
|
||||
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,
|
||||
@@ -2145,6 +2229,7 @@ const Sidebar: React.FC<{
|
||||
materializedViews: TreeNode[];
|
||||
routines: TreeNode[];
|
||||
triggers: TreeNode[];
|
||||
events: TreeNode[];
|
||||
};
|
||||
|
||||
const schemaMap = new Map<string, SchemaBucket>();
|
||||
@@ -2160,6 +2245,7 @@ const Sidebar: React.FC<{
|
||||
materializedViews: [],
|
||||
routines: [],
|
||||
triggers: [],
|
||||
events: [],
|
||||
};
|
||||
schemaMap.set(schemaKey, bucket);
|
||||
}
|
||||
@@ -2172,10 +2258,12 @@ const Sidebar: React.FC<{
|
||||
materializedViewEntries.forEach((entry) => getSchemaBucket(entry.schemaName).materializedViews.push(buildMaterializedViewNode(entry)));
|
||||
routineEntries.forEach((entry) => getSchemaBucket(entry.schemaName).routines.push(buildRoutineNode(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 includeEvents = supportsDatabaseEvents(conn as SavedConnection);
|
||||
|
||||
const schemaNodes: TreeNode[] = Array.from(schemaMap.values())
|
||||
.filter((bucket) => !(isOracleLike && !bucket.schemaName))
|
||||
@@ -2194,6 +2282,7 @@ const Sidebar: React.FC<{
|
||||
...(includeMaterializedViews ? [buildObjectGroup(schemaNodeKey, 'materializedViews', '物化视图', <ThunderboltOutlined />, bucket.materializedViews, { schemaName: bucket.schemaName })] : []),
|
||||
buildObjectGroup(schemaNodeKey, 'routines', '函数', <CodeOutlined />, bucket.routines, { schemaName: bucket.schemaName }),
|
||||
buildObjectGroup(schemaNodeKey, 'triggers', '触发器', <FunctionOutlined />, bucket.triggers, { schemaName: bucket.schemaName }),
|
||||
...(includeEvents ? [buildObjectGroup(schemaNodeKey, 'events', '事件', <ClockCircleOutlined />, bucket.events, { schemaName: bucket.schemaName })] : []),
|
||||
];
|
||||
|
||||
return {
|
||||
@@ -2210,12 +2299,14 @@ const Sidebar: React.FC<{
|
||||
replaceTreeNodeChildren(key, [queriesNode, externalSQLRootNode, ...schemaNodes]);
|
||||
} else {
|
||||
const includeMaterializedViews = getMetadataDialect(conn as SavedConnection) === 'starrocks';
|
||||
const includeEvents = supportsDatabaseEvents(conn as SavedConnection);
|
||||
const groupedNodes: TreeNode[] = [
|
||||
buildObjectGroup(key as string, 'tables', '表', <TableOutlined />, sortedTableEntries.map(buildTableNode)),
|
||||
buildObjectGroup(key as string, 'views', '视图', <EyeOutlined />, viewEntries.map(buildViewNode)),
|
||||
...(includeMaterializedViews ? [buildObjectGroup(key as string, 'materializedViews', '物化视图', <ThunderboltOutlined />, materializedViewEntries.map(buildMaterializedViewNode))] : []),
|
||||
buildObjectGroup(key as string, 'routines', '函数', <CodeOutlined />, routineEntries.map(buildRoutineNode)),
|
||||
buildObjectGroup(key as string, 'triggers', '触发器', <FunctionOutlined />, triggerEntries.map(buildTriggerNode)),
|
||||
...(includeEvents ? [buildObjectGroup(key as string, 'events', '事件', <ClockCircleOutlined />, eventEntries.map(buildEventNode))] : []),
|
||||
];
|
||||
|
||||
replaceTreeNodeChildren(key, [queriesNode, externalSQLRootNode, ...groupedNodes]);
|
||||
@@ -2435,7 +2526,7 @@ const Sidebar: React.FC<{
|
||||
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
|
||||
} else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') {
|
||||
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: '' });
|
||||
} else if (type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'routine') {
|
||||
} else if (type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'db-event' || type === 'routine') {
|
||||
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
|
||||
} else if (type === 'saved-query') {
|
||||
setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
|
||||
@@ -2493,7 +2584,7 @@ const Sidebar: React.FC<{
|
||||
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
|
||||
} else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') {
|
||||
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: '' });
|
||||
} else if (type === 'table' || type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'routine') {
|
||||
} else if (type === 'table' || type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'db-event' || type === 'routine') {
|
||||
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
|
||||
} else if (type === 'saved-query') setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
|
||||
else if (type === 'external-sql-root' || type === 'external-sql-directory' || type === 'external-sql-folder' || type === 'external-sql-file') setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
|
||||
@@ -2559,6 +2650,9 @@ const Sidebar: React.FC<{
|
||||
triggerName
|
||||
});
|
||||
return;
|
||||
} else if (node.type === 'db-event') {
|
||||
openEventDefinition(node);
|
||||
return;
|
||||
} else if (node.type === 'routine') {
|
||||
const { routineName, routineType, dbName, id } = node.dataRef;
|
||||
const typeLabel = routineType === 'PROCEDURE' ? '存储过程' : '函数';
|
||||
@@ -4021,6 +4115,18 @@ const Sidebar: React.FC<{
|
||||
});
|
||||
};
|
||||
|
||||
const openEventDefinition = (node: any) => {
|
||||
const { eventName, dbName, id } = node.dataRef;
|
||||
addTab({
|
||||
id: `event-def-${id}-${dbName}-${eventName}`,
|
||||
title: `事件: ${eventName}`,
|
||||
type: 'event-def',
|
||||
connectionId: id,
|
||||
dbName,
|
||||
eventName,
|
||||
});
|
||||
};
|
||||
|
||||
const openEditRoutine = async (node: any) => {
|
||||
const conn = node.dataRef;
|
||||
const { routineName, routineType, dbName, id } = conn;
|
||||
@@ -4769,16 +4875,25 @@ const Sidebar: React.FC<{
|
||||
icon: <DatabaseOutlined />,
|
||||
node,
|
||||
});
|
||||
} else if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') {
|
||||
} else if (
|
||||
node.type === 'table'
|
||||
|| node.type === 'view'
|
||||
|| node.type === 'materialized-view'
|
||||
|| node.type === 'db-trigger'
|
||||
|| node.type === 'db-event'
|
||||
|| node.type === 'routine'
|
||||
) {
|
||||
const conn = connections.find((item) => item.id === dataRef.id);
|
||||
const objectName = String(dataRef.tableName || dataRef.viewName || node.title || '').trim();
|
||||
const objectName = String(dataRef.tableName || dataRef.viewName || dataRef.triggerName || dataRef.eventName || dataRef.routineName || node.title || '').trim();
|
||||
const displayName = String(node.title || extractObjectName(objectName) || objectName).trim();
|
||||
result.push({
|
||||
key: `node-${node.key}`,
|
||||
kind: 'node',
|
||||
title: displayName,
|
||||
meta: [conn?.name || dataRef.id, dataRef.dbName].filter(Boolean).join(' · '),
|
||||
icon: node.type === 'table' ? <TableOutlined /> : <EyeOutlined />,
|
||||
icon: node.type === 'table'
|
||||
? <TableOutlined />
|
||||
: (node.type === 'db-event' ? <ClockCircleOutlined /> : (node.type === 'routine' ? <CodeOutlined /> : <EyeOutlined />)),
|
||||
node,
|
||||
});
|
||||
}
|
||||
@@ -5352,6 +5467,7 @@ const Sidebar: React.FC<{
|
||||
if (groupKey === 'views') return '视图 · views';
|
||||
if (groupKey === 'routines') return '函数 · functions';
|
||||
if (groupKey === 'triggers') return '触发器 · triggers';
|
||||
if (groupKey === 'events') return '事件 · events';
|
||||
if (groupKey === 'materializedViews') return '物化视图 · materialized';
|
||||
}
|
||||
return rawTitle;
|
||||
@@ -5361,6 +5477,7 @@ const Sidebar: React.FC<{
|
||||
|| node.type === 'view'
|
||||
|| node.type === 'materialized-view'
|
||||
|| node.type === 'db-trigger'
|
||||
|| node.type === 'db-event'
|
||||
|| node.type === 'routine'
|
||||
|| node.type === 'saved-query'
|
||||
|| node.type === 'external-sql-file';
|
||||
@@ -5446,6 +5563,14 @@ const Sidebar: React.FC<{
|
||||
objectGroup: node.type === 'table' ? 'tables' : (node.type === 'materialized-view' ? 'materializedViews' : 'views'),
|
||||
});
|
||||
onDoubleClick(null, node);
|
||||
return;
|
||||
}
|
||||
if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine') {
|
||||
setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
setSelectedKeys([node.key]);
|
||||
selectedNodesRef.current = [node];
|
||||
scrollSidebarTreeToKey(node.key);
|
||||
onDoubleClick(null, node);
|
||||
}
|
||||
}, [activeContext, activeTab, addTab, closeV2CommandSearch, selectConnectionFromRail, setActiveContext]);
|
||||
|
||||
@@ -5639,6 +5764,26 @@ const Sidebar: React.FC<{
|
||||
return routineMenu;
|
||||
}
|
||||
|
||||
if (node.type === 'object-group' && node.dataRef?.groupKey === 'events') {
|
||||
return [
|
||||
{
|
||||
key: 'create-event-query',
|
||||
label: '新建事件',
|
||||
icon: <PlusOutlined />,
|
||||
onClick: () => {
|
||||
addTab({
|
||||
id: `query-create-event-${Date.now()}`,
|
||||
title: '新建事件',
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.dataRef.dbName,
|
||||
query: `CREATE EVENT event_name\nON SCHEDULE EVERY 1 DAY\nDO\nBEGIN\n -- event body\nEND;`
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Connection Tag Menu — must be BEFORE the connection check
|
||||
if (node.type === 'tag') {
|
||||
return [
|
||||
@@ -6197,6 +6342,31 @@ const Sidebar: React.FC<{
|
||||
]
|
||||
},
|
||||
];
|
||||
} else if (node.type === 'db-event') {
|
||||
return [
|
||||
{
|
||||
key: 'view-event-def',
|
||||
label: '查看定义',
|
||||
icon: <CodeOutlined />,
|
||||
onClick: () => openEventDefinition(node)
|
||||
},
|
||||
{
|
||||
key: 'edit-event-query',
|
||||
label: '编辑定义',
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
const { eventName, dbName, id } = node.dataRef;
|
||||
addTab({
|
||||
id: `query-edit-event-${Date.now()}`,
|
||||
title: `编辑事件: ${eventName}`,
|
||||
type: 'query',
|
||||
connectionId: id,
|
||||
dbName,
|
||||
query: `SHOW CREATE EVENT \`${String(eventName || '').replace(/`/g, '``')}\`;`
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
} else if (node.type === 'table') {
|
||||
const isStarRocks = getMetadataDialect(node.dataRef as SavedConnection) === 'starrocks';
|
||||
return [
|
||||
@@ -6418,8 +6588,8 @@ const Sidebar: React.FC<{
|
||||
|
||||
const displayTitle = String(node.title ?? '');
|
||||
let hoverTitle = displayTitle;
|
||||
if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') {
|
||||
const rawTableName = String(node?.dataRef?.tableName || node?.dataRef?.viewName || '').trim();
|
||||
if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view' || node.type === 'db-event') {
|
||||
const rawTableName = String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.eventName || '').trim();
|
||||
const conn = node?.dataRef as SavedConnection | undefined;
|
||||
if (rawTableName && shouldHideSchemaPrefix(conn)) {
|
||||
const lastDotIndex = rawTableName.lastIndexOf('.');
|
||||
|
||||
@@ -36,6 +36,7 @@ const getTabKindLabel = (tab: TabData): string => {
|
||||
if (tab.type.startsWith('jvm')) return 'JVM';
|
||||
if (tab.type === 'trigger') return 'TRG';
|
||||
if (tab.type === 'view-def') return tab.viewKind === 'materialized' ? 'MV' : 'VIEW';
|
||||
if (tab.type === 'event-def') return 'EVT';
|
||||
if (tab.type === 'routine-def') return 'FUNC';
|
||||
return 'TAB';
|
||||
};
|
||||
@@ -65,6 +66,7 @@ const getTabKindTooltipLabel = (tab: TabData): string => {
|
||||
if (tab.type === 'jvm-monitoring') return 'JVM 监控';
|
||||
if (tab.type === 'trigger') return '触发器';
|
||||
if (tab.type === 'view-def') return tab.viewKind === 'materialized' ? '物化视图' : '视图';
|
||||
if (tab.type === 'event-def') return '事件';
|
||||
if (tab.type === 'routine-def') return '函数 / 过程';
|
||||
return '标签页';
|
||||
};
|
||||
@@ -72,6 +74,7 @@ const getTabKindTooltipLabel = (tab: TabData): string => {
|
||||
const getTabObjectLabel = (tab: TabData): string => {
|
||||
if (tab.tableName) return tab.tableName;
|
||||
if (tab.viewName) return tab.viewName;
|
||||
if (tab.eventName) return tab.eventName;
|
||||
if (tab.routineName) return tab.routineName;
|
||||
if (tab.triggerName) return tab.triggerName;
|
||||
if (tab.resourcePath) return tab.resourcePath;
|
||||
@@ -410,7 +413,7 @@ const TabManager: React.FC = React.memo(() => {
|
||||
content = <RedisMonitor connectionId={tab.connectionId} redisDB={tab.redisDB ?? 0} />;
|
||||
} else if (tab.type === 'trigger') {
|
||||
content = <TriggerViewer tab={tab} />;
|
||||
} else if (tab.type === 'view-def' || tab.type === 'routine-def') {
|
||||
} else if (tab.type === 'view-def' || tab.type === 'event-def' || tab.type === 'routine-def') {
|
||||
content = <DefinitionViewer tab={tab} />;
|
||||
} else if (tab.type === 'table-overview') {
|
||||
content = <TableOverview tab={tab} />;
|
||||
|
||||
@@ -400,6 +400,7 @@ export interface TabData {
|
||||
| "redis-monitor"
|
||||
| "trigger"
|
||||
| "view-def"
|
||||
| "event-def"
|
||||
| "routine-def"
|
||||
| "table-overview"
|
||||
| "jvm-overview"
|
||||
@@ -421,6 +422,7 @@ export interface TabData {
|
||||
triggerName?: string; // Trigger name for trigger tabs
|
||||
viewName?: string; // View name for view definition tabs
|
||||
viewKind?: "view" | "materialized";
|
||||
eventName?: string; // Event name for MySQL event definition tabs
|
||||
routineName?: string; // Routine name for function/procedure definition tabs
|
||||
routineType?: string; // 'FUNCTION' or 'PROCEDURE'
|
||||
savedQueryId?: string; // Saved query identity for quick-save behavior
|
||||
|
||||
Reference in New Issue
Block a user