mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-02 03:38:16 +08:00
🐛 fix(connection-package): 导出/导入连接包携带 Redis DB 别名
- 连接包 payload 与导出选项支持 redisDbAliases(连接 ID → db 序号 → 别名) - 导入返回 ConnectionPackageImportResult,一并恢复别名映射 - 前端导出时注入 appearance.redisDbAliases,导入后 merge 回 appearance - 补充别名往返测试与 mergeRedisDbAliases 单元测试
This commit is contained in:
@@ -65,6 +65,11 @@ import {
|
||||
resolveConnectionPackageExportResult,
|
||||
normalizeConnectionPackagePassword,
|
||||
} from './utils/connectionExport';
|
||||
import {
|
||||
mergeRedisDbAliases,
|
||||
sanitizeRedisDbAliases,
|
||||
type RedisDbAliasMap,
|
||||
} from './utils/redisDbAlias';
|
||||
import {
|
||||
bootstrapSecureConfig,
|
||||
finalizeSecurityUpdateStatus,
|
||||
@@ -400,6 +405,32 @@ const mergeSavedConnections = (current: SavedConnection[], imported: SavedConnec
|
||||
return Array.from(merged.values());
|
||||
};
|
||||
|
||||
type ConnectionPackageImportPayload = {
|
||||
connections: SavedConnection[];
|
||||
redisDbAliases: RedisDbAliasMap;
|
||||
};
|
||||
|
||||
/** Normalize ImportConnectionsPayload results: object (new) or bare array (legacy/mock). */
|
||||
const normalizeConnectionPackageImportPayload = (value: unknown): ConnectionPackageImportPayload | null => {
|
||||
if (Array.isArray(value)) {
|
||||
return {
|
||||
connections: value as SavedConnection[],
|
||||
redisDbAliases: {},
|
||||
};
|
||||
}
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const record = value as { connections?: unknown; redisDbAliases?: unknown };
|
||||
if (!Array.isArray(record.connections)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
connections: record.connections as SavedConnection[],
|
||||
redisDbAliases: sanitizeRedisDbAliases(record.redisDbAliases),
|
||||
};
|
||||
};
|
||||
|
||||
type ConnectionPackageDialogMode = 'import' | 'export';
|
||||
type ToolCenterGroupKey = 'config' | 'workflow' | 'workspace';
|
||||
type ToolCenterPaneKey =
|
||||
@@ -2363,9 +2394,9 @@ function App() {
|
||||
throw new Error(t('app.connection_package.error.import_capability_unavailable'));
|
||||
}
|
||||
|
||||
let importedViews: unknown;
|
||||
let importedRaw: unknown;
|
||||
try {
|
||||
importedViews = await backendApp.ImportConnectionsPayload(raw, password);
|
||||
importedRaw = await backendApp.ImportConnectionsPayload(raw, password);
|
||||
} catch (error) {
|
||||
if (isConnectionPackagePasswordRequiredError(error)) {
|
||||
throw error;
|
||||
@@ -2377,11 +2408,19 @@ function App() {
|
||||
: t('app.connection_package.message.import_failed'),
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(importedViews)) {
|
||||
const imported = normalizeConnectionPackageImportPayload(importedRaw);
|
||||
if (!imported) {
|
||||
throw new Error(t('app.connection_package.error.import_no_connections'));
|
||||
}
|
||||
await refreshConnectionsAfterImport(importedViews as SavedConnection[]);
|
||||
return importedViews as SavedConnection[];
|
||||
await refreshConnectionsAfterImport(imported.connections);
|
||||
// Redis DB 别名存在前端 appearance,需随连接包一并恢复
|
||||
if (Object.keys(imported.redisDbAliases).length > 0) {
|
||||
const currentAliases = useStore.getState().appearance.redisDbAliases;
|
||||
useStore.getState().setAppearance({
|
||||
redisDbAliases: mergeRedisDbAliases(currentAliases, imported.redisDbAliases),
|
||||
});
|
||||
}
|
||||
return imported.connections;
|
||||
}, [refreshConnectionsAfterImport, t]);
|
||||
|
||||
const handleImportConnections = async (sourceGroup?: ToolCenterGroupKey) => {
|
||||
@@ -2502,6 +2541,8 @@ function App() {
|
||||
connectionPackageDialog.includeSecrets
|
||||
&& connectionPackageDialog.useFilePassword
|
||||
) ? password : '',
|
||||
// Redis DB 别名仅存前端,导出时注入连接包
|
||||
redisDbAliases: useStore.getState().appearance.redisDbAliases,
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error ?? '').trim();
|
||||
|
||||
@@ -420,14 +420,29 @@ if (
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map((item) => saveMockConnection(item));
|
||||
return {
|
||||
connections: parsed.map((item) => saveMockConnection(item)),
|
||||
redisDbAliases: {},
|
||||
};
|
||||
}
|
||||
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.connections)) {
|
||||
return {
|
||||
connections: parsed.connections.map((item: unknown) => saveMockConnection(item)),
|
||||
redisDbAliases: parsed.redisDbAliases && typeof parsed.redisDbAliases === 'object'
|
||||
? parsed.redisDbAliases
|
||||
: {},
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
throw new Error(t('app.browser_mock.import_connection_package_unsupported'));
|
||||
}
|
||||
throw new Error(t('app.browser_mock.import_connection_package_unsupported'));
|
||||
},
|
||||
ExportConnectionsPackage: async (_options?: { includeSecrets?: boolean; filePassword?: string }) => ({ success: false, message: t('app.browser_mock.export_connection_package_unsupported') }),
|
||||
ExportConnectionsPackage: async (_options?: {
|
||||
includeSecrets?: boolean;
|
||||
filePassword?: string;
|
||||
redisDbAliases?: Record<string, Record<string, string>>;
|
||||
}) => ({ success: false, message: t('app.browser_mock.export_connection_package_unsupported') }),
|
||||
ExportData: async () => ({ success: false }),
|
||||
GetGlobalProxyConfig: async () => ({ success: true, data: cloneBrowserMockValue(mockGlobalProxy) }),
|
||||
SetUpdateChannel: async (channel: string) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
MAX_REDIS_DB_ALIAS_LENGTH,
|
||||
buildRedisDbNodeLabel,
|
||||
getRedisDbAlias,
|
||||
mergeRedisDbAliases,
|
||||
sanitizeRedisDbAlias,
|
||||
sanitizeRedisDbAliases,
|
||||
setRedisDbAlias,
|
||||
@@ -76,4 +77,20 @@ describe('redisDbAlias helpers', () => {
|
||||
it('does not append key counts to the sidebar label', () => {
|
||||
expect(buildRedisDbNodeLabel(0, '12')).toBe('db0 12');
|
||||
});
|
||||
|
||||
it('merges imported aliases over local ones without dropping unrelated labels', () => {
|
||||
const current = {
|
||||
'conn-a': { '0': 'local-cache', '1': 'local-queue' },
|
||||
'conn-b': { '0': 'sessions' },
|
||||
};
|
||||
const incoming = {
|
||||
'conn-a': { '0': 'imported-cache' },
|
||||
'conn-c': { '2': 'metrics' },
|
||||
};
|
||||
expect(mergeRedisDbAliases(current, incoming)).toEqual({
|
||||
'conn-a': { '0': 'imported-cache', '1': 'local-queue' },
|
||||
'conn-b': { '0': 'sessions' },
|
||||
'conn-c': { '2': 'metrics' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,6 +126,26 @@ export const setRedisDbAlias = (
|
||||
return next;
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge imported Redis DB aliases into the local map. Imported labels win for
|
||||
* the same connection + DB index; unrelated local aliases are preserved.
|
||||
*/
|
||||
export const mergeRedisDbAliases = (
|
||||
current: RedisDbAliasMap | undefined,
|
||||
incoming: RedisDbAliasMap | undefined,
|
||||
): RedisDbAliasMap => {
|
||||
const base = sanitizeRedisDbAliases(current);
|
||||
const imported = sanitizeRedisDbAliases(incoming);
|
||||
const next: RedisDbAliasMap = { ...base };
|
||||
Object.entries(imported).forEach(([connectionId, aliases]) => {
|
||||
next[connectionId] = {
|
||||
...(base[connectionId] || {}),
|
||||
...aliases,
|
||||
};
|
||||
});
|
||||
return next;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the sidebar label for a Redis DB node. Returns `dbN` when there is no
|
||||
* alias, and `dbN alias` when one is set. Key counts are intentionally not
|
||||
|
||||
Reference in New Issue
Block a user