feat(mysql): 新增左侧事件对象展示

- 加载 MySQL 事件元数据并展示事件分组

- 支持双击事件查看定义

- 兼容旧版侧边栏与新版 UI 筛选

Refs #411
This commit is contained in:
Syngnat
2026-05-24 11:38:26 +08:00
parent 358d799af8
commit 85a0f9d007
5 changed files with 257 additions and 14 deletions

View File

@@ -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 (

View File

@@ -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', () => {

View File

@@ -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('.');

View File

@@ -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} />;

View File

@@ -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