diff --git a/frontend/src/components/DefinitionViewer.tsx b/frontend/src/components/DefinitionViewer.tsx index 1279c96..e063ee6 100644 --- a/frontend/src/components/DefinitionViewer.tsx +++ b/frontend/src/components/DefinitionViewer.tsx @@ -225,6 +225,27 @@ const DefinitionViewer: React.FC = ({ 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, dbName: string, @@ -366,6 +387,30 @@ const DefinitionViewer: React.FC = ({ 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 = ({ 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 = ({ 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 ( diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 7bc4929..69dafab 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -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', () => { diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 4b16739..20f1a6a 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -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 | null | undefined): boolean => { @@ -114,6 +114,7 @@ const isV2SidebarObjectNode = (node: Pick | 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, 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(); + 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: , + 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(); @@ -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', '物化视图', , bucket.materializedViews, { schemaName: bucket.schemaName })] : []), buildObjectGroup(schemaNodeKey, 'routines', '函数', , bucket.routines, { schemaName: bucket.schemaName }), buildObjectGroup(schemaNodeKey, 'triggers', '触发器', , bucket.triggers, { schemaName: bucket.schemaName }), + ...(includeEvents ? [buildObjectGroup(schemaNodeKey, 'events', '事件', , 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', '表', , sortedTableEntries.map(buildTableNode)), buildObjectGroup(key as string, 'views', '视图', , viewEntries.map(buildViewNode)), ...(includeMaterializedViews ? [buildObjectGroup(key as string, 'materializedViews', '物化视图', , materializedViewEntries.map(buildMaterializedViewNode))] : []), buildObjectGroup(key as string, 'routines', '函数', , routineEntries.map(buildRoutineNode)), buildObjectGroup(key as string, 'triggers', '触发器', , triggerEntries.map(buildTriggerNode)), + ...(includeEvents ? [buildObjectGroup(key as string, 'events', '事件', , 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: , 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' ? : , + icon: node.type === 'table' + ? + : (node.type === 'db-event' ? : (node.type === 'routine' ? : )), 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: , + 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: , + onClick: () => openEventDefinition(node) + }, + { + key: 'edit-event-query', + label: '编辑定义', + icon: , + 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('.'); diff --git a/frontend/src/components/TabManager.tsx b/frontend/src/components/TabManager.tsx index 584432c..e094165 100644 --- a/frontend/src/components/TabManager.tsx +++ b/frontend/src/components/TabManager.tsx @@ -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 = ; } else if (tab.type === 'trigger') { content = ; - } 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 = ; } else if (tab.type === 'table-overview') { content = ; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 89ba5a9..38ccb05 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -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