From 322d0a7cb80fe49f83ad1bf58e2e3b28d7bb74b5 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 24 Jun 2026 22:26:39 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(sidebar):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20Oracle=20=E5=BA=8F=E5=88=97=E5=92=8C=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E5=8C=85=E5=AF=B9=E8=B1=A1=E6=A0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Oracle/Dameng 序列与存储包元数据加载 - 同步对象树节点、V2 筛选、搜索、复制和拖拽支持 - 补充多语言文案和侧边栏回归测试 --- .../Sidebar.locate-toolbar.test.tsx | 43 ++++- frontend/src/components/Sidebar.tsx | 8 +- .../SidebarCopyObjectName.i18n.test.ts | 9 +- .../SidebarObjectGroups.i18n.test.ts | 11 +- .../components/sidebar/SidebarTreeTitle.tsx | 2 + .../src/components/sidebar/sidebarHelpers.ts | 10 +- .../sidebar/sidebarMetadataLoaders.test.ts | 67 ++++++- .../sidebar/sidebarMetadataLoaders.ts | 165 ++++++++++++++++++ .../sidebar/useSidebarCommandSearchRunner.ts | 6 +- .../sidebar/useSidebarObjectActions.tsx | 2 + .../sidebar/useSidebarSearchModel.tsx | 9 +- .../sidebar/useSidebarTitleRender.tsx | 4 +- .../sidebar/useSidebarTreeLoaders.tsx | 70 +++++++- .../src/components/sidebarCoreUtils.test.ts | 10 ++ frontend/src/components/sidebarCoreUtils.ts | 6 +- frontend/src/components/sidebarV2Utils.ts | 20 ++- frontend/src/v2-theme.css | 12 +- shared/i18n/de-DE.json | 6 + shared/i18n/en-US.json | 6 + shared/i18n/ja-JP.json | 6 + shared/i18n/ru-RU.json | 6 + shared/i18n/zh-CN.json | 6 + shared/i18n/zh-TW.json | 6 + 23 files changed, 465 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index ae0b520..44f3f9d 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -642,7 +642,7 @@ describe('Sidebar locate toolbar', () => { const source = readSidebarSource(); const commandSearchRunSource = source.slice( source.indexOf("if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view')"), - source.indexOf("if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine')"), + source.indexOf("if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine' || node.type === 'sequence' || node.type === 'package')"), ); expect(commandSearchRunSource).toContain("tabId: String(node.key || '')"); @@ -824,12 +824,16 @@ describe('Sidebar locate toolbar', () => { expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.all'"); expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.tables'"); expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.views'"); + expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.sequences'"); expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.routines'"); + expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.packages'"); expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.events'"); expect(objectKindSource).not.toContain("label: '全部'"); expect(objectKindSource).not.toContain("label: '表'"); expect(objectKindSource).not.toContain("label: '视图'"); + expect(objectKindSource).not.toContain("label: '序列'"); expect(objectKindSource).not.toContain("label: '函数'"); + expect(objectKindSource).not.toContain("label: '存储包'"); expect(objectKindSource).not.toContain("label: '事件'"); expect(searchScopeSource).toContain("labelKey: 'sidebar.command_search.scope.smart'"); @@ -875,7 +879,9 @@ describe('Sidebar locate toolbar', () => { 'sidebar.command_search.object_kind.all', 'sidebar.command_search.object_kind.tables', 'sidebar.command_search.object_kind.views', + 'sidebar.command_search.object_kind.sequences', 'sidebar.command_search.object_kind.routines', + 'sidebar.command_search.object_kind.packages', 'sidebar.command_search.object_kind.events', 'sidebar.command_search.scope.smart', 'sidebar.command_search.scope.object', @@ -913,7 +919,9 @@ describe('Sidebar locate toolbar', () => { 'sidebar.command_search.object_kind.all': 'All', 'sidebar.command_search.object_kind.tables': 'Tables', 'sidebar.command_search.object_kind.views': 'Views', + 'sidebar.command_search.object_kind.sequences': 'Sequences', 'sidebar.command_search.object_kind.routines': 'Routines', + 'sidebar.command_search.object_kind.packages': 'Packages', 'sidebar.command_search.object_kind.events': 'Events', 'table_overview.section.pinned': 'Pinned', 'table_overview.section.all': 'All', @@ -927,8 +935,8 @@ describe('Sidebar locate toolbar', () => { t('sidebar.search.scope.host', undefined, 'zh-CN'), t('sidebar.search.scope.tag', undefined, 'zh-CN'), ]); - expect(buildV2ExplorerFilterOptions(translate).map((option) => option.label)).toEqual(['All', 'Tables', 'Views', 'Routines', 'Events']); - expect(V2_UTILS_EXPLORER_FILTER_OPTIONS.map((option) => option.label)).toEqual(['全部', '表', '视图', '函数', '事件']); + expect(buildV2ExplorerFilterOptions(translate).map((option) => option.label)).toEqual(['All', 'Tables', 'Views', 'Sequences', 'Routines', 'Packages', 'Events']); + expect(V2_UTILS_EXPLORER_FILTER_OPTIONS.map((option) => option.label)).toEqual(['全部', '表', '视图', '序列', '函数', '存储包', '事件']); const tableNodes = [ { title: 'orders', key: 'orders', type: 'table' as const, dataRef: { pinnedSidebarTable: true } }, @@ -994,6 +1002,13 @@ describe('Sidebar locate toolbar', () => { expect(css).not.toContain('.gn-v2-active-connection-trigger:hover'); }); + it('keeps v2 explorer filter tabs on a single line when Oracle object filters are present', () => { + const css = readV2ThemeCss(); + + expect(css).toMatch(/\.gn-v2-explorer-filter-tabs \{[^}]*flex-wrap: nowrap;[^}]*overflow-x: auto;[^}]*overflow-y: hidden;/s); + expect(css).toMatch(/\.gn-v2-explorer-filter-tabs button \{[^}]*flex: 0 0 auto;[^}]*white-space: nowrap;/s); + }); + it('keeps v2 tree status dots circular while using virtual horizontal scroll for long labels', () => { const css = readV2ThemeCss(); const source = readSidebarSource(); @@ -1370,6 +1385,13 @@ describe('Sidebar locate toolbar', () => { dataRef: { groupKey: 'views' }, children: [{ title: 'v_users', key: 'v_users', type: 'view' as const }], }, + { + title: '序列', + key: 'conn-main-sequences', + type: 'object-group' as const, + dataRef: { groupKey: 'sequences' }, + children: [{ title: 'seq_person_id', key: 'seq_person_id', type: 'sequence' as const }], + }, { title: '函数', key: 'conn-main-routines', @@ -1377,6 +1399,13 @@ describe('Sidebar locate toolbar', () => { dataRef: { groupKey: 'routines' }, children: [{ title: 'calc_total', key: 'calc_total', type: 'routine' as const }], }, + { + title: '存储包', + key: 'conn-main-packages', + type: 'object-group' as const, + dataRef: { groupKey: 'packages' }, + children: [{ title: 'pkg_person', key: 'pkg_person', type: 'package' as const }], + }, { title: '事件', key: 'conn-main-events', @@ -1391,12 +1420,16 @@ describe('Sidebar locate toolbar', () => { 'conn-main-queries', 'conn-main-tables', 'conn-main-views', + 'conn-main-sequences', 'conn-main-routines', + 'conn-main-packages', 'conn-main-events', ]); expect(filterV2ExplorerTreeByKind(tree, 'tables')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-tables']); expect(filterV2ExplorerTreeByKind(tree, 'views')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-views']); + expect(filterV2ExplorerTreeByKind(tree, 'sequences')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-sequences']); expect(filterV2ExplorerTreeByKind(tree, 'routines')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-routines']); + expect(filterV2ExplorerTreeByKind(tree, 'packages')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-packages']); expect(filterV2ExplorerTreeByKind(tree, 'events')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-events']); }); @@ -2609,7 +2642,9 @@ describe('Sidebar locate toolbar', () => { const objectGroupTitleCases = [ ['tables', 'sidebar.v2_table_group_menu.title', '表'], ['views', 'sidebar.object_group.views', '视图'], + ['sequences', 'sidebar.object_group.sequences', '序列'], ['routines', 'sidebar.object_group.routines', '函数'], + ['packages', 'sidebar.object_group.packages', '存储包'], ['triggers', 'sidebar.object_group.triggers', '触发器'], ['events', 'sidebar.object_group.events', '事件'], ['materializedViews', 'sidebar.object_group.materialized_views', '物化视图'], @@ -2684,7 +2719,9 @@ describe('Sidebar locate toolbar', () => { [ "if (groupKey === 'tables') return t('sidebar.v2_table_group_menu.title');", "if (groupKey === 'views') return t('sidebar.object_group.views');", + "if (groupKey === 'sequences') return t('sidebar.object_group.sequences');", "if (groupKey === 'routines') return t('sidebar.object_group.routines');", + "if (groupKey === 'packages') return t('sidebar.object_group.packages');", "if (groupKey === 'triggers') return t('sidebar.object_group.triggers');", "if (groupKey === 'events') return t('sidebar.object_group.events');", "if (groupKey === 'materializedViews') return t('sidebar.object_group.materialized_views');", diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 416e89f..4142c45 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -228,7 +228,9 @@ const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; labelKey: strin { key: 'all', labelKey: 'sidebar.command_search.object_kind.all' }, { key: 'tables', labelKey: 'sidebar.command_search.object_kind.tables' }, { key: 'views', labelKey: 'sidebar.command_search.object_kind.views' }, + { key: 'sequences', labelKey: 'sidebar.command_search.object_kind.sequences' }, { key: 'routines', labelKey: 'sidebar.command_search.object_kind.routines' }, + { key: 'packages', labelKey: 'sidebar.command_search.object_kind.packages' }, { key: 'events', labelKey: 'sidebar.command_search.object_kind.events' }, ]; @@ -1483,7 +1485,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 === 'db-event' || type === 'routine') { + } else if (type === 'view' || type === 'materialized-view' || type === 'sequence' || type === 'package' || 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 }); @@ -1545,7 +1547,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 === 'db-event' || type === 'routine') { + } else if (type === 'table' || type === 'view' || type === 'materialized-view' || type === 'sequence' || type === 'package' || 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 === 'redis-db') setActiveContext({ connectionId: dataRef.id, dbName: `db${dataRef.redisDB}` }); @@ -1633,6 +1635,8 @@ const Sidebar: React.FC<{ routineType }); return; + } else if (node.type === 'sequence' || node.type === 'package') { + return; } else if (node.type === 'jvm-mode') { const { providerMode, id } = node.dataRef; const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection; diff --git a/frontend/src/components/SidebarCopyObjectName.i18n.test.ts b/frontend/src/components/SidebarCopyObjectName.i18n.test.ts index 1fa8aeb..8d44f34 100644 --- a/frontend/src/components/SidebarCopyObjectName.i18n.test.ts +++ b/frontend/src/components/SidebarCopyObjectName.i18n.test.ts @@ -1,13 +1,18 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); +const source = [ + readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'), + readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8'), +].join('\n'); const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; const requiredKeys = [ 'sidebar.copy_object_name.label.table', 'sidebar.copy_object_name.label.view', 'sidebar.copy_object_name.label.materialized_view', + 'sidebar.copy_object_name.label.sequence', + 'sidebar.copy_object_name.label.package', 'sidebar.copy_object_name.label.event', 'sidebar.copy_object_name.empty', 'sidebar.copy_object_name.copied', @@ -27,6 +32,8 @@ describe('Sidebar copy object name i18n', () => { expect(source).not.toContain('`复制${label}失败: `'); expect(source).toContain("t('sidebar.copy_object_name.label.view')"); expect(source).toContain("t('sidebar.copy_object_name.label.materialized_view')"); + expect(source).toContain("t('sidebar.copy_object_name.label.sequence')"); + expect(source).toContain("t('sidebar.copy_object_name.label.package')"); expect(source).toContain("t('sidebar.copy_object_name.label.event')"); expect(source).toContain("t('sidebar.copy_object_name.label.table')"); expect(source).toContain("t('sidebar.copy_object_name.empty'"); diff --git a/frontend/src/components/SidebarObjectGroups.i18n.test.ts b/frontend/src/components/SidebarObjectGroups.i18n.test.ts index d1ac664..fe08d8c 100644 --- a/frontend/src/components/SidebarObjectGroups.i18n.test.ts +++ b/frontend/src/components/SidebarObjectGroups.i18n.test.ts @@ -1,7 +1,10 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); +const source = [ + readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'), + readFileSync(new URL('./sidebar/useSidebarTreeLoaders.tsx', import.meta.url), 'utf8'), +].join('\n'); const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; const requiredKeys = [ @@ -9,7 +12,9 @@ const requiredKeys = [ 'sidebar.object_group.tables', 'sidebar.object_group.views', 'sidebar.object_group.materialized_views', + 'sidebar.object_group.sequences', 'sidebar.object_group.routines', + 'sidebar.object_group.packages', 'sidebar.object_group.triggers', 'sidebar.object_group.events', ]; @@ -21,13 +26,17 @@ describe('Sidebar object group i18n', () => { "buildObjectGroup(schemaNodeKey, 'tables', '表'", "buildObjectGroup(schemaNodeKey, 'views', '视图'", "buildObjectGroup(schemaNodeKey, 'materializedViews', '物化视图'", + "buildObjectGroup(schemaNodeKey, 'sequences', '序列'", "buildObjectGroup(schemaNodeKey, 'routines', '函数'", + "buildObjectGroup(schemaNodeKey, 'packages', '存储包'", "buildObjectGroup(schemaNodeKey, 'triggers', '触发器'", "buildObjectGroup(schemaNodeKey, 'events', '事件'", "buildObjectGroup(key as string, 'tables', '表'", "buildObjectGroup(key as string, 'views', '视图'", "buildObjectGroup(key as string, 'materializedViews', '物化视图'", + "buildObjectGroup(key as string, 'sequences', '序列'", "buildObjectGroup(key as string, 'routines', '函数'", + "buildObjectGroup(key as string, 'packages', '存储包'", "buildObjectGroup(key as string, 'triggers', '触发器'", "buildObjectGroup(key as string, 'events', '事件'", ].forEach((snippet) => { diff --git a/frontend/src/components/sidebar/SidebarTreeTitle.tsx b/frontend/src/components/sidebar/SidebarTreeTitle.tsx index 3db6508..b4cf3a3 100644 --- a/frontend/src/components/sidebar/SidebarTreeTitle.tsx +++ b/frontend/src/components/sidebar/SidebarTreeTitle.tsx @@ -55,9 +55,11 @@ export const renderSidebarV2TreeTitle = ({ const isMono = node.type === 'table' || node.type === 'view' || node.type === 'materialized-view' + || node.type === 'sequence' || node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine' + || node.type === 'package' || node.type === 'saved-query' || node.type === 'external-sql-file'; const titleClassName = [ diff --git a/frontend/src/components/sidebar/sidebarHelpers.ts b/frontend/src/components/sidebar/sidebarHelpers.ts index a5ba099..69d40a3 100644 --- a/frontend/src/components/sidebar/sidebarHelpers.ts +++ b/frontend/src/components/sidebar/sidebarHelpers.ts @@ -18,7 +18,7 @@ export const V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID = '__gonavi-v2-ungrouped-conn // === 共享类型 === /** V2 资源管理器过滤维度 */ -export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines' | 'events'; +export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'sequences' | 'routines' | 'packages' | 'events'; // === 纯函数 === @@ -94,9 +94,11 @@ export const isV2SidebarObjectNode = ( return node?.type === 'table' || node?.type === 'view' || node?.type === 'materialized-view' + || node?.type === 'sequence' || node?.type === 'db-trigger' || node?.type === 'db-event' - || node?.type === 'routine'; + || node?.type === 'routine' + || node?.type === 'package'; }; // === 第二期:依赖 i18n 但不依赖 TreeNode 内部类型的工具函数 === @@ -125,7 +127,9 @@ export const resolveV2ObjectGroupTitle = ( const groupKey = String(node?.dataRef?.groupKey || ''); if (groupKey === 'tables') return t('sidebar.v2_table_group_menu.title'); if (groupKey === 'views') return t('sidebar.object_group.views'); + if (groupKey === 'sequences') return t('sidebar.object_group.sequences'); if (groupKey === 'routines') return t('sidebar.object_group.routines'); + if (groupKey === 'packages') return t('sidebar.object_group.packages'); if (groupKey === 'triggers') return t('sidebar.object_group.triggers'); if (groupKey === 'events') return t('sidebar.object_group.events'); if (groupKey === 'materializedViews') return t('sidebar.object_group.materialized_views'); @@ -139,7 +143,7 @@ export const resolveV2ObjectGroupTitle = ( export const resolveSidebarTableNameForCopy = ( node: Pick | null | undefined, ): string => { - return String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.eventName || node?.title || '').trim(); + return String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.sequenceName || node?.dataRef?.packageName || node?.dataRef?.eventName || node?.title || '').trim(); }; // === 命令搜索相关类型与解析(V2 Command Search)=== diff --git a/frontend/src/components/sidebar/sidebarMetadataLoaders.test.ts b/frontend/src/components/sidebar/sidebarMetadataLoaders.test.ts index 4615461..6292d56 100644 --- a/frontend/src/components/sidebar/sidebarMetadataLoaders.test.ts +++ b/frontend/src/components/sidebar/sidebarMetadataLoaders.test.ts @@ -5,7 +5,14 @@ vi.mock("../../../wailsjs/go/app/App", () => ({ })); import { DBQuery } from "../../../wailsjs/go/app/App"; -import { buildSchemasMetadataQuerySpecs, loadViews } from "./sidebarMetadataLoaders"; +import { + buildPackagesMetadataQuerySpecs, + buildSchemasMetadataQuerySpecs, + buildSequencesMetadataQuerySpecs, + loadPackages, + loadSequences, + loadViews, +} from "./sidebarMetadataLoaders"; const mockedDBQuery = vi.mocked(DBQuery); @@ -67,3 +74,61 @@ describe("buildSchemasMetadataQuerySpecs", () => { ]); }); }); + +describe("Oracle object metadata loaders", () => { + it("builds owner-scoped sequence and package queries for Oracle", () => { + expect(buildSequencesMetadataQuerySpecs("oracle", "MYCIMLED").map((spec) => spec.sql)).toEqual([ + "SELECT SEQUENCE_OWNER AS schema_name, SEQUENCE_NAME AS sequence_name FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = 'MYCIMLED' ORDER BY SEQUENCE_NAME", + ]); + expect(buildPackagesMetadataQuerySpecs("oracle", "MYCIMLED").map((spec) => spec.sql)).toEqual([ + "SELECT OWNER AS schema_name, OBJECT_NAME AS package_name FROM ALL_OBJECTS WHERE OWNER = 'MYCIMLED' AND OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME", + ]); + }); + + it("loads and deduplicates Oracle sequences and packages", async () => { + mockedDBQuery.mockImplementation(async (_config: unknown, _dbName: string, sql: string) => { + if (sql.includes("ALL_SEQUENCES")) { + return { + success: true, + message: "", + data: [ + { schema_name: "MYCIMLED", sequence_name: "SEQ_PERSON_ID" }, + { SEQUENCE_OWNER: "MYCIMLED", SEQUENCE_NAME: "SEQ_PERSON_ID" }, + ], + }; + } + if (sql.includes("ALL_OBJECTS") && sql.includes("PACKAGE")) { + return { + success: true, + message: "", + data: [ + { schema_name: "MYCIMLED", package_name: "PKG_PERSON" }, + { OWNER: "MYCIMLED", OBJECT_NAME: "PKG_PERSON" }, + ], + }; + } + return { success: false, message: "", data: [] }; + }); + + await expect(loadSequences({ config: { type: "oracle" } }, "MYCIMLED")).resolves.toEqual({ + supported: true, + sequences: [ + { + displayName: "MYCIMLED.SEQ_PERSON_ID", + schemaName: "MYCIMLED", + sequenceName: "MYCIMLED.SEQ_PERSON_ID", + }, + ], + }); + await expect(loadPackages({ config: { type: "oracle" } }, "MYCIMLED")).resolves.toEqual({ + supported: true, + packages: [ + { + displayName: "MYCIMLED.PKG_PERSON", + packageName: "MYCIMLED.PKG_PERSON", + schemaName: "MYCIMLED", + }, + ], + }); + }); +}); diff --git a/frontend/src/components/sidebar/sidebarMetadataLoaders.ts b/frontend/src/components/sidebar/sidebarMetadataLoaders.ts index 3f53ce7..a503fe4 100644 --- a/frontend/src/components/sidebar/sidebarMetadataLoaders.ts +++ b/frontend/src/components/sidebar/sidebarMetadataLoaders.ts @@ -604,6 +604,46 @@ const buildFunctionsMetadataQuerySpecs = ( } }; +const buildSequencesMetadataQuerySpecs = ( + dialect: string, + dbName: string, +): MetadataQuerySpec[] => { + const safeDbName = escapeSQLLiteral(dbName); + switch (dialect) { + case "oracle": + case "dm": + return normalizeMetadataQuerySpecs([ + { + sql: safeDbName + ? `SELECT SEQUENCE_OWNER AS schema_name, SEQUENCE_NAME AS sequence_name FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '${safeDbName.toUpperCase()}' ORDER BY SEQUENCE_NAME` + : `SELECT SEQUENCE_NAME AS sequence_name FROM USER_SEQUENCES ORDER BY SEQUENCE_NAME`, + }, + ]); + default: + return []; + } +}; + +const buildPackagesMetadataQuerySpecs = ( + dialect: string, + dbName: string, +): MetadataQuerySpec[] => { + const safeDbName = escapeSQLLiteral(dbName); + switch (dialect) { + case "oracle": + case "dm": + return normalizeMetadataQuerySpecs([ + { + sql: safeDbName + ? `SELECT OWNER AS schema_name, OBJECT_NAME AS package_name FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME` + : `SELECT OBJECT_NAME AS package_name FROM USER_OBJECTS WHERE OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME`, + }, + ]); + default: + return []; + } +}; + const buildEventsMetadataQuerySpecs = ( dialect: string, dbName: string, @@ -977,6 +1017,127 @@ const loadFunctions = async ( return { routines, supported: hasSuccessfulQuery }; }; +const loadSequences = async ( + conn: any, + dbName: string, +): Promise<{ + sequences: Array<{ + displayName: string; + sequenceName: string; + schemaName: string; + }>; + supported: boolean; +}> => { + const dialect = getMetadataDialect(conn as SavedConnection); + const querySpecs = buildSequencesMetadataQuerySpecs(dialect, dbName); + const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs( + conn, + dbName, + querySpecs, + ); + const seen = new Set(); + const sequences: Array<{ + displayName: string; + sequenceName: string; + schemaName: string; + }> = []; + + results.forEach((queryResult) => { + queryResult.rows.forEach((row) => { + const rawSequenceName = + getCaseInsensitiveValue(row, [ + "sequence_name", + "sequencename", + "object_name", + "name", + ]) || getFirstRowValue(row); + if (!rawSequenceName) return; + + const sequenceParts = splitQualifiedName(rawSequenceName); + const schemaName = ( + getCaseInsensitiveValue(row, [ + "schema_name", + "sequence_owner", + "owner", + ]) || + sequenceParts.schemaName || + "" + ).trim(); + const objectName = (sequenceParts.objectName || rawSequenceName).trim(); + const fullName = buildQualifiedName(schemaName, objectName); + const uniqueKey = `${schemaName.toLowerCase()}@@${objectName.toLowerCase()}`; + if (!fullName || seen.has(uniqueKey)) return; + seen.add(uniqueKey); + sequences.push({ + displayName: fullName, + sequenceName: fullName, + schemaName, + }); + }); + }); + return { sequences, supported: hasSuccessfulQuery }; +}; + +const loadPackages = async ( + conn: any, + dbName: string, +): Promise<{ + packages: Array<{ + displayName: string; + packageName: string; + schemaName: string; + }>; + supported: boolean; +}> => { + const dialect = getMetadataDialect(conn as SavedConnection); + const querySpecs = buildPackagesMetadataQuerySpecs(dialect, dbName); + const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs( + conn, + dbName, + querySpecs, + ); + const seen = new Set(); + const packages: Array<{ + displayName: string; + packageName: string; + schemaName: string; + }> = []; + + results.forEach((queryResult) => { + queryResult.rows.forEach((row) => { + const rawPackageName = + getCaseInsensitiveValue(row, [ + "package_name", + "packagename", + "object_name", + "name", + ]) || getFirstRowValue(row); + if (!rawPackageName) return; + + const packageParts = splitQualifiedName(rawPackageName); + const schemaName = ( + getCaseInsensitiveValue(row, [ + "schema_name", + "owner", + ]) || + packageParts.schemaName || + "" + ).trim(); + const objectName = (packageParts.objectName || rawPackageName).trim(); + const fullName = buildQualifiedName(schemaName, objectName); + const uniqueKey = `${schemaName.toLowerCase()}@@${objectName.toLowerCase()}`; + if (!fullName || seen.has(uniqueKey)) return; + seen.add(uniqueKey); + packages.push({ + displayName: fullName, + packageName: fullName, + schemaName, + }); + }); + }); + return { packages, supported: hasSuccessfulQuery }; +}; + const loadDatabaseEvents = async ( conn: any, dbName: string, @@ -1084,8 +1245,10 @@ export { buildDuckDBMacroDDL, buildEventsMetadataQuerySpecs, buildFunctionsMetadataQuerySpecs, + buildPackagesMetadataQuerySpecs, buildQualifiedName, buildSchemasMetadataQuerySpecs, + buildSequencesMetadataQuerySpecs, buildSidebarObjectKeyName, buildSidebarTableStatusSQL, buildTriggersMetadataQuerySpecs, @@ -1102,7 +1265,9 @@ export { loadDatabaseEvents, loadDatabaseTriggers, loadFunctions, + loadPackages, loadSchemas, + loadSequences, loadStarRocksMaterializedViews, loadViews, normalizeMetadataQuerySpecs, diff --git a/frontend/src/components/sidebar/useSidebarCommandSearchRunner.ts b/frontend/src/components/sidebar/useSidebarCommandSearchRunner.ts index 66cf16e..aa5f6ee 100644 --- a/frontend/src/components/sidebar/useSidebarCommandSearchRunner.ts +++ b/frontend/src/components/sidebar/useSidebarCommandSearchRunner.ts @@ -105,12 +105,14 @@ export const useSidebarCommandSearchRunner = ({ onDoubleClick(null, node); return; } - if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine') { + if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine' || node.type === 'sequence' || node.type === 'package') { setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName }); setSelectedKeys([node.key]); selectedNodesRef.current = [node]; scrollSidebarTreeToKey(node.key); - onDoubleClick(null, node); + if (node.type !== 'sequence' && node.type !== 'package') { + onDoubleClick(null, node); + } } }, [ activeContext, diff --git a/frontend/src/components/sidebar/useSidebarObjectActions.tsx b/frontend/src/components/sidebar/useSidebarObjectActions.tsx index 68f9614..a8b3928 100644 --- a/frontend/src/components/sidebar/useSidebarObjectActions.tsx +++ b/frontend/src/components/sidebar/useSidebarObjectActions.tsx @@ -108,6 +108,8 @@ type UseSidebarObjectActionsArgs = { const resolveCopyObjectNameLabel = (node: any): string => { if (node?.type === 'view') return t('sidebar.copy_object_name.label.view'); if (node?.type === 'materialized-view') return t('sidebar.copy_object_name.label.materialized_view'); + if (node?.type === 'sequence') return t('sidebar.copy_object_name.label.sequence'); + if (node?.type === 'package') return t('sidebar.copy_object_name.label.package'); if (node?.type === 'db-event') return t('sidebar.copy_object_name.label.event'); return t('sidebar.copy_object_name.label.table'); }; diff --git a/frontend/src/components/sidebar/useSidebarSearchModel.tsx b/frontend/src/components/sidebar/useSidebarSearchModel.tsx index 427ad4e..bdbb9c6 100644 --- a/frontend/src/components/sidebar/useSidebarSearchModel.tsx +++ b/frontend/src/components/sidebar/useSidebarSearchModel.tsx @@ -9,6 +9,7 @@ import { DatabaseOutlined, EyeOutlined, FilterOutlined, + KeyOutlined, PlusOutlined, RobotOutlined, TableOutlined, @@ -398,12 +399,14 @@ export const useSidebarSearchModel = ({ node.type === 'table' || node.type === 'view' || node.type === 'materialized-view' + || node.type === 'sequence' || node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine' + || node.type === 'package' ) { const conn = connectionById.get(String(dataRef.id || '')); - const objectName = String(dataRef.tableName || dataRef.viewName || dataRef.triggerName || dataRef.eventName || dataRef.routineName || node.title || '').trim(); + const objectName = String(dataRef.tableName || dataRef.viewName || dataRef.sequenceName || dataRef.triggerName || dataRef.eventName || dataRef.routineName || dataRef.packageName || node.title || '').trim(); const displayName = String(node.title || extractObjectName(objectName) || objectName).trim(); result.push({ key: `node-${node.key}`, @@ -412,7 +415,9 @@ export const useSidebarSearchModel = ({ meta: [conn?.name || dataRef.id, dataRef.dbName].filter(Boolean).join(' · '), icon: node.type === 'table' ? - : (node.type === 'db-event' ? : (node.type === 'routine' ? : )), + : (node.type === 'sequence' + ? + : (node.type === 'db-event' ? : ((node.type === 'routine' || node.type === 'package') ? : ))), node, }); } diff --git a/frontend/src/components/sidebar/useSidebarTitleRender.tsx b/frontend/src/components/sidebar/useSidebarTitleRender.tsx index 071096c..f2b40c8 100644 --- a/frontend/src/components/sidebar/useSidebarTitleRender.tsx +++ b/frontend/src/components/sidebar/useSidebarTitleRender.tsx @@ -51,8 +51,8 @@ export const useSidebarTitleRender = ({ const displayTitle = String(node.title ?? ''); const dragText = resolveSidebarObjectDragText(node); let hoverTitle = displayTitle; - 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(); + if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view' || node.type === 'sequence' || node.type === 'package' || node.type === 'db-event') { + const rawTableName = String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.sequenceName || node?.dataRef?.packageName || node?.dataRef?.eventName || '').trim(); const conn = node?.dataRef as SavedConnection | undefined; if (rawTableName && shouldHideSchemaPrefix(conn)) { if (splitQualifiedName(rawTableName).schemaName) { diff --git a/frontend/src/components/sidebar/useSidebarTreeLoaders.tsx b/frontend/src/components/sidebar/useSidebarTreeLoaders.tsx index 72b0afa..0fe1ecc 100644 --- a/frontend/src/components/sidebar/useSidebarTreeLoaders.tsx +++ b/frontend/src/components/sidebar/useSidebarTreeLoaders.tsx @@ -10,6 +10,7 @@ import { FolderOpenOutlined, FunctionOutlined, HddOutlined, + KeyOutlined, TableOutlined, ThunderboltOutlined, } from '@ant-design/icons'; @@ -32,7 +33,9 @@ import { loadDatabaseEvents, loadDatabaseTriggers, loadFunctions, + loadPackages, loadSchemas, + loadSequences, loadStarRocksMaterializedViews, loadViews, parseMetadataRowCount, @@ -505,18 +508,22 @@ export const useSidebarTreeLoaders = ({ }; }); - const [schemasResult, viewsResult, materializedViewsResult, triggersResult, routinesResult, eventsResult] = await Promise.all([ + 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 : []; @@ -578,6 +585,24 @@ export const useSidebarTreeLoaders = ({ }; }); + 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(), @@ -627,6 +652,10 @@ export const useSidebarTreeLoaders = ({ // 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: { tableName: string; schemaName: string; displayName: string; rowCount?: number }): TreeNode => { @@ -695,6 +724,30 @@ export const useSidebarTreeLoaders = ({ 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: , + 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: , + 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}`, @@ -735,6 +788,8 @@ export const useSidebarTreeLoaders = ({ views: TreeNode[]; materializedViews: TreeNode[]; routines: TreeNode[]; + sequences: TreeNode[]; + packages: TreeNode[]; triggers: TreeNode[]; events: TreeNode[]; }; @@ -751,6 +806,8 @@ export const useSidebarTreeLoaders = ({ views: [], materializedViews: [], routines: [], + sequences: [], + packages: [], triggers: [], events: [], }; @@ -764,12 +821,15 @@ export const useSidebarTreeLoaders = ({ 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 includeEvents = supportsDatabaseEvents(conn as SavedConnection); const schemaNodes: TreeNode[] = Array.from(schemaMap.values()) @@ -787,7 +847,9 @@ export const useSidebarTreeLoaders = ({ buildObjectGroup(schemaNodeKey, 'tables', t('sidebar.object_group.tables'), , bucket.tables, { schemaName: bucket.schemaName }), buildObjectGroup(schemaNodeKey, 'views', t('sidebar.object_group.views'), , bucket.views, { schemaName: bucket.schemaName }), ...(includeMaterializedViews ? [buildObjectGroup(schemaNodeKey, 'materializedViews', t('sidebar.object_group.materialized_views'), , bucket.materializedViews, { schemaName: bucket.schemaName })] : []), + ...(includeOracleObjects ? [buildObjectGroup(schemaNodeKey, 'sequences', t('sidebar.object_group.sequences'), , bucket.sequences, { schemaName: bucket.schemaName })] : []), buildObjectGroup(schemaNodeKey, 'routines', t('sidebar.object_group.routines'), , bucket.routines, { schemaName: bucket.schemaName }), + ...(includeOracleObjects ? [buildObjectGroup(schemaNodeKey, 'packages', t('sidebar.object_group.packages'), , bucket.packages, { schemaName: bucket.schemaName })] : []), buildObjectGroup(schemaNodeKey, 'triggers', t('sidebar.object_group.triggers'), , bucket.triggers, { schemaName: bucket.schemaName }), ...(includeEvents ? [buildObjectGroup(schemaNodeKey, 'events', t('sidebar.object_group.events'), , bucket.events, { schemaName: bucket.schemaName })] : []), ]; @@ -805,13 +867,17 @@ export const useSidebarTreeLoaders = ({ replaceTreeNodeChildren(key, [queriesNode, ...schemaNodes]); } else { - const includeMaterializedViews = getMetadataDialect(conn as SavedConnection) === 'starrocks'; + const dialect = getMetadataDialect(conn as SavedConnection); + const includeMaterializedViews = dialect === 'starrocks'; + const includeOracleObjects = dialect === 'oracle' || dialect === 'dm'; const includeEvents = supportsDatabaseEvents(conn as SavedConnection); const groupedNodes: TreeNode[] = [ buildObjectGroup(key as string, 'tables', t('sidebar.object_group.tables'), , sortedTableEntries.map(buildTableNode)), buildObjectGroup(key as string, 'views', t('sidebar.object_group.views'), , viewEntries.map(buildViewNode)), ...(includeMaterializedViews ? [buildObjectGroup(key as string, 'materializedViews', t('sidebar.object_group.materialized_views'), , materializedViewEntries.map(buildMaterializedViewNode))] : []), + ...(includeOracleObjects ? [buildObjectGroup(key as string, 'sequences', t('sidebar.object_group.sequences'), , sequenceEntries.map(buildSequenceNode))] : []), buildObjectGroup(key as string, 'routines', t('sidebar.object_group.routines'), , routineEntries.map(buildRoutineNode)), + ...(includeOracleObjects ? [buildObjectGroup(key as string, 'packages', t('sidebar.object_group.packages'), , packageEntries.map(buildPackageNode))] : []), buildObjectGroup(key as string, 'triggers', t('sidebar.object_group.triggers'), , triggerEntries.map(buildTriggerNode)), ...(includeEvents ? [buildObjectGroup(key as string, 'events', t('sidebar.object_group.events'), , eventEntries.map(buildEventNode))] : []), ]; diff --git a/frontend/src/components/sidebarCoreUtils.test.ts b/frontend/src/components/sidebarCoreUtils.test.ts index 3a7106e..22ec63c 100644 --- a/frontend/src/components/sidebarCoreUtils.test.ts +++ b/frontend/src/components/sidebarCoreUtils.test.ts @@ -59,5 +59,15 @@ describe('sidebarCoreUtils', () => { title: 'trg_users_audit', dataRef: {}, })).toBe('trg_users_audit'); + expect(resolveSidebarObjectDragText({ + type: 'sequence', + title: 'SEQ_PERSON_ID', + dataRef: { sequenceName: 'MYCIMLED.SEQ_PERSON_ID' }, + })).toBe('MYCIMLED.SEQ_PERSON_ID'); + expect(resolveSidebarObjectDragText({ + type: 'package', + title: 'PKG_PERSON', + dataRef: { packageName: 'MYCIMLED.PKG_PERSON' }, + })).toBe('MYCIMLED.PKG_PERSON'); }); }); diff --git a/frontend/src/components/sidebarCoreUtils.ts b/frontend/src/components/sidebarCoreUtils.ts index 36dd729..b19ffa7 100644 --- a/frontend/src/components/sidebarCoreUtils.ts +++ b/frontend/src/components/sidebarCoreUtils.ts @@ -58,9 +58,11 @@ export const isV2SidebarObjectNode = (node: Pick return node?.type === 'table' || node?.type === 'view' || node?.type === 'materialized-view' + || node?.type === 'sequence' || node?.type === 'db-trigger' || node?.type === 'db-event' - || node?.type === 'routine'; + || node?.type === 'routine' + || node?.type === 'package'; }; export const resolveSidebarObjectDragText = ( @@ -69,8 +71,10 @@ export const resolveSidebarObjectDragText = ( const dataRef = node?.dataRef || {}; if (node?.type === 'table') return String(dataRef.tableName || node?.title || '').trim(); if (node?.type === 'view' || node?.type === 'materialized-view') return String(dataRef.viewName || dataRef.tableName || node?.title || '').trim(); + if (node?.type === 'sequence') return String(dataRef.sequenceName || node?.title || '').trim(); if (node?.type === 'db-trigger') return String(dataRef.triggerName || node?.title || '').trim(); if (node?.type === 'routine') return String(dataRef.routineName || node?.title || '').trim(); + if (node?.type === 'package') return String(dataRef.packageName || node?.title || '').trim(); if (node?.type === 'db-event') return String(dataRef.eventName || node?.title || '').trim(); return ''; }; diff --git a/frontend/src/components/sidebarV2Utils.ts b/frontend/src/components/sidebarV2Utils.ts index 999f460..7fa2a18 100644 --- a/frontend/src/components/sidebarV2Utils.ts +++ b/frontend/src/components/sidebarV2Utils.ts @@ -24,6 +24,8 @@ export type SidebarTreeNodeType = | 'db-trigger' | 'db-event' | 'routine' + | 'sequence' + | 'package' | 'object-group' | 'v2-table-section' | 'queries-folder' @@ -76,7 +78,7 @@ export const shouldLoadSidebarNodeOnExpand = ( export const resolveSidebarTableNameForCopy = ( node: Pick | null | undefined, ): string => { - return String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.eventName || node?.title || '').trim(); + return String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.sequenceName || node?.dataRef?.packageName || node?.dataRef?.eventName || node?.title || '').trim(); }; type SidebarTableSortPreference = 'name' | 'frequency'; @@ -294,7 +296,7 @@ export const getV2RailConnectionGroupBadgeText = (name: unknown, fallback = t('c return trimmed.slice(0, 2); }; -export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines' | 'events'; +export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'sequences' | 'routines' | 'packages' | 'events'; export const buildV2ExplorerFilterOptions = ( translate: SidebarV2Translate = translateSidebarV2Current, @@ -302,7 +304,9 @@ export const buildV2ExplorerFilterOptions = ( { key: 'all', label: translate('sidebar.command_search.object_kind.all') }, { key: 'tables', label: translate('sidebar.command_search.object_kind.tables') }, { key: 'views', label: translate('sidebar.command_search.object_kind.views') }, + { key: 'sequences', label: translate('sidebar.command_search.object_kind.sequences') }, { key: 'routines', label: translate('sidebar.command_search.object_kind.routines') }, + { key: 'packages', label: translate('sidebar.command_search.object_kind.packages') }, { key: 'events', label: translate('sidebar.command_search.object_kind.events') }, ]; @@ -311,7 +315,9 @@ export const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; label: s const V2_EXPLORER_FILTER_GROUP_KEYS: Record, string[]> = { tables: ['tables'], views: ['views', 'materializedViews'], + sequences: ['sequences'], routines: ['routines'], + packages: ['packages'], events: ['events'], }; @@ -365,7 +371,9 @@ export const filterV2ExplorerTreeByKind = ( const objectTypeMatches = (node: SidebarTreeNode): boolean => { if (filter === 'tables') return node.type === 'table'; if (filter === 'views') return node.type === 'view' || node.type === 'materialized-view'; + if (filter === 'sequences') return node.type === 'sequence'; if (filter === 'routines') return node.type === 'routine'; + if (filter === 'packages') return node.type === 'package'; if (filter === 'events') return node.type === 'db-event'; return false; }; @@ -483,7 +491,9 @@ export const parseV2CommandSearchQuery = (value: unknown): V2CommandSearchQuery const isV2CommandSearchObjectNode = (node: SidebarTreeNode): boolean => { return node.type === 'table' || node.type === 'view' - || node.type === 'materialized-view'; + || node.type === 'materialized-view' + || node.type === 'sequence' + || node.type === 'package'; }; export const V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT = 24; @@ -499,7 +509,7 @@ export const buildV2CommandSearchTreeIndex = ( const dataRef = item.node.dataRef || {}; const normalizedTitle = String(item.title || '').toLowerCase(); const normalizedPrimaryObjectText = String( - dataRef.tableName || dataRef.viewName || item.title || '', + dataRef.tableName || dataRef.viewName || dataRef.sequenceName || dataRef.packageName || item.title || '', ).toLowerCase(); return [{ @@ -509,6 +519,8 @@ export const buildV2CommandSearchTreeIndex = ( item.meta, dataRef.tableName, dataRef.viewName, + dataRef.sequenceName, + dataRef.packageName, dataRef.dbName, dataRef.name, dataRef.config?.host, diff --git a/frontend/src/v2-theme.css b/frontend/src/v2-theme.css index f61e684..73b6ffa 100644 --- a/frontend/src/v2-theme.css +++ b/frontend/src/v2-theme.css @@ -2511,19 +2511,29 @@ body[data-ui-version="v2"] .gn-v2-search-shortcut kbd { body[data-ui-version="v2"] .gn-v2-explorer-filter-tabs { display: flex; + flex-wrap: nowrap; gap: 4px; + overflow-x: auto; + overflow-y: hidden; padding: 2px 10px 8px; + scrollbar-width: none; +} + +body[data-ui-version="v2"] .gn-v2-explorer-filter-tabs::-webkit-scrollbar { + display: none; } body[data-ui-version="v2"] .gn-v2-explorer-filter-tabs button { - flex: 1; + flex: 0 0 auto; height: 22px; + padding: 0 6px; border: none; border-radius: 5px; background: transparent; color: var(--gn-fg-4); font-size: 11px; font-weight: 600; + white-space: nowrap; } body[data-ui-version="v2"] .gn-v2-explorer-filter-tabs button.is-active { diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 1d34df4..079f0c0 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -6612,7 +6612,9 @@ "sidebar.command_search.object_kind.all": "Alle", "sidebar.command_search.object_kind.events": "Ereignisse", "sidebar.command_search.object_kind.filter_aria": "Objektfilter", + "sidebar.command_search.object_kind.packages": "Pakete", "sidebar.command_search.object_kind.routines": "Routinen", + "sidebar.command_search.object_kind.sequences": "Sequenzen", "sidebar.command_search.object_kind.tables": "Tabellen", "sidebar.command_search.object_kind.views": "Sichten", "sidebar.command_search.placeholder": "Tabellen, Verbindungen, Aktionen suchen... oder KI fragen", @@ -6644,6 +6646,8 @@ "sidebar.copy_object_name.failed": "{{label}} konnte nicht kopiert werden: {{error}}", "sidebar.copy_object_name.label.event": "Ereignisname", "sidebar.copy_object_name.label.materialized_view": "Name der materialisierten Ansicht", + "sidebar.copy_object_name.label.package": "Paketname", + "sidebar.copy_object_name.label.sequence": "Sequenzname", "sidebar.copy_object_name.label.table": "Tabellenname", "sidebar.copy_object_name.label.view": "Ansichtsname", "sidebar.error.unknown": "Unbekannter Fehler", @@ -6950,7 +6954,9 @@ "sidebar.modal.tag.edit_title": "Gruppe bearbeiten", "sidebar.object_group.events": "Ereignisse", "sidebar.object_group.materialized_views": "Materialisierte Ansichten", + "sidebar.object_group.packages": "Pakete", "sidebar.object_group.routines": "Funktionen und Prozeduren", + "sidebar.object_group.sequences": "Sequenzen", "sidebar.object_group.tables": "Tabellen", "sidebar.object_group.triggers": "Trigger", "sidebar.object_group.views": "Ansichten", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index 0a53930..f487f94 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -6612,7 +6612,9 @@ "sidebar.command_search.object_kind.all": "All", "sidebar.command_search.object_kind.events": "Events", "sidebar.command_search.object_kind.filter_aria": "Object filters", + "sidebar.command_search.object_kind.packages": "Packages", "sidebar.command_search.object_kind.routines": "Routines", + "sidebar.command_search.object_kind.sequences": "Sequences", "sidebar.command_search.object_kind.tables": "Tables", "sidebar.command_search.object_kind.views": "Views", "sidebar.command_search.placeholder": "Search tables, connections, actions... or ask AI", @@ -6644,6 +6646,8 @@ "sidebar.copy_object_name.failed": "Failed to copy {{label}}: {{error}}", "sidebar.copy_object_name.label.event": "Event name", "sidebar.copy_object_name.label.materialized_view": "Materialized view name", + "sidebar.copy_object_name.label.package": "Package name", + "sidebar.copy_object_name.label.sequence": "Sequence name", "sidebar.copy_object_name.label.table": "Table name", "sidebar.copy_object_name.label.view": "View name", "sidebar.error.unknown": "Unknown error", @@ -6950,7 +6954,9 @@ "sidebar.modal.tag.edit_title": "Edit group", "sidebar.object_group.events": "Events", "sidebar.object_group.materialized_views": "Materialized views", + "sidebar.object_group.packages": "Packages", "sidebar.object_group.routines": "Functions and procedures", + "sidebar.object_group.sequences": "Sequences", "sidebar.object_group.tables": "Tables", "sidebar.object_group.triggers": "Triggers", "sidebar.object_group.views": "Views", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index 2d51f0b..a9ef62d 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -6612,7 +6612,9 @@ "sidebar.command_search.object_kind.all": "すべて", "sidebar.command_search.object_kind.events": "イベント", "sidebar.command_search.object_kind.filter_aria": "オブジェクトフィルター", + "sidebar.command_search.object_kind.packages": "パッケージ", "sidebar.command_search.object_kind.routines": "ルーチン", + "sidebar.command_search.object_kind.sequences": "シーケンス", "sidebar.command_search.object_kind.tables": "テーブル", "sidebar.command_search.object_kind.views": "ビュー", "sidebar.command_search.placeholder": "テーブル、接続、操作を検索... または AI に質問", @@ -6644,6 +6646,8 @@ "sidebar.copy_object_name.failed": "{{label}}のコピーに失敗しました: {{error}}", "sidebar.copy_object_name.label.event": "イベント名", "sidebar.copy_object_name.label.materialized_view": "マテリアライズドビュー名", + "sidebar.copy_object_name.label.package": "パッケージ名", + "sidebar.copy_object_name.label.sequence": "シーケンス名", "sidebar.copy_object_name.label.table": "テーブル名", "sidebar.copy_object_name.label.view": "ビュー名", "sidebar.error.unknown": "不明なエラー", @@ -6950,7 +6954,9 @@ "sidebar.modal.tag.edit_title": "グループを編集", "sidebar.object_group.events": "イベント", "sidebar.object_group.materialized_views": "マテリアライズドビュー", + "sidebar.object_group.packages": "パッケージ", "sidebar.object_group.routines": "関数とプロシージャ", + "sidebar.object_group.sequences": "シーケンス", "sidebar.object_group.tables": "テーブル", "sidebar.object_group.triggers": "トリガー", "sidebar.object_group.views": "ビュー", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index 3e2ef0e..c190d6f 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -6612,7 +6612,9 @@ "sidebar.command_search.object_kind.all": "Все", "sidebar.command_search.object_kind.events": "События", "sidebar.command_search.object_kind.filter_aria": "Фильтр объектов", + "sidebar.command_search.object_kind.packages": "Пакеты", "sidebar.command_search.object_kind.routines": "Подпрограммы", + "sidebar.command_search.object_kind.sequences": "Последовательности", "sidebar.command_search.object_kind.tables": "Таблицы", "sidebar.command_search.object_kind.views": "Представления", "sidebar.command_search.placeholder": "Искать таблицы, подключения, действия... или спросить AI", @@ -6644,6 +6646,8 @@ "sidebar.copy_object_name.failed": "Не удалось скопировать {{label}}: {{error}}", "sidebar.copy_object_name.label.event": "Имя события", "sidebar.copy_object_name.label.materialized_view": "Имя материализованного представления", + "sidebar.copy_object_name.label.package": "Имя пакета", + "sidebar.copy_object_name.label.sequence": "Имя последовательности", "sidebar.copy_object_name.label.table": "Имя таблицы", "sidebar.copy_object_name.label.view": "Имя представления", "sidebar.error.unknown": "Неизвестная ошибка", @@ -6950,7 +6954,9 @@ "sidebar.modal.tag.edit_title": "Изменить группу", "sidebar.object_group.events": "События", "sidebar.object_group.materialized_views": "Материализованные представления", + "sidebar.object_group.packages": "Пакеты", "sidebar.object_group.routines": "Функции и процедуры", + "sidebar.object_group.sequences": "Последовательности", "sidebar.object_group.tables": "Таблицы", "sidebar.object_group.triggers": "Триггеры", "sidebar.object_group.views": "Представления", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 3d5205c..94dd5a9 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -6612,7 +6612,9 @@ "sidebar.command_search.object_kind.all": "全部", "sidebar.command_search.object_kind.events": "事件", "sidebar.command_search.object_kind.filter_aria": "对象筛选", + "sidebar.command_search.object_kind.packages": "存储包", "sidebar.command_search.object_kind.routines": "函数", + "sidebar.command_search.object_kind.sequences": "序列", "sidebar.command_search.object_kind.tables": "表", "sidebar.command_search.object_kind.views": "视图", "sidebar.command_search.placeholder": "搜索表、连接、动作... 或问 AI", @@ -6644,6 +6646,8 @@ "sidebar.copy_object_name.failed": "复制{{label}}失败: {{error}}", "sidebar.copy_object_name.label.event": "事件名称", "sidebar.copy_object_name.label.materialized_view": "物化视图名称", + "sidebar.copy_object_name.label.package": "存储包名称", + "sidebar.copy_object_name.label.sequence": "序列名称", "sidebar.copy_object_name.label.table": "表名", "sidebar.copy_object_name.label.view": "视图名称", "sidebar.error.unknown": "未知错误", @@ -6950,7 +6954,9 @@ "sidebar.modal.tag.edit_title": "编辑分组", "sidebar.object_group.events": "事件", "sidebar.object_group.materialized_views": "物化视图", + "sidebar.object_group.packages": "存储包", "sidebar.object_group.routines": "函数和存储过程", + "sidebar.object_group.sequences": "序列", "sidebar.object_group.tables": "表", "sidebar.object_group.triggers": "触发器", "sidebar.object_group.views": "视图", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index 4c96f5a..f459a48 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -6612,7 +6612,9 @@ "sidebar.command_search.object_kind.all": "全部", "sidebar.command_search.object_kind.events": "事件", "sidebar.command_search.object_kind.filter_aria": "物件篩選", + "sidebar.command_search.object_kind.packages": "套件", "sidebar.command_search.object_kind.routines": "函式", + "sidebar.command_search.object_kind.sequences": "序列", "sidebar.command_search.object_kind.tables": "表格", "sidebar.command_search.object_kind.views": "檢視", "sidebar.command_search.placeholder": "搜尋表、連線、動作... 或問 AI", @@ -6644,6 +6646,8 @@ "sidebar.copy_object_name.failed": "複製{{label}}失敗:{{error}}", "sidebar.copy_object_name.label.event": "事件名稱", "sidebar.copy_object_name.label.materialized_view": "物化檢視名稱", + "sidebar.copy_object_name.label.package": "套件名稱", + "sidebar.copy_object_name.label.sequence": "序列名稱", "sidebar.copy_object_name.label.table": "資料表名稱", "sidebar.copy_object_name.label.view": "檢視名稱", "sidebar.error.unknown": "未知錯誤", @@ -6950,7 +6954,9 @@ "sidebar.modal.tag.edit_title": "編輯分組", "sidebar.object_group.events": "事件", "sidebar.object_group.materialized_views": "物化檢視", + "sidebar.object_group.packages": "套件", "sidebar.object_group.routines": "函式和預存程序", + "sidebar.object_group.sequences": "序列", "sidebar.object_group.tables": "資料表", "sidebar.object_group.triggers": "觸發器", "sidebar.object_group.views": "檢視",