mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-12 17:39:42 +08:00
✨ feat(query-editor): 扩展 SQL 编辑器对象跳转到视图触发器和存储过程
- 为 QueryEditor 补充视图、物化视图、触发器和函数元数据解析 - 支持 Ctrl/Cmd 点击打开对应对象定义页并同步当前 host/db 上下文 - 扩展 sidebarLocate 对触发器和函数的定位能力 - 补充 QueryEditor 与 sidebarLocate 定向测试覆盖
This commit is contained in:
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { SavedQuery, TabData } from '../types';
|
||||
import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator';
|
||||
import QueryEditor from './QueryEditor';
|
||||
import QueryEditor, { resolveQueryEditorNavigationTarget } from './QueryEditor';
|
||||
|
||||
const storeState = vi.hoisted(() => ({
|
||||
connections: [
|
||||
@@ -23,6 +23,7 @@ const storeState = vi.hoisted(() => ({
|
||||
],
|
||||
addSqlLog: vi.fn(),
|
||||
addTab: vi.fn(),
|
||||
setActiveContext: vi.fn(),
|
||||
updateQueryTabDraft: vi.fn(),
|
||||
savedQueries: [] as SavedQuery[],
|
||||
saveQuery: vi.fn(),
|
||||
@@ -72,6 +73,10 @@ const dataGridState = vi.hoisted(() => ({
|
||||
latestProps: null as any,
|
||||
}));
|
||||
|
||||
const autoFetchState = vi.hoisted(() => ({
|
||||
visible: false,
|
||||
}));
|
||||
|
||||
const editorState = vi.hoisted(() => {
|
||||
const state = {
|
||||
value: '',
|
||||
@@ -80,7 +85,11 @@ const editorState = vi.hoisted(() => {
|
||||
selection: null as any,
|
||||
providers: [] as any[],
|
||||
cursorPositionListeners: [] as Array<(event: any) => void>,
|
||||
mouseMoveListeners: [] as Array<(event: any) => void>,
|
||||
mouseDownListeners: [] as Array<(event: any) => void>,
|
||||
mouseLeaveListeners: [] as Array<() => void>,
|
||||
hasTextFocus: true,
|
||||
decorationIds: [] as string[],
|
||||
};
|
||||
const offsetAt = (position: { lineNumber: number; column: number }) => {
|
||||
const text = state.value;
|
||||
@@ -147,6 +156,24 @@ const editorState = vi.hoisted(() => {
|
||||
state.cursorPositionListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
onMouseMove: vi.fn((listener: (event: any) => void) => {
|
||||
state.mouseMoveListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
onMouseDown: vi.fn((listener: (event: any) => void) => {
|
||||
state.mouseDownListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
onMouseLeave: vi.fn((listener: () => void) => {
|
||||
state.mouseLeaveListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
deltaDecorations: vi.fn((oldDecorations: string[], newDecorations: any[]) => {
|
||||
state.decorationIds = newDecorations.map((_: any, index: number) => `decoration-${index + 1}`);
|
||||
return state.decorationIds;
|
||||
}),
|
||||
updateOptions: vi.fn(),
|
||||
onDidDispose: vi.fn(),
|
||||
hasTextFocus: vi.fn(() => state.hasTextFocus),
|
||||
revealLineInCenterIfOutsideViewport: vi.fn(),
|
||||
revealRangeInCenterIfOutsideViewport: vi.fn(),
|
||||
@@ -167,7 +194,7 @@ vi.mock('../store', () => {
|
||||
vi.mock('../../wailsjs/go/app/App', () => backendApp);
|
||||
|
||||
vi.mock('../utils/autoFetchVisibility', () => ({
|
||||
useAutoFetchVisibility: () => false,
|
||||
useAutoFetchVisibility: () => autoFetchState.visible,
|
||||
}));
|
||||
|
||||
vi.mock('@monaco-editor/react', () => ({
|
||||
@@ -196,6 +223,12 @@ vi.mock('@monaco-editor/react', () => ({
|
||||
this.endColumn = endColumn;
|
||||
}
|
||||
},
|
||||
MarkdownString: class {
|
||||
value: string;
|
||||
constructor(value: string) {
|
||||
this.value = value;
|
||||
}
|
||||
},
|
||||
Position: class {
|
||||
lineNumber: number;
|
||||
column: number;
|
||||
@@ -291,6 +324,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
dispatchEvent: vi.fn(),
|
||||
});
|
||||
storeState.addTab.mockReset();
|
||||
storeState.setActiveContext.mockReset();
|
||||
storeState.saveQuery.mockReset();
|
||||
storeState.savedQueries = [];
|
||||
storeState.activeTabId = 'tab-1';
|
||||
@@ -302,20 +336,30 @@ describe('QueryEditor external SQL save', () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValue({ success: true, data: [] });
|
||||
backendApp.DBGetColumns.mockResolvedValue({ success: true, data: [] });
|
||||
backendApp.DBGetIndexes.mockResolvedValue({ success: true, data: [] });
|
||||
backendApp.DBGetAllColumns.mockResolvedValue({ success: true, data: [] });
|
||||
backendApp.DBGetDatabases.mockResolvedValue({ success: true, data: [] });
|
||||
backendApp.DBGetTables.mockResolvedValue({ success: true, data: [] });
|
||||
backendApp.GenerateQueryID.mockResolvedValue('query-1');
|
||||
storeState.connections[0].config.type = 'mysql';
|
||||
storeState.connections[0].config.database = 'main';
|
||||
storeState.appearance.uiVersion = 'legacy';
|
||||
autoFetchState.visible = false;
|
||||
dataGridState.latestProps = null;
|
||||
editorState.value = '';
|
||||
editorState.position = { lineNumber: 1, column: 1 };
|
||||
editorState.selection = null;
|
||||
editorState.providers = [];
|
||||
editorState.cursorPositionListeners = [];
|
||||
editorState.mouseMoveListeners = [];
|
||||
editorState.mouseDownListeners = [];
|
||||
editorState.mouseLeaveListeners = [];
|
||||
editorState.hasTextFocus = true;
|
||||
editorState.decorationIds = [];
|
||||
editorState.editor.getValue.mockClear();
|
||||
editorState.editor.setValue.mockClear();
|
||||
editorState.editor.executeEdits.mockClear();
|
||||
editorState.editor.deltaDecorations.mockClear();
|
||||
editorState.editor.updateOptions.mockClear();
|
||||
storeState.updateQueryTabDraft.mockReset();
|
||||
});
|
||||
|
||||
@@ -332,6 +376,321 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(editorState.value).toBe('SELECT * FROM ');
|
||||
});
|
||||
|
||||
it('resolves database and table targets for ctrl/cmd navigation', () => {
|
||||
const tables = [
|
||||
{ dbName: 'main', tableName: 'users' },
|
||||
{ dbName: 'main', tableName: 'dbo.orders' },
|
||||
{ dbName: 'analytics', tableName: 'events' },
|
||||
];
|
||||
const views = [
|
||||
{ dbName: 'main', viewName: 'reporting.active_users', schemaName: 'reporting' },
|
||||
];
|
||||
const materializedViews = [
|
||||
{ dbName: 'analytics', viewName: 'mv_daily_stats', schemaName: undefined },
|
||||
];
|
||||
const triggers = [
|
||||
{ dbName: 'main', triggerName: 'audit.users_bi', tableName: 'audit.users', schemaName: 'audit' },
|
||||
];
|
||||
const routines = [
|
||||
{ dbName: 'main', routineName: 'reporting.refresh_stats', routineType: 'PROCEDURE', schemaName: 'reporting' },
|
||||
];
|
||||
|
||||
expect(resolveQueryEditorNavigationTarget('select * from analytics.events', 31, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
type: 'table',
|
||||
dbName: 'analytics',
|
||||
tableName: 'events',
|
||||
schemaName: undefined,
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('select * from dbo.orders', 21, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
type: 'table',
|
||||
dbName: 'main',
|
||||
tableName: 'dbo.orders',
|
||||
schemaName: 'dbo',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('use analytics', 6, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
type: 'database',
|
||||
dbName: 'analytics',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('select * from users', 18, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
type: 'table',
|
||||
dbName: 'main',
|
||||
tableName: 'users',
|
||||
schemaName: undefined,
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('select * from reporting.active_users', 31, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
type: 'view',
|
||||
dbName: 'main',
|
||||
viewName: 'reporting.active_users',
|
||||
schemaName: 'reporting',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('select * from analytics.mv_daily_stats', 37, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
type: 'materialized-view',
|
||||
dbName: 'analytics',
|
||||
viewName: 'mv_daily_stats',
|
||||
schemaName: undefined,
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('call audit.users_bi()', 18, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
type: 'trigger',
|
||||
dbName: 'main',
|
||||
triggerName: 'audit.users_bi',
|
||||
tableName: 'audit.users',
|
||||
schemaName: 'audit',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('call reporting.refresh_stats()', 21, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
type: 'routine',
|
||||
dbName: 'main',
|
||||
routineName: 'reporting.refresh_stats',
|
||||
routineType: 'PROCEDURE',
|
||||
schemaName: 'reporting',
|
||||
});
|
||||
});
|
||||
|
||||
it('opens a table tab on ctrl left click inside the editor', async () => {
|
||||
editorState.value = 'select * from analytics.events where id = 1';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }, { Database: 'analytics' }] });
|
||||
backendApp.DBGetTables
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] })
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_analytics: 'events' }] });
|
||||
backendApp.DBGetAllColumns
|
||||
.mockResolvedValueOnce({ success: true, data: [] })
|
||||
.mockResolvedValueOnce({ success: true, data: [] });
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'main' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const preventDefault = vi.fn();
|
||||
const stopPropagation = vi.fn();
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 27 } },
|
||||
event: {
|
||||
leftButton: true,
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
preventDefault,
|
||||
stopPropagation,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'analytics' });
|
||||
expect(storeState.addTab).toHaveBeenCalledWith({
|
||||
id: 'conn-1-analytics-table-events',
|
||||
title: 'events',
|
||||
type: 'table',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'analytics',
|
||||
tableName: 'events',
|
||||
});
|
||||
expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'gonavi:locate-sidebar-object',
|
||||
}));
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows link-style hover feedback when ctrl/cmd is pressed over a navigable identifier', async () => {
|
||||
editorState.value = 'select * from analytics.events where id = 1';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }, { Database: 'analytics' }] });
|
||||
backendApp.DBGetTables
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] })
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_analytics: 'events' }] });
|
||||
backendApp.DBGetAllColumns
|
||||
.mockResolvedValueOnce({ success: true, data: [] })
|
||||
.mockResolvedValueOnce({ success: true, data: [] });
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'main' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseMoveListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 27 } },
|
||||
event: {
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(editorState.editor.deltaDecorations).toHaveBeenCalled();
|
||||
expect(editorState.editor.updateOptions).toHaveBeenCalledWith({ mouseStyle: 'pointer' });
|
||||
const lastDecorationCall = editorState.editor.deltaDecorations.mock.calls.at(-1);
|
||||
expect(lastDecorationCall?.[1]?.[0]?.options?.inlineClassName).toBe('gonavi-query-editor-link-hint');
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseLeaveListeners[0]?.();
|
||||
});
|
||||
expect(editorState.editor.updateOptions).toHaveBeenLastCalledWith({ mouseStyle: 'text' });
|
||||
});
|
||||
|
||||
it('opens a view tab on ctrl left click inside the editor', async () => {
|
||||
editorState.value = 'select * from reporting.active_users';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
|
||||
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] });
|
||||
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
|
||||
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
|
||||
if (sql.includes('information_schema.views') || sql.includes('pg_catalog.pg_views') || sql.includes('USER_VIEWS') || sql.includes('ALL_VIEWS')) {
|
||||
return { success: true, data: [{ view_name: 'active_users', schema_name: 'reporting' }] };
|
||||
}
|
||||
return { success: true, data: [] };
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'main' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 31 } },
|
||||
event: {
|
||||
leftButton: true,
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'main' });
|
||||
expect(storeState.addTab).toHaveBeenCalledWith({
|
||||
id: 'view-def-conn-1-main-active_users',
|
||||
title: '视图: active_users',
|
||||
type: 'view-def',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
viewName: 'active_users',
|
||||
viewKind: 'view',
|
||||
});
|
||||
});
|
||||
|
||||
it('opens trigger and routine tabs on ctrl left click inside the editor', async () => {
|
||||
editorState.value = 'call audit.users_bi(); call reporting.refresh_stats();';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
|
||||
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] });
|
||||
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
|
||||
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
|
||||
if (sql.includes('information_schema.triggers') || sql.includes('SHOW TRIGGERS') || sql.includes('USER_TRIGGERS') || sql.includes('ALL_TRIGGERS')) {
|
||||
return { success: true, data: [{ trigger_name: 'users_bi', table_name: 'users', schema_name: 'audit' }] };
|
||||
}
|
||||
if (sql.includes('information_schema.routines') || sql.includes('SHOW FUNCTION STATUS') || sql.includes('SHOW PROCEDURE STATUS') || sql.includes('USER_OBJECTS') || sql.includes('ALL_OBJECTS')) {
|
||||
return { success: true, data: [{ routine_name: 'refresh_stats', routine_type: 'PROCEDURE', schema_name: 'reporting' }] };
|
||||
}
|
||||
return { success: true, data: [] };
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'main' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 12 } },
|
||||
event: {
|
||||
leftButton: true,
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 39 } },
|
||||
event: {
|
||||
leftButton: true,
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(storeState.addTab).toHaveBeenCalledWith({
|
||||
id: 'trigger-conn-1-main-audit.users_bi',
|
||||
title: '触发器: audit.users_bi',
|
||||
type: 'trigger',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
triggerName: 'audit.users_bi',
|
||||
});
|
||||
expect(storeState.addTab).toHaveBeenCalledWith({
|
||||
id: 'routine-def-conn-1-main-reporting.refresh_stats',
|
||||
title: '存储过程: reporting.refresh_stats',
|
||||
type: 'routine-def',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
routineName: 'reporting.refresh_stats',
|
||||
routineType: 'PROCEDURE',
|
||||
});
|
||||
});
|
||||
|
||||
it('switches current database on cmd left click for database identifiers', async () => {
|
||||
editorState.value = 'use analytics';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }, { Database: 'analytics' }] });
|
||||
backendApp.DBGetTables
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] })
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_analytics: 'events' }] });
|
||||
backendApp.DBGetAllColumns
|
||||
.mockResolvedValueOnce({ success: true, data: [] })
|
||||
.mockResolvedValueOnce({ success: true, data: [] });
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'main' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 6 } },
|
||||
event: {
|
||||
leftButton: true,
|
||||
ctrlKey: false,
|
||||
metaKey: true,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'analytics' });
|
||||
expect(storeState.addTab).not.toHaveBeenCalled();
|
||||
expect(storeState.updateQueryTabDraft).toHaveBeenLastCalledWith('tab-1', expect.objectContaining({
|
||||
dbName: 'analytics',
|
||||
}));
|
||||
});
|
||||
|
||||
it('keeps the editor empty when a tab draft is externally synced to an empty query', async () => {
|
||||
let renderer!: ReactTestRenderer;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,6 +96,32 @@ describe('sidebarLocate', () => {
|
||||
})).toBeNull();
|
||||
});
|
||||
|
||||
it('builds locate requests from trigger and routine tabs', () => {
|
||||
expect(normalizeSidebarLocateObjectRequestFromTab({
|
||||
id: 'trigger-conn-1-main-audit.users_bi',
|
||||
type: 'trigger',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
triggerName: 'audit.users_bi',
|
||||
})).toMatchObject({
|
||||
tableName: 'audit.users_bi',
|
||||
schemaName: 'audit',
|
||||
objectGroup: 'triggers',
|
||||
});
|
||||
|
||||
expect(normalizeSidebarLocateObjectRequestFromTab({
|
||||
id: 'routine-def-conn-1-main-reporting.refresh_stats',
|
||||
type: 'routine-def',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
routineName: 'reporting.refresh_stats',
|
||||
})).toMatchObject({
|
||||
tableName: 'reporting.refresh_stats',
|
||||
schemaName: 'reporting',
|
||||
objectGroup: 'routines',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps StarRocks materialized view tabs on the materialized views branch', () => {
|
||||
const request = normalizeSidebarLocateObjectRequestFromTab({
|
||||
id: 'view-def-conn-1-main-sales.mv_daily',
|
||||
@@ -182,4 +208,81 @@ describe('sidebarLocate', () => {
|
||||
'conn-1-main-public.users',
|
||||
]);
|
||||
});
|
||||
|
||||
it('finds trigger and routine paths from loaded tree data', () => {
|
||||
const triggerTarget = resolveSidebarLocateTarget({
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
tableName: 'audit.users_bi',
|
||||
schemaName: 'audit',
|
||||
objectGroup: 'triggers',
|
||||
}, { groupBySchema: true });
|
||||
|
||||
const routineTarget = resolveSidebarLocateTarget({
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
tableName: 'reporting.refresh_stats',
|
||||
schemaName: 'reporting',
|
||||
objectGroup: 'routines',
|
||||
}, { groupBySchema: true });
|
||||
|
||||
const tree = [
|
||||
{
|
||||
key: 'conn-1',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-main',
|
||||
dataRef: { id: 'conn-1', dbName: 'main' },
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-main-schema-audit',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-main-schema-audit-triggers',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-main-trigger-audit.users_bi-audit.users',
|
||||
type: 'db-trigger',
|
||||
dataRef: { id: 'conn-1', dbName: 'main', triggerName: 'audit.users_bi', schemaName: 'audit' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'conn-1-main-schema-reporting',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-main-schema-reporting-routines',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-main-routine-reporting.refresh_stats',
|
||||
type: 'routine',
|
||||
dataRef: { id: 'conn-1', dbName: 'main', routineName: 'reporting.refresh_stats', schemaName: 'reporting' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(findSidebarNodePathForLocate(tree, triggerTarget)).toEqual([
|
||||
'conn-1',
|
||||
'conn-1-main',
|
||||
'conn-1-main-schema-audit',
|
||||
'conn-1-main-schema-audit-triggers',
|
||||
'conn-1-main-trigger-audit.users_bi-audit.users',
|
||||
]);
|
||||
expect(findSidebarNodePathForLocate(tree, routineTarget)).toEqual([
|
||||
'conn-1',
|
||||
'conn-1-main',
|
||||
'conn-1-main-schema-reporting',
|
||||
'conn-1-main-schema-reporting-routines',
|
||||
'conn-1-main-routine-reporting.refresh_stats',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SidebarLocateObjectGroup = 'tables' | 'views' | 'materializedViews';
|
||||
export type SidebarLocateObjectGroup = 'tables' | 'views' | 'materializedViews' | 'triggers' | 'routines';
|
||||
|
||||
export interface SidebarLocateObjectRequest {
|
||||
tabId?: string;
|
||||
@@ -38,6 +38,8 @@ export interface SidebarLocateTabLike {
|
||||
tableName?: string;
|
||||
viewName?: string;
|
||||
viewKind?: string;
|
||||
triggerName?: string;
|
||||
routineName?: string;
|
||||
}
|
||||
|
||||
const toTrimmedString = (value: unknown): string => String(value ?? '').trim();
|
||||
@@ -57,15 +59,21 @@ const inferObjectGroup = (detail: Record<string, unknown>, connectionId: string,
|
||||
const explicitGroup = toTrimmedString(detail.objectGroup);
|
||||
if (explicitGroup === 'views' || explicitGroup === 'view') return 'views';
|
||||
if (explicitGroup === 'materializedViews' || explicitGroup === 'materialized-view') return 'materializedViews';
|
||||
if (explicitGroup === 'triggers' || explicitGroup === 'trigger') return 'triggers';
|
||||
if (explicitGroup === 'routines' || explicitGroup === 'routine') return 'routines';
|
||||
|
||||
const explicitType = toTrimmedString(detail.objectType);
|
||||
if (explicitType === 'view' || explicitType === 'views') return 'views';
|
||||
if (explicitType === 'materialized' || explicitType === 'materialized-view') return 'materializedViews';
|
||||
if (explicitType === 'trigger' || explicitType === 'triggers') return 'triggers';
|
||||
if (explicitType === 'routine' || explicitType === 'routines') return 'routines';
|
||||
|
||||
const tabId = toTrimmedString(detail.tabId);
|
||||
const dbNodeKey = `${connectionId}-${dbName}`;
|
||||
if (tabId.startsWith(`${dbNodeKey}-materialized-view-`)) return 'materializedViews';
|
||||
if (tabId.startsWith(`${dbNodeKey}-view-`)) return 'views';
|
||||
if (tabId.startsWith(`${dbNodeKey}-trigger-`)) return 'triggers';
|
||||
if (tabId.startsWith(`${dbNodeKey}-routine-`) || tabId.startsWith(`routine-def-${connectionId}-${dbName}-`)) return 'routines';
|
||||
|
||||
return 'tables';
|
||||
};
|
||||
@@ -74,7 +82,7 @@ export const normalizeSidebarLocateObjectRequest = (detail: unknown): SidebarLoc
|
||||
const raw = (detail || {}) as Record<string, unknown>;
|
||||
const connectionId = toTrimmedString(raw.connectionId);
|
||||
const dbName = toTrimmedString(raw.dbName);
|
||||
const tableName = toTrimmedString(raw.tableName || raw.objectName || raw.viewName);
|
||||
const tableName = toTrimmedString(raw.tableName || raw.objectName || raw.viewName || raw.triggerName || raw.routineName);
|
||||
|
||||
if (!connectionId || !dbName || !tableName) {
|
||||
return null;
|
||||
@@ -97,8 +105,12 @@ export const normalizeSidebarLocateObjectRequestFromTab = (tab: SidebarLocateTab
|
||||
if (!tab) return null;
|
||||
const objectName = tab.type === 'view-def'
|
||||
? toTrimmedString(tab.viewName || tab.tableName)
|
||||
: toTrimmedString(tab.tableName || tab.viewName);
|
||||
if (tab.type !== 'table' && tab.type !== 'view-def') {
|
||||
: tab.type === 'trigger'
|
||||
? toTrimmedString(tab.triggerName || tab.tableName)
|
||||
: tab.type === 'routine-def'
|
||||
? toTrimmedString(tab.routineName || tab.tableName)
|
||||
: toTrimmedString(tab.tableName || tab.viewName);
|
||||
if (tab.type !== 'table' && tab.type !== 'view-def' && tab.type !== 'trigger' && tab.type !== 'routine-def') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -109,7 +121,7 @@ export const normalizeSidebarLocateObjectRequestFromTab = (tab: SidebarLocateTab
|
||||
tableName: objectName,
|
||||
objectGroup: tab.type === 'view-def'
|
||||
? (tab.viewKind === 'materialized' ? 'materializedViews' : 'views')
|
||||
: undefined,
|
||||
: (tab.type === 'trigger' ? 'triggers' : (tab.type === 'routine-def' ? 'routines' : undefined)),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -121,7 +133,13 @@ export const resolveSidebarLocateTarget = (
|
||||
const databaseKey = `${request.connectionId}-${request.dbName}`;
|
||||
const fallbackTargetKey = request.objectGroup === 'materializedViews'
|
||||
? `${databaseKey}-materialized-view-${request.tableName}`
|
||||
: (request.objectGroup === 'views' ? `${databaseKey}-view-${request.tableName}` : `${databaseKey}-${request.tableName}`);
|
||||
: request.objectGroup === 'views'
|
||||
? `${databaseKey}-view-${request.tableName}`
|
||||
: request.objectGroup === 'triggers'
|
||||
? `${databaseKey}-trigger-${request.tableName}`
|
||||
: request.objectGroup === 'routines'
|
||||
? `${databaseKey}-routine-${request.tableName}`
|
||||
: `${databaseKey}-${request.tableName}`;
|
||||
const targetKey = request.tabId || fallbackTargetKey;
|
||||
const schemaSegment = request.schemaName || 'default';
|
||||
const schemaKey = options.groupBySchema ? `${databaseKey}-schema-${schemaSegment}` : undefined;
|
||||
@@ -204,6 +222,16 @@ const matchesLocateObjectNode = (node: SidebarLocateTreeNodeLike, target: Sideba
|
||||
return matchesLocateObjectName(target, toTrimmedString(dataRef.viewName || dataRef.tableName), toTrimmedString(dataRef.schemaName));
|
||||
}
|
||||
|
||||
if (target.objectGroup === 'triggers') {
|
||||
if (node.type !== 'db-trigger') return false;
|
||||
return matchesLocateObjectName(target, toTrimmedString(dataRef.triggerName || dataRef.tableName), toTrimmedString(dataRef.schemaName));
|
||||
}
|
||||
|
||||
if (target.objectGroup === 'routines') {
|
||||
if (node.type !== 'routine') return false;
|
||||
return matchesLocateObjectName(target, toTrimmedString(dataRef.routineName || dataRef.tableName), toTrimmedString(dataRef.schemaName));
|
||||
}
|
||||
|
||||
if (node.type !== 'table') return false;
|
||||
return matchesLocateObjectName(target, toTrimmedString(dataRef.tableName), toTrimmedString(dataRef.schemaName));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user