mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-22 22:43:46 +08:00
⚡️ perf(frontend): 优化长时运行下的搜索与缓存占用
- 为 V2 cmd+k 搜索预建索引并限制初始/宽泛结果数量 - 清理冷数据库树和 DataViewer 长生命周期快照缓存 - 收紧运行时 SQL 日志预算并在 hydration 时压缩旧缓存
This commit is contained in:
@@ -164,6 +164,14 @@ describe('DataViewer safe editing locator', () => {
|
||||
expect(source).toContain('data_viewer.sql_log.phase.sort_buffer_retry');
|
||||
});
|
||||
|
||||
it('caps viewer filter snapshots so long-running sessions do not retain unbounded table state', () => {
|
||||
const source = readFileSync(new URL('./DataViewer.tsx', import.meta.url), 'utf8');
|
||||
|
||||
expect(source).toContain('const MAX_VIEWER_FILTER_SNAPSHOTS = 64;');
|
||||
expect(source).toContain('const trimViewerFilterSnapshots = () => {');
|
||||
expect(source).toContain('setViewerFilterSnapshot(normalizedTabId, {');
|
||||
});
|
||||
|
||||
it('enables table preview editing after primary keys are loaded', async () => {
|
||||
backendApp.DBGetColumns.mockResolvedValue({
|
||||
success: true,
|
||||
|
||||
@@ -280,8 +280,32 @@ type ViewerScrollSnapshot = {
|
||||
};
|
||||
|
||||
const viewerFilterSnapshotsByTab = new Map<string, ViewerFilterSnapshot>();
|
||||
const MAX_VIEWER_FILTER_SNAPSHOTS = 64;
|
||||
const VIEWER_SCROLL_SNAPSHOT_PERSIST_DELAY_MS = 160;
|
||||
|
||||
const trimViewerFilterSnapshots = () => {
|
||||
while (viewerFilterSnapshotsByTab.size > MAX_VIEWER_FILTER_SNAPSHOTS) {
|
||||
const oldestKey = viewerFilterSnapshotsByTab.keys().next().value;
|
||||
if (!oldestKey) {
|
||||
break;
|
||||
}
|
||||
viewerFilterSnapshotsByTab.delete(oldestKey);
|
||||
}
|
||||
};
|
||||
|
||||
const setViewerFilterSnapshot = (
|
||||
tabId: string,
|
||||
snapshot: ViewerFilterSnapshot,
|
||||
) => {
|
||||
const normalizedTabId = String(tabId || '').trim();
|
||||
if (!normalizedTabId) return;
|
||||
if (viewerFilterSnapshotsByTab.has(normalizedTabId)) {
|
||||
viewerFilterSnapshotsByTab.delete(normalizedTabId);
|
||||
}
|
||||
viewerFilterSnapshotsByTab.set(normalizedTabId, snapshot);
|
||||
trimViewerFilterSnapshots();
|
||||
};
|
||||
|
||||
const normalizeViewerFilterConditions = (conditions: FilterCondition[] | undefined): FilterCondition[] => {
|
||||
if (!Array.isArray(conditions)) return [];
|
||||
return conditions.map((cond) => ({
|
||||
@@ -380,7 +404,7 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
const persistViewerSnapshot = useCallback((tabId: string, overrides?: Partial<ViewerFilterSnapshot>) => {
|
||||
const normalizedTabId = String(tabId || '').trim();
|
||||
if (!normalizedTabId) return;
|
||||
viewerFilterSnapshotsByTab.set(normalizedTabId, {
|
||||
setViewerFilterSnapshot(normalizedTabId, {
|
||||
showFilter,
|
||||
conditions: normalizeViewerFilterConditions(filterConditions),
|
||||
quickWhereCondition: normalizeQuickWhereCondition(quickWhereCondition),
|
||||
|
||||
@@ -2344,9 +2344,6 @@ describe('Sidebar locate toolbar', () => {
|
||||
const loadTablesStart = source.indexOf('const loadTables = async (node: any) => {');
|
||||
const loadTablesEnd = source.indexOf('const config = {', loadTablesStart);
|
||||
const loadTablesSource = source.slice(loadTablesStart, loadTablesEnd);
|
||||
const externalSqlReadStart = source.indexOf('const externalSQLDirectoryResults = await Promise.all(', loadTablesStart);
|
||||
const externalSqlReadEnd = source.indexOf('const externalSQLTrees = externalSQLDirectoryResults.reduce', externalSqlReadStart);
|
||||
const externalSqlReadSource = source.slice(externalSqlReadStart, externalSqlReadEnd);
|
||||
const externalSqlFlowStart = source.indexOf('const handleAddExternalSQLDirectory = async (node: any) => {');
|
||||
const externalSqlFlowEnd = source.indexOf('const cancelSQLFileExecution = () => {', externalSqlFlowStart);
|
||||
const externalSqlFlowSource = source.slice(externalSqlFlowStart, externalSqlFlowEnd);
|
||||
@@ -2369,8 +2366,6 @@ describe('Sidebar locate toolbar', () => {
|
||||
[
|
||||
loadTablesStart,
|
||||
loadTablesEnd,
|
||||
externalSqlReadStart,
|
||||
externalSqlReadEnd,
|
||||
externalSqlFlowStart,
|
||||
externalSqlFlowEnd,
|
||||
treeTitleStart,
|
||||
@@ -2387,9 +2382,7 @@ describe('Sidebar locate toolbar', () => {
|
||||
|
||||
expect(loadTablesSource).toContain("title: t('sidebar.tree.saved_queries')");
|
||||
expect(loadTablesSource).not.toContain("title: '已存查询'");
|
||||
|
||||
expect(externalSqlReadSource).toContain("t('sidebar.message.external_sql_directory_read_failed'");
|
||||
expect(externalSqlReadSource).not.toContain('SQL 目录读取失败');
|
||||
expect(source).not.toContain('const externalSQLDirectoryResults = await Promise.all(');
|
||||
expect(loadTablesSource).not.toContain('SQL 目录读取失败');
|
||||
expect(loadTablesSource).not.toContain("'SQL目录'");
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ import {
|
||||
resolveSidebarDropInsertBefore,
|
||||
resolveSidebarDropNodeFromDomEvent,
|
||||
resolveSidebarDropTargetMetricsFromDomEvent,
|
||||
resolveSidebarDatabaseTreePruneKeys,
|
||||
resolveSidebarNodeConnectionId,
|
||||
resolveSidebarTagDropInsertBefore,
|
||||
resolveV2ActiveConnectionId,
|
||||
@@ -190,6 +191,7 @@ export {
|
||||
resolveSidebarDropInsertBefore,
|
||||
resolveSidebarDropNodeFromDomEvent,
|
||||
resolveSidebarDropTargetMetricsFromDomEvent,
|
||||
resolveSidebarDatabaseTreePruneKeys,
|
||||
resolveSidebarNodeConnectionId,
|
||||
resolveSidebarTagDropInsertBefore,
|
||||
resolveV2ActiveConnectionId,
|
||||
@@ -205,6 +207,7 @@ export type { V2CommandSearchItem, V2RailConnectionGroup } from './sidebarV2Util
|
||||
const { Search } = Input;
|
||||
const SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS = 50;
|
||||
const SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS = 160;
|
||||
const SIDEBAR_CACHED_DATABASE_TREE_LIMIT = 12;
|
||||
|
||||
// resolveV2ObjectGroupTitle 已迁移到 ./sidebar/sidebarHelpers
|
||||
|
||||
@@ -506,6 +509,8 @@ const Sidebar: React.FC<{
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
const selectedNodesRef = useRef<any[]>([]);
|
||||
const loadingNodesRef = useRef<Set<string>>(new Set());
|
||||
const databaseTreeTouchedAtRef = useRef<Record<string, number>>({});
|
||||
const pruneLoadedDatabaseTreesRef = useRef<() => void>(() => {});
|
||||
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const treeDragSelectSuppressUntilRef = useRef(0);
|
||||
const treeDragSelectionSnapshotRef = useRef<{
|
||||
@@ -544,6 +549,7 @@ const Sidebar: React.FC<{
|
||||
}, [setActiveContext]);
|
||||
|
||||
const openV2CommandSearch = useCallback(() => {
|
||||
pruneLoadedDatabaseTreesRef.current();
|
||||
setIsV2CommandSearchOpen(true);
|
||||
setV2CommandActiveIndex(0);
|
||||
}, []);
|
||||
@@ -984,6 +990,55 @@ const Sidebar: React.FC<{
|
||||
return nextTreeData;
|
||||
};
|
||||
|
||||
const clearTreeNodeChildrenByKeys = useCallback((keysToClear: string[]) => {
|
||||
const keysToClearSet = new Set(keysToClear.map((key) => String(key || '').trim()).filter(Boolean));
|
||||
if (keysToClearSet.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clearChildren = (nodes: TreeNode[]): TreeNode[] => (
|
||||
nodes.map((node) => {
|
||||
const nodeKey = String(node.key || '').trim();
|
||||
if (keysToClearSet.has(nodeKey)) {
|
||||
return { ...node, children: undefined };
|
||||
}
|
||||
if (node.children?.length) {
|
||||
return { ...node, children: clearChildren(node.children) };
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
|
||||
setTreeData((prev) => {
|
||||
const nextTreeData = clearChildren(prev);
|
||||
treeDataRef.current = nextTreeData;
|
||||
return nextTreeData;
|
||||
});
|
||||
setLoadedKeys((prev) => prev.filter((key) => !keysToClearSet.has(String(key))));
|
||||
keysToClearSet.forEach((key) => {
|
||||
delete databaseTreeTouchedAtRef.current[key];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const pruneLoadedDatabaseTrees = useCallback(() => {
|
||||
const activeDatabaseKey = activeContext?.connectionId && activeContext?.dbName
|
||||
? `${activeContext.connectionId}-${activeContext.dbName}`
|
||||
: '';
|
||||
const keysToClear = resolveSidebarDatabaseTreePruneKeys({
|
||||
treeData: treeDataRef.current,
|
||||
expandedKeys,
|
||||
selectedKeys,
|
||||
activeDatabaseKey,
|
||||
touchedAtByDatabaseKey: databaseTreeTouchedAtRef.current,
|
||||
maxLoadedDatabases: SIDEBAR_CACHED_DATABASE_TREE_LIMIT,
|
||||
});
|
||||
if (keysToClear.length === 0) {
|
||||
return;
|
||||
}
|
||||
clearTreeNodeChildrenByKeys(keysToClear);
|
||||
}, [activeContext?.connectionId, activeContext?.dbName, clearTreeNodeChildrenByKeys, expandedKeys, selectedKeys]);
|
||||
pruneLoadedDatabaseTreesRef.current = pruneLoadedDatabaseTrees;
|
||||
|
||||
const mergeExpandedTreeKeys = (requiredKeys: React.Key[]) => {
|
||||
setExpandedKeys(prev => {
|
||||
const merged = [...prev];
|
||||
@@ -1727,7 +1782,6 @@ const Sidebar: React.FC<{
|
||||
loadTables,
|
||||
} = useSidebarTreeLoaders({
|
||||
savedQueries,
|
||||
externalSQLDirectories,
|
||||
tableSortPreference,
|
||||
tableAccessCount,
|
||||
pinnedSidebarTables,
|
||||
@@ -1740,7 +1794,10 @@ const Sidebar: React.FC<{
|
||||
buildJVMRuntimeConfig,
|
||||
buildJVMDiagnosticTreeNodes,
|
||||
resolveSavedQueryDisplayName,
|
||||
decorateExternalSQLTreeNode,
|
||||
onDatabaseTreeLoaded: (databaseKey: string) => {
|
||||
databaseTreeTouchedAtRef.current[databaseKey] = Date.now();
|
||||
pruneLoadedDatabaseTrees();
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -1950,6 +2007,7 @@ const Sidebar: React.FC<{
|
||||
treeViewportWidth,
|
||||
treeHeight,
|
||||
isV2Ui,
|
||||
isV2CommandSearchOpen,
|
||||
connections,
|
||||
connectionIds,
|
||||
selectedKeys,
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from './sidebarHelpers';
|
||||
import type { SearchScope } from '../sidebarCoreUtils';
|
||||
import {
|
||||
buildV2CommandSearchTreeIndex,
|
||||
V2_TREE_HORIZONTAL_SCROLL_BOTTOM_RESERVE,
|
||||
estimateV2TreeHorizontalScrollWidth,
|
||||
filterV2CommandSearchTreeItems,
|
||||
@@ -74,6 +75,7 @@ type SidebarSearchModelArgs = {
|
||||
treeViewportWidth: number;
|
||||
treeHeight: number;
|
||||
isV2Ui: boolean;
|
||||
isV2CommandSearchOpen: boolean;
|
||||
connections: SavedConnection[];
|
||||
connectionIds: string[];
|
||||
selectedKeys: React.Key[];
|
||||
@@ -111,6 +113,7 @@ export const useSidebarSearchModel = ({
|
||||
treeViewportWidth,
|
||||
treeHeight,
|
||||
isV2Ui,
|
||||
isV2CommandSearchOpen,
|
||||
connections,
|
||||
connectionIds,
|
||||
selectedKeys,
|
||||
@@ -179,6 +182,10 @@ export const useSidebarSearchModel = ({
|
||||
};
|
||||
|
||||
const currentLanguage = getCurrentLanguage();
|
||||
const connectionById = useMemo(
|
||||
() => new Map(connections.map((connection) => [connection.id, connection])),
|
||||
[connections],
|
||||
);
|
||||
|
||||
const searchScopeSummary = useMemo(() => {
|
||||
if (searchScopes.includes('smart')) {
|
||||
@@ -360,6 +367,9 @@ export const useSidebarSearchModel = ({
|
||||
}, [deferredSearchValue, searchScopes, treeData]);
|
||||
|
||||
const commandSearchTreeItems = useMemo(() => {
|
||||
if (!isV2CommandSearchOpen) {
|
||||
return [];
|
||||
}
|
||||
const result: V2CommandSearchItem[] = [];
|
||||
const visit = (nodes: TreeNode[]) => {
|
||||
nodes.forEach((node) => {
|
||||
@@ -375,7 +385,7 @@ export const useSidebarSearchModel = ({
|
||||
node,
|
||||
});
|
||||
} else if (node.type === 'database') {
|
||||
const conn = connections.find((item) => item.id === dataRef.id);
|
||||
const conn = connectionById.get(String(dataRef.id || ''));
|
||||
result.push({
|
||||
key: `node-${node.key}`,
|
||||
kind: 'node',
|
||||
@@ -392,7 +402,7 @@ export const useSidebarSearchModel = ({
|
||||
|| node.type === 'db-event'
|
||||
|| node.type === 'routine'
|
||||
) {
|
||||
const conn = connections.find((item) => item.id === dataRef.id);
|
||||
const conn = connectionById.get(String(dataRef.id || ''));
|
||||
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({
|
||||
@@ -412,7 +422,11 @@ export const useSidebarSearchModel = ({
|
||||
|
||||
visit(treeData);
|
||||
return result;
|
||||
}, [connections, treeData]);
|
||||
}, [connectionById, extractObjectName, isV2CommandSearchOpen, treeData]);
|
||||
const commandSearchTreeIndex = useMemo(
|
||||
() => buildV2CommandSearchTreeIndex(commandSearchTreeItems),
|
||||
[commandSearchTreeItems],
|
||||
);
|
||||
|
||||
const commandSearchRecentItems = useMemo<V2CommandSearchItem[]>(() => {
|
||||
return sqlLogs.slice(0, 5).map((log) => ({
|
||||
@@ -473,8 +487,8 @@ export const useSidebarSearchModel = ({
|
||||
const v2CommandSearchObjectMode = v2CommandSearchQuery.mode === 'object';
|
||||
const v2CommandSearchAiMode = v2CommandSearchQuery.mode === 'ai';
|
||||
const filteredCommandSearchTreeItems = useMemo(() => {
|
||||
return filterV2CommandSearchTreeItems(commandSearchTreeItems, v2CommandSearchQuery);
|
||||
}, [commandSearchTreeItems, v2CommandSearchQuery]);
|
||||
return filterV2CommandSearchTreeItems(commandSearchTreeIndex, v2CommandSearchQuery);
|
||||
}, [commandSearchTreeIndex, v2CommandSearchQuery]);
|
||||
|
||||
const filteredCommandSearchActionItems = useMemo(() => {
|
||||
if (v2CommandSearchObjectMode || v2CommandSearchAiMode) return [];
|
||||
|
||||
@@ -13,14 +13,13 @@ import {
|
||||
TableOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { SavedConnection, SavedQuery, ExternalSQLDirectory, ExternalSQLTreeEntry, JVMCapability, JVMResourceSummary } from '../../types';
|
||||
import type { SavedConnection, SavedQuery, JVMCapability, JVMResourceSummary } from '../../types';
|
||||
import { useStore } from '../../store';
|
||||
import { t } from '../../i18n';
|
||||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
|
||||
import { buildRedisDbNodeLabel, getRedisDbAlias } from '../../utils/redisDbAlias';
|
||||
import { buildJVMMonitoringActionDescriptors } from '../../utils/jvmSidebarActions';
|
||||
import { type SidebarViewMetadataEntry } from '../../utils/sidebarMetadata';
|
||||
import { buildExternalSQLRootNode, type ExternalSQLTreeNode } from '../../utils/externalSqlTree';
|
||||
import {
|
||||
buildQualifiedName,
|
||||
buildSidebarObjectKeyName,
|
||||
@@ -47,7 +46,7 @@ import {
|
||||
sortSidebarTableEntries,
|
||||
type SidebarTreeNode as TreeNode,
|
||||
} from '../sidebarV2Utils';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, GetDriverStatusList, JVMProbeCapabilities, ListSQLDirectory } from '../../../wailsjs/go/app/App';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, GetDriverStatusList, JVMProbeCapabilities } from '../../../wailsjs/go/app/App';
|
||||
|
||||
type DriverStatusSnapshot = {
|
||||
type: string;
|
||||
@@ -119,7 +118,6 @@ const resolveSavedConnectionDriverType = (conn: SavedConnection | undefined): st
|
||||
|
||||
type UseSidebarTreeLoadersOptions = {
|
||||
savedQueries: SavedQuery[];
|
||||
externalSQLDirectories: ExternalSQLDirectory[];
|
||||
tableSortPreference: Record<string, any>;
|
||||
tableAccessCount: Record<string, any>;
|
||||
pinnedSidebarTables: any[];
|
||||
@@ -132,12 +130,11 @@ type UseSidebarTreeLoadersOptions = {
|
||||
buildJVMRuntimeConfig: (conn: SavedConnection & { dbName?: string }, providerMode: string) => any;
|
||||
buildJVMDiagnosticTreeNodes: (conn: SavedConnection) => TreeNode[];
|
||||
resolveSavedQueryDisplayName: (name: string | null | undefined) => string;
|
||||
decorateExternalSQLTreeNode: (node: ExternalSQLTreeNode) => TreeNode;
|
||||
onDatabaseTreeLoaded?: (databaseKey: string) => void;
|
||||
};
|
||||
|
||||
export const useSidebarTreeLoaders = ({
|
||||
savedQueries,
|
||||
externalSQLDirectories,
|
||||
tableSortPreference,
|
||||
tableAccessCount,
|
||||
pinnedSidebarTables,
|
||||
@@ -150,7 +147,7 @@ export const useSidebarTreeLoaders = ({
|
||||
buildJVMRuntimeConfig,
|
||||
buildJVMDiagnosticTreeNodes,
|
||||
resolveSavedQueryDisplayName,
|
||||
decorateExternalSQLTreeNode,
|
||||
onDatabaseTreeLoaded,
|
||||
}: UseSidebarTreeLoadersOptions) => {
|
||||
const driverStatusCacheRef = useRef<{
|
||||
fetchedAt: number;
|
||||
@@ -516,40 +513,6 @@ export const useSidebarTreeLoaders = ({
|
||||
loadFunctions(conn, conn.dbName),
|
||||
loadDatabaseEvents(conn, conn.dbName),
|
||||
]);
|
||||
const externalSQLDirectoryResults = await Promise.all(
|
||||
externalSQLDirectories.map(async (directory: ExternalSQLDirectory) => {
|
||||
const directoryRes = await ListSQLDirectory(directory.path);
|
||||
if (!directoryRes.success) {
|
||||
message.warning({
|
||||
key: `external-sql-${directory.id}`,
|
||||
content: t('sidebar.message.external_sql_directory_read_failed', {
|
||||
name: directory.name,
|
||||
error: directoryRes.message,
|
||||
}),
|
||||
});
|
||||
return { id: directory.id, entries: [] as ExternalSQLTreeEntry[] };
|
||||
}
|
||||
return {
|
||||
id: directory.id,
|
||||
entries: Array.isArray(directoryRes.data) ? directoryRes.data as ExternalSQLTreeEntry[] : [],
|
||||
};
|
||||
}),
|
||||
);
|
||||
const externalSQLTrees = externalSQLDirectoryResults.reduce<Record<string, ExternalSQLTreeEntry[]>>((accumulator, item) => {
|
||||
accumulator[item.id] = item.entries;
|
||||
return accumulator;
|
||||
}, {});
|
||||
const externalSQLRootNode = decorateExternalSQLTreeNode(buildExternalSQLRootNode({
|
||||
dbNodeKey: String(key),
|
||||
connectionId: String(conn.id),
|
||||
dbName: String(conn.dbName),
|
||||
directories: externalSQLDirectories,
|
||||
directoryTrees: externalSQLTrees,
|
||||
labels: {
|
||||
root: t('sidebar.external_sql.root'),
|
||||
directoryFallback: t('sidebar.external_sql.directory_fallback'),
|
||||
},
|
||||
}));
|
||||
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 : [];
|
||||
@@ -855,6 +818,7 @@ export const useSidebarTreeLoaders = ({
|
||||
|
||||
replaceTreeNodeChildren(key, [queriesNode, ...groupedNodes]);
|
||||
}
|
||||
onDatabaseTreeLoaded?.(String(key));
|
||||
} else {
|
||||
setConnectionStates(prev => ({ ...prev, [key as string]: 'error' }));
|
||||
message.error({ content: res.message, key: `db-${key}-tables` });
|
||||
|
||||
116
frontend/src/components/sidebarV2Utils.command-search.test.ts
Normal file
116
frontend/src/components/sidebarV2Utils.command-search.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT,
|
||||
V2_COMMAND_SEARCH_MAX_TREE_RESULTS,
|
||||
buildV2CommandSearchTreeIndex,
|
||||
filterV2CommandSearchTreeItems,
|
||||
parseV2CommandSearchQuery,
|
||||
resolveSidebarDatabaseTreePruneKeys,
|
||||
type V2CommandSearchItem,
|
||||
} from './sidebarV2Utils';
|
||||
|
||||
const buildNodeItems = (count: number): V2CommandSearchItem[] => {
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
key: `node-table-${index}`,
|
||||
kind: 'node' as const,
|
||||
title: `fs_order_${index}`,
|
||||
meta: `开发240 · front_end_sys_${index % 4}`,
|
||||
icon: null,
|
||||
node: {
|
||||
type: index % 6 === 0 ? 'view' : 'table',
|
||||
key: `table-${index}`,
|
||||
title: `fs_order_${index}`,
|
||||
dataRef: {
|
||||
tableName: `fs_order_${index}`,
|
||||
viewName: index % 6 === 0 ? `v_order_${index}` : undefined,
|
||||
dbName: `front_end_sys_${index % 4}`,
|
||||
name: `obj_${index}`,
|
||||
config: {
|
||||
host: `10.0.0.${index % 16}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
describe('sidebarV2 command search performance helpers', () => {
|
||||
it('keeps the initial tree result limit when the query is empty', () => {
|
||||
const items = buildNodeItems(V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT + 80);
|
||||
|
||||
expect(
|
||||
filterV2CommandSearchTreeItems(items, parseV2CommandSearchQuery('')),
|
||||
).toHaveLength(V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT);
|
||||
});
|
||||
|
||||
it('caps broad keyword matches to avoid rendering the full loaded tree', () => {
|
||||
const items = buildNodeItems(V2_COMMAND_SEARCH_MAX_TREE_RESULTS + 160);
|
||||
|
||||
const result = filterV2CommandSearchTreeItems(
|
||||
items,
|
||||
parseV2CommandSearchQuery('fs_order'),
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(V2_COMMAND_SEARCH_MAX_TREE_RESULTS);
|
||||
expect(result[0]?.key).toBe('node-table-0');
|
||||
expect(result[result.length - 1]?.key).toBe(`node-table-${V2_COMMAND_SEARCH_MAX_TREE_RESULTS - 1}`);
|
||||
});
|
||||
|
||||
it('returns the same matches when filtering with a prebuilt search index', () => {
|
||||
const items = buildNodeItems(200);
|
||||
const index = buildV2CommandSearchTreeIndex(items);
|
||||
const query = parseV2CommandSearchQuery('@fs_order_1');
|
||||
|
||||
expect(filterV2CommandSearchTreeItems(index, query)).toEqual(
|
||||
filterV2CommandSearchTreeItems(items, query),
|
||||
);
|
||||
});
|
||||
|
||||
it('prunes only cold collapsed database trees when too many object trees stay loaded', () => {
|
||||
expect(resolveSidebarDatabaseTreePruneKeys({
|
||||
treeData: [
|
||||
{
|
||||
key: 'conn-1',
|
||||
title: 'conn-1',
|
||||
type: 'connection',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-db-a',
|
||||
title: 'db-a',
|
||||
type: 'database',
|
||||
children: [{ key: 'a-tables', title: '表', type: 'object-group' }],
|
||||
},
|
||||
{
|
||||
key: 'conn-1-db-b',
|
||||
title: 'db-b',
|
||||
type: 'database',
|
||||
children: [{ key: 'b-tables', title: '表', type: 'object-group' }],
|
||||
},
|
||||
{
|
||||
key: 'conn-1-db-c',
|
||||
title: 'db-c',
|
||||
type: 'database',
|
||||
children: [{ key: 'c-tables', title: '表', type: 'object-group' }],
|
||||
},
|
||||
{
|
||||
key: 'conn-1-db-d',
|
||||
title: 'db-d',
|
||||
type: 'database',
|
||||
children: [{ key: 'd-tables', title: '表', type: 'object-group' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
expandedKeys: ['conn-1-db-c'],
|
||||
selectedKeys: [],
|
||||
activeDatabaseKey: 'conn-1-db-d',
|
||||
touchedAtByDatabaseKey: {
|
||||
'conn-1-db-a': 10,
|
||||
'conn-1-db-b': 20,
|
||||
'conn-1-db-c': 30,
|
||||
'conn-1-db-d': 40,
|
||||
},
|
||||
maxLoadedDatabases: 2,
|
||||
})).toEqual(['conn-1-db-a', 'conn-1-db-b']);
|
||||
});
|
||||
});
|
||||
@@ -415,6 +415,13 @@ export type V2CommandSearchItem =
|
||||
dbName?: string;
|
||||
};
|
||||
|
||||
export interface V2CommandSearchTreeIndexEntry {
|
||||
item: Extract<V2CommandSearchItem, { kind: 'node' }>;
|
||||
normalizedSearchText: string;
|
||||
normalizedObjectText: string;
|
||||
objectNode: boolean;
|
||||
}
|
||||
|
||||
export type V2CommandSearchMode = 'default' | 'object' | 'ai';
|
||||
|
||||
export interface V2CommandSearchQuery {
|
||||
@@ -467,40 +474,69 @@ const isV2CommandSearchObjectNode = (node: SidebarTreeNode): boolean => {
|
||||
|| node.type === 'materialized-view';
|
||||
};
|
||||
|
||||
const V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT = 24;
|
||||
export const V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT = 24;
|
||||
export const V2_COMMAND_SEARCH_MAX_TREE_RESULTS = 120;
|
||||
|
||||
export const buildV2CommandSearchTreeIndex = (
|
||||
items: V2CommandSearchItem[],
|
||||
): V2CommandSearchTreeIndexEntry[] => {
|
||||
return items.flatMap((item) => {
|
||||
if (item.kind !== 'node') {
|
||||
return [];
|
||||
}
|
||||
const dataRef = item.node.dataRef || {};
|
||||
const normalizedTitle = String(item.title || '').toLowerCase();
|
||||
const normalizedPrimaryObjectText = String(
|
||||
dataRef.tableName || dataRef.viewName || item.title || '',
|
||||
).toLowerCase();
|
||||
|
||||
return [{
|
||||
item,
|
||||
normalizedSearchText: [
|
||||
item.title,
|
||||
item.meta,
|
||||
dataRef.tableName,
|
||||
dataRef.viewName,
|
||||
dataRef.dbName,
|
||||
dataRef.name,
|
||||
dataRef.config?.host,
|
||||
].filter(Boolean).join(' ').toLowerCase(),
|
||||
normalizedObjectText: `${normalizedPrimaryObjectText} ${normalizedTitle}`.trim(),
|
||||
objectNode: isV2CommandSearchObjectNode(item.node),
|
||||
}];
|
||||
});
|
||||
};
|
||||
|
||||
export const filterV2CommandSearchTreeItems = (
|
||||
items: V2CommandSearchItem[],
|
||||
items: V2CommandSearchItem[] | V2CommandSearchTreeIndexEntry[],
|
||||
query: V2CommandSearchQuery,
|
||||
): V2CommandSearchItem[] => {
|
||||
if (query.mode === 'ai') return [];
|
||||
const index = items.length > 0 && 'item' in items[0]
|
||||
? items as V2CommandSearchTreeIndexEntry[]
|
||||
: buildV2CommandSearchTreeIndex(items as V2CommandSearchItem[]);
|
||||
const normalizedKeyword = query.normalizedKeyword;
|
||||
const objectMode = query.mode === 'object';
|
||||
const matchedItems = items.filter((item) => {
|
||||
if (item.kind !== 'node') return false;
|
||||
const node = item.node;
|
||||
const dataRef = node.dataRef || {};
|
||||
if (objectMode && !isV2CommandSearchObjectNode(node)) {
|
||||
return false;
|
||||
const result: V2CommandSearchItem[] = [];
|
||||
const maxResults = normalizedKeyword
|
||||
? V2_COMMAND_SEARCH_MAX_TREE_RESULTS
|
||||
: V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT;
|
||||
|
||||
for (const entry of index) {
|
||||
if (objectMode && !entry.objectNode) {
|
||||
continue;
|
||||
}
|
||||
if (!normalizedKeyword) return true;
|
||||
const objectName = String(dataRef.tableName || dataRef.viewName || item.title || '').toLowerCase();
|
||||
if (objectMode) {
|
||||
return objectName.includes(normalizedKeyword)
|
||||
|| String(item.title || '').toLowerCase().includes(normalizedKeyword);
|
||||
if (!normalizedKeyword) {
|
||||
result.push(entry.item);
|
||||
} else if (objectMode ? entry.normalizedObjectText.includes(normalizedKeyword) : entry.normalizedSearchText.includes(normalizedKeyword)) {
|
||||
result.push(entry.item);
|
||||
}
|
||||
const haystack = [
|
||||
item.title,
|
||||
item.meta,
|
||||
dataRef.tableName,
|
||||
dataRef.viewName,
|
||||
dataRef.dbName,
|
||||
dataRef.name,
|
||||
dataRef.config?.host,
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
return haystack.includes(normalizedKeyword);
|
||||
});
|
||||
return normalizedKeyword ? matchedItems : matchedItems.slice(0, V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT);
|
||||
if (result.length >= maxResults) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export interface V2CommandSearchEnterState {
|
||||
@@ -765,4 +801,63 @@ export const resolveV2ActiveConnectionId = ({
|
||||
|| '';
|
||||
};
|
||||
|
||||
export const resolveSidebarDatabaseTreePruneKeys = ({
|
||||
treeData,
|
||||
expandedKeys,
|
||||
selectedKeys,
|
||||
activeDatabaseKey,
|
||||
touchedAtByDatabaseKey,
|
||||
maxLoadedDatabases,
|
||||
}: {
|
||||
treeData: SidebarTreeNode[];
|
||||
expandedKeys: React.Key[];
|
||||
selectedKeys: React.Key[];
|
||||
activeDatabaseKey?: string;
|
||||
touchedAtByDatabaseKey?: Record<string, number>;
|
||||
maxLoadedDatabases: number;
|
||||
}): string[] => {
|
||||
if (!Number.isFinite(maxLoadedDatabases) || maxLoadedDatabases <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const loadedDatabaseKeys: string[] = [];
|
||||
const visit = (nodes: SidebarTreeNode[]) => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.type === 'database' && Array.isArray(node.children) && node.children.length > 0) {
|
||||
loadedDatabaseKeys.push(String(node.key || '').trim());
|
||||
return;
|
||||
}
|
||||
if (node.children?.length) {
|
||||
visit(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
visit(treeData);
|
||||
|
||||
if (loadedDatabaseKeys.length <= maxLoadedDatabases) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const expandedKeySet = new Set(expandedKeys.map((key) => String(key || '').trim()).filter(Boolean));
|
||||
const selectedKeySet = new Set(selectedKeys.map((key) => String(key || '').trim()).filter(Boolean));
|
||||
const protectedDatabaseKeys = new Set<string>();
|
||||
if (activeDatabaseKey) {
|
||||
protectedDatabaseKeys.add(String(activeDatabaseKey).trim());
|
||||
}
|
||||
|
||||
const candidates = loadedDatabaseKeys
|
||||
.filter((key) => key && !expandedKeySet.has(key) && !selectedKeySet.has(key) && !protectedDatabaseKeys.has(key))
|
||||
.sort((left, right) => {
|
||||
const leftTouchedAt = Number(touchedAtByDatabaseKey?.[left] || 0);
|
||||
const rightTouchedAt = Number(touchedAtByDatabaseKey?.[right] || 0);
|
||||
if (leftTouchedAt !== rightTouchedAt) {
|
||||
return leftTouchedAt - rightTouchedAt;
|
||||
}
|
||||
return left.localeCompare(right);
|
||||
});
|
||||
|
||||
const pruneCount = loadedDatabaseKeys.length - maxLoadedDatabases;
|
||||
return candidates.slice(0, pruneCount);
|
||||
};
|
||||
|
||||
export const shouldClearSidebarActiveContextOnEmptySelect = (isV2Ui: boolean): boolean => !isV2Ui;
|
||||
|
||||
@@ -1253,33 +1253,80 @@ describe('store appearance persistence', () => {
|
||||
expect(useStore.getState().activeTabId).toBe('query-1');
|
||||
});
|
||||
|
||||
it('persists recent SQL execution logs and trims oversized entries', async () => {
|
||||
it('keeps only the most recent runtime SQL logs and trims oversized entries', async () => {
|
||||
const { useStore } = await importStore();
|
||||
const longSql = `select '${'x'.repeat(120 * 1024)}'`;
|
||||
const longSql = `select '${'x'.repeat(20 * 1024)}'`;
|
||||
|
||||
useStore.getState().addSqlLog({
|
||||
id: 'log-1',
|
||||
timestamp: 100,
|
||||
sql: longSql,
|
||||
status: 'success',
|
||||
duration: 12,
|
||||
for (let i = 0; i < 140; i += 1) {
|
||||
useStore.getState().addSqlLog({
|
||||
id: `log-${i}`,
|
||||
timestamp: 100 + i,
|
||||
sql: longSql,
|
||||
status: 'success',
|
||||
duration: 12 + i,
|
||||
dbName: 'main',
|
||||
});
|
||||
}
|
||||
|
||||
expect(useStore.getState().sqlLogs).toHaveLength(120);
|
||||
expect(useStore.getState().sqlLogs[0]).toEqual(expect.objectContaining({
|
||||
id: 'log-139',
|
||||
dbName: 'main',
|
||||
});
|
||||
}));
|
||||
expect(useStore.getState().sqlLogs[119]).toEqual(expect.objectContaining({
|
||||
id: 'log-20',
|
||||
}));
|
||||
expect(useStore.getState().sqlLogs[0]?.sql.length).toBe(12 * 1024);
|
||||
|
||||
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
|
||||
expect(persisted.state.sqlLogs).toHaveLength(1);
|
||||
expect(persisted.state.sqlLogs[0].sql.length).toBe(100 * 1024);
|
||||
expect(persisted.state.sqlLogs).toHaveLength(120);
|
||||
expect(persisted.state.sqlLogs[0].sql.length).toBe(12 * 1024);
|
||||
expect(persisted.state.sqlLogs[0].dbName).toBe('main');
|
||||
|
||||
vi.resetModules();
|
||||
const reloaded = await importStore();
|
||||
expect(reloaded.useStore.getState().sqlLogs[0]).toEqual(expect.objectContaining({
|
||||
id: 'log-1',
|
||||
id: 'log-139',
|
||||
status: 'success',
|
||||
duration: 12,
|
||||
duration: 151,
|
||||
dbName: 'main',
|
||||
}));
|
||||
expect(reloaded.useStore.getState().sqlLogs[0]?.sql.length).toBe(100 * 1024);
|
||||
expect(reloaded.useStore.getState().sqlLogs).toHaveLength(120);
|
||||
expect(reloaded.useStore.getState().sqlLogs[119]).toEqual(expect.objectContaining({
|
||||
id: 'log-20',
|
||||
}));
|
||||
expect(reloaded.useStore.getState().sqlLogs[0]?.sql.length).toBe(12 * 1024);
|
||||
});
|
||||
|
||||
it('shrinks oversized SQL logs from older persisted snapshots during hydration', async () => {
|
||||
storage.setItem('lite-db-storage', JSON.stringify({
|
||||
state: {
|
||||
sqlLogs: Array.from({ length: 200 }, (_, index) => ({
|
||||
id: `legacy-log-${index}`,
|
||||
timestamp: 500 + index,
|
||||
sql: `select '${'x'.repeat(18 * 1024)}'`,
|
||||
status: index % 2 === 0 ? 'success' : 'error',
|
||||
duration: index,
|
||||
dbName: 'legacy',
|
||||
message: 'm'.repeat(3 * 1024),
|
||||
})),
|
||||
},
|
||||
version: 12,
|
||||
}));
|
||||
|
||||
const { useStore } = await importStore();
|
||||
const sqlLogs = useStore.getState().sqlLogs;
|
||||
|
||||
expect(sqlLogs).toHaveLength(120);
|
||||
expect(sqlLogs[0]).toEqual(expect.objectContaining({
|
||||
id: 'legacy-log-0',
|
||||
dbName: 'legacy',
|
||||
}));
|
||||
expect(sqlLogs[119]).toEqual(expect.objectContaining({
|
||||
id: 'legacy-log-119',
|
||||
}));
|
||||
expect(sqlLogs[0]?.sql.length).toBe(12 * 1024);
|
||||
expect(sqlLogs[0]?.message?.length).toBe(1024);
|
||||
});
|
||||
|
||||
it('defaults AI chat send shortcut to Enter in shared shortcut options', async () => {
|
||||
|
||||
@@ -137,14 +137,16 @@ const MIN_KEEPALIVE_INTERVAL_MINUTES = 1;
|
||||
const MAX_KEEPALIVE_INTERVAL_MINUTES = 1440;
|
||||
const DEFAULT_DIAGNOSTIC_TIMEOUT_SECONDS = 15;
|
||||
const MAX_DIAGNOSTIC_TIMEOUT_SECONDS = 300;
|
||||
const PERSIST_VERSION = 12;
|
||||
const PERSIST_VERSION = 13;
|
||||
const PERSIST_STORAGE_KEY = "lite-db-storage";
|
||||
const PERSIST_WRITE_DEBOUNCE_MS = 160;
|
||||
const MAX_PERSISTED_QUERY_TABS = 20;
|
||||
const MAX_PERSISTED_QUERY_LENGTH = 1024 * 1024;
|
||||
const MAX_SQL_LOGS = 1000;
|
||||
const MAX_RUNTIME_SQL_LOGS = 120;
|
||||
const MAX_RUNTIME_SQL_LOG_LENGTH = 12 * 1024;
|
||||
const MAX_RUNTIME_SQL_LOG_MESSAGE_LENGTH = 1024;
|
||||
const MAX_PERSISTED_SQL_LOGS = 200;
|
||||
const MAX_PERSISTED_SQL_LOG_LENGTH = 100 * 1024;
|
||||
const MAX_PERSISTED_SQL_LOG_LENGTH = 24 * 1024;
|
||||
const MAX_PERSISTED_SQL_LOG_MESSAGE_LENGTH = 2 * 1024;
|
||||
const MAX_TABLE_EXPORT_HISTORY_PER_TARGET = 20;
|
||||
const MAX_TABLE_EXPORT_HISTORY_TARGETS = 200;
|
||||
@@ -1708,50 +1710,101 @@ const resolveActiveContextForTabId = (
|
||||
return fallbackContext;
|
||||
};
|
||||
|
||||
const sanitizeSqlLogs = (value: unknown, limit = MAX_PERSISTED_SQL_LOGS): SqlLog[] => {
|
||||
type SqlLogSanitizeOptions = {
|
||||
limit: number;
|
||||
sqlLength: number;
|
||||
messageLength: number;
|
||||
};
|
||||
|
||||
const RUNTIME_SQL_LOG_SANITIZE_OPTIONS: SqlLogSanitizeOptions = {
|
||||
limit: MAX_RUNTIME_SQL_LOGS,
|
||||
sqlLength: MAX_RUNTIME_SQL_LOG_LENGTH,
|
||||
messageLength: MAX_RUNTIME_SQL_LOG_MESSAGE_LENGTH,
|
||||
};
|
||||
|
||||
const PERSISTED_SQL_LOG_SANITIZE_OPTIONS: SqlLogSanitizeOptions = {
|
||||
limit: MAX_PERSISTED_SQL_LOGS,
|
||||
sqlLength: MAX_PERSISTED_SQL_LOG_LENGTH,
|
||||
messageLength: MAX_PERSISTED_SQL_LOG_MESSAGE_LENGTH,
|
||||
};
|
||||
|
||||
const sanitizeSqlLogEntry = (
|
||||
entry: unknown,
|
||||
index: number,
|
||||
options: SqlLogSanitizeOptions,
|
||||
): SqlLog | null => {
|
||||
if (!entry || typeof entry !== "object") return null;
|
||||
const raw = entry as Record<string, unknown>;
|
||||
const sql = typeof raw.sql === "string" ? raw.sql.slice(0, options.sqlLength) : "";
|
||||
if (!sql.trim()) return null;
|
||||
|
||||
const status = raw.status === "error" ? "error" : "success";
|
||||
const timestamp = Number(raw.timestamp);
|
||||
const duration = Number(raw.duration);
|
||||
const affectedRows = Number(raw.affectedRows);
|
||||
const message = typeof raw.message === "string"
|
||||
? raw.message.slice(0, options.messageLength)
|
||||
: "";
|
||||
|
||||
const log: SqlLog = {
|
||||
id: toTrimmedString(raw.id, `log-${index + 1}`) || `log-${index + 1}`,
|
||||
timestamp: Number.isFinite(timestamp) && timestamp > 0 ? timestamp : Date.now(),
|
||||
sql,
|
||||
status,
|
||||
duration: Number.isFinite(duration) && duration >= 0 ? duration : 0,
|
||||
dbName: toTrimmedString(raw.dbName) || undefined,
|
||||
};
|
||||
|
||||
if (message) {
|
||||
log.message = message;
|
||||
}
|
||||
if (Number.isFinite(affectedRows)) {
|
||||
log.affectedRows = affectedRows;
|
||||
}
|
||||
|
||||
return log;
|
||||
};
|
||||
|
||||
const sanitizeSqlLogs = (
|
||||
value: unknown,
|
||||
options: SqlLogSanitizeOptions = PERSISTED_SQL_LOG_SANITIZE_OPTIONS,
|
||||
): SqlLog[] => {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const result: SqlLog[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
|
||||
value.forEach((entry, index) => {
|
||||
if (!entry || typeof entry !== "object") return;
|
||||
const raw = entry as Record<string, unknown>;
|
||||
const sql = typeof raw.sql === "string" ? raw.sql.slice(0, MAX_PERSISTED_SQL_LOG_LENGTH) : "";
|
||||
if (!sql.trim()) return;
|
||||
const log = sanitizeSqlLogEntry(entry, index, options);
|
||||
if (!log) return;
|
||||
|
||||
let id = toTrimmedString(raw.id, `log-${index + 1}`) || `log-${index + 1}`;
|
||||
let id = log.id;
|
||||
if (seenIds.has(id)) {
|
||||
id = `${id}-${index + 1}`;
|
||||
}
|
||||
seenIds.add(id);
|
||||
|
||||
const status = raw.status === "error" ? "error" : "success";
|
||||
const timestamp = Number(raw.timestamp);
|
||||
const duration = Number(raw.duration);
|
||||
const affectedRows = Number(raw.affectedRows);
|
||||
const log: SqlLog = {
|
||||
id,
|
||||
timestamp: Number.isFinite(timestamp) && timestamp > 0 ? timestamp : Date.now(),
|
||||
sql,
|
||||
status,
|
||||
duration: Number.isFinite(duration) && duration >= 0 ? duration : 0,
|
||||
dbName: toTrimmedString(raw.dbName) || undefined,
|
||||
};
|
||||
|
||||
const message = typeof raw.message === "string"
|
||||
? raw.message.slice(0, MAX_PERSISTED_SQL_LOG_MESSAGE_LENGTH)
|
||||
: "";
|
||||
if (message) {
|
||||
log.message = message;
|
||||
}
|
||||
if (Number.isFinite(affectedRows)) {
|
||||
log.affectedRows = affectedRows;
|
||||
}
|
||||
|
||||
result.push(log);
|
||||
result.push(id === log.id ? log : { ...log, id });
|
||||
});
|
||||
|
||||
return result.slice(0, limit);
|
||||
return result.slice(0, options.limit);
|
||||
};
|
||||
|
||||
const sanitizeRuntimeSqlLogs = (value: unknown) =>
|
||||
sanitizeSqlLogs(value, RUNTIME_SQL_LOG_SANITIZE_OPTIONS);
|
||||
|
||||
const sanitizePersistedSqlLogs = (value: unknown) =>
|
||||
sanitizeSqlLogs(value, PERSISTED_SQL_LOG_SANITIZE_OPTIONS);
|
||||
|
||||
const appendRuntimeSqlLog = (existing: SqlLog[], entry: SqlLog): SqlLog[] => {
|
||||
const nextEntry = sanitizeSqlLogEntry(entry, 0, RUNTIME_SQL_LOG_SANITIZE_OPTIONS);
|
||||
if (!nextEntry) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const nextLogs = [nextEntry, ...existing.slice(0, MAX_RUNTIME_SQL_LOGS - 1)];
|
||||
return existing.some((item) => item.id === nextEntry.id)
|
||||
? sanitizeRuntimeSqlLogs(nextLogs)
|
||||
: nextLogs;
|
||||
};
|
||||
|
||||
const hasLegacyConnectionSecrets = (
|
||||
@@ -3155,7 +3208,7 @@ export const useStore = create<AppState>()(
|
||||
}),
|
||||
|
||||
addSqlLog: (log) =>
|
||||
set((state) => ({ sqlLogs: sanitizeSqlLogs([log, ...state.sqlLogs], MAX_SQL_LOGS) })),
|
||||
set((state) => ({ sqlLogs: appendRuntimeSqlLog(state.sqlLogs, log) })),
|
||||
clearSqlLogs: () => set({ sqlLogs: [] }),
|
||||
upsertTableExportHistory: (historyKey, entry) =>
|
||||
set((state) => {
|
||||
@@ -3552,7 +3605,7 @@ export const useStore = create<AppState>()(
|
||||
nextState.shortcutOptions = sanitizeShortcutOptions(
|
||||
state.shortcutOptions,
|
||||
);
|
||||
nextState.sqlLogs = sanitizeSqlLogs(state.sqlLogs);
|
||||
nextState.sqlLogs = sanitizeRuntimeSqlLogs(state.sqlLogs);
|
||||
nextState.tableExportHistories = sanitizeTableExportHistories(
|
||||
state.tableExportHistories,
|
||||
);
|
||||
@@ -3665,7 +3718,7 @@ export const useStore = create<AppState>()(
|
||||
state.sqlEditorTransactionOptions,
|
||||
),
|
||||
shortcutOptions: sanitizeShortcutOptions(state.shortcutOptions),
|
||||
sqlLogs: sanitizeSqlLogs(state.sqlLogs),
|
||||
sqlLogs: sanitizeRuntimeSqlLogs(state.sqlLogs),
|
||||
sqlSnippets: sanitizeSqlSnippets(state.sqlSnippets),
|
||||
tableAccessCount: sanitizeTableAccessCount(state.tableAccessCount),
|
||||
|
||||
@@ -3697,7 +3750,7 @@ export const useStore = create<AppState>()(
|
||||
dataEditTransactionOptions: state.dataEditTransactionOptions,
|
||||
sqlEditorTransactionOptions: state.sqlEditorTransactionOptions,
|
||||
shortcutOptions: resolveShortcutOptionsForPersistence(state.shortcutOptions),
|
||||
sqlLogs: sanitizeSqlLogs(state.sqlLogs),
|
||||
sqlLogs: sanitizePersistedSqlLogs(state.sqlLogs),
|
||||
tableExportHistories: sanitizeTableExportHistories(
|
||||
state.tableExportHistories,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user