mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-12 12:10:47 +08:00
* 🎨 style(DataGrid): 清理冗余代码与静态分析告警 - 类型重构:通过修正 React Context 的函数签名解决了 void 类型的链式调用错误 - 代码精简:利用 Nullish Coalescing (??) 优化组件配置项降级逻辑,剥离无意义的隐式 undefined 赋值 - 工具链适配:适配 IDE 拼写检查与 Promise strict rules,确保全文件零警 * 🔧 fix(db/kingbase_impl): 修复标识符无条件加双引号导致SQL语法报错 - quoteKingbaseIdent 改为条件引用,仅对大写字母、保留字、特殊字符的标识符添加双引号 - 新增 kingbaseIdentNeedsQuote 判断标识符是否需要引用 - 新增 isKingbaseReservedWord 检测常见SQL保留字 - 补充 TestQuoteKingbaseIdent、TestKingbaseIdentNeedsQuote 单测覆盖各场景 - refs #176 * 🔧 fix(release,db/kingbase_impl): 修复金仓默认 schema 并静默生成 DMG - Kingbase:在 current_schema() 为 public 时探测候选 schema,并通过 DSN search_path 重连,兼容未限定 schema 的查询 - 候选优先级:数据库名/用户名同名 schema(存在性校验),否则仅在“唯一用户 schema 有表”场景兜底 - 避免连接污染:每次 Connect 重置探测结果,重连成功后替换连接并关闭旧连接 - 打包脚本:create-dmg 增加 --sandbox-safe,避免构建时自动弹出/打开挂载窗口 - 产物格式:强制 --format UDZO,并将 rw.*.dmg/UDRW 中间产物转换为可分发 DMG - 校验门禁:增加 hdiutil verify,失败时保留 .app 便于排查,同时修正卷图标探测并补 ad-hoc 签名 * 🐛 fix(connection/redis): 修复 Redis URI 用户名处理导致认证失败 - Redis URI 解析回填 user 字段,兼容 redis://user:pass@... 与 redis://:pass@... - 生成 URI 时按需输出 user/password,避免丢失用户名信息 - Redis 类型默认用户名置空,并在构建配置时清理历史默认 root - 避免 go-redis 触发 ACL AUTH(user, pass) 导致 WRONGPASS - refs #212 * 🔧 fix(release,ssh): 修复 SSH 误判连接成功并纠正 DMG 打包结构 - SSH 缓存 key 纳入认证指纹(password/keyPath),避免改错凭证仍复用旧连接/端口转发 - MySQL/MariaDB/Doris:SSH 隧道建立失败直接返回错误,不再回退直连导致测试误判成功 - 新增最小单测覆盖 SSH cache key 与 UseSSH 异常路径 - build-release.sh:create-dmg 使用 staging 目录作为 source,避免 DMG 根目录变成 Contents - refs #213 * fix: KingBase 连接后自动设置 search_path,修复自定义 schema 下表查询报 relation does not exist 的问题 (#215) * 🔧 fix(driver/kingbase,mongodb): 修复外置驱动事务引用与连接测试链路问题 - 金仓外置驱动链路增加表名与变更字段归一化,修复 ApplyChanges 场景下双引号转义异常导致的 SQL 语法错误 - 新增金仓公共标识符工具并复用到 kingbase_impl 与 optional_driver_agent_impl,统一处理多重转义、schema.table 拆分与引用规范 - 金仓代理连接后自动探测并设置 search_path,降低查询时必须手写 schema 前缀的概率 - MongoDB 连接参数改为显式 host/hosts 优先,避免被 URI 中 localhost 覆盖;代理链路保留目标地址不再改写为本地地址 - 连接测试增加前后端超时收敛与日志增强,避免长时间转圈;连接错误文案在未启用 TLS 时移除误导性的“SSL”前缀 - 统一日志级别为 INFO/WARN/ERROR,默认日志目录收敛到 ~/.GoNavi/Logs,并补充驱动构建脚本 build-driver-agents.sh * 🔧 fix(release/sidebar): 统一跨平台UPX压缩并修复PG函数列表查询兼容性 - 构建脚本新增通用 UPX 压缩函数,覆盖 macOS、Linux、Windows 产物 - 本地打包改为强制压缩策略:未安装 upx、压缩失败或校验失败直接终止 - macOS 打包在签名前压缩 .app 主程序并执行 upx -t 校验 - Linux 打包在生成 tar.gz 前压缩可执行文件并执行 upx -t 校验 - GitHub Release 与测试构建流程补齐 macOS/Linux/Windows 的 upx 安装与压缩步骤 - PostgreSQL/PG-like 函数元数据查询增加多路兼容 SQL,修复函数列表不显示问题 - refs #221 - refs #222 --------- Co-authored-by: Syngnat <yangguofeng919@gmail.com> Co-authored-by: 凌封 <49424247+fengin@users.noreply.github.com>
961 lines
37 KiB
TypeScript
961 lines
37 KiB
TypeScript
import { create } from 'zustand';
|
||
import { persist } from 'zustand/middleware';
|
||
import { ConnectionConfig, ProxyConfig, SavedConnection, TabData, SavedQuery, ConnectionTag } from './types';
|
||
import {
|
||
ShortcutAction,
|
||
ShortcutBinding,
|
||
ShortcutOptions,
|
||
DEFAULT_SHORTCUT_OPTIONS,
|
||
cloneShortcutOptions,
|
||
sanitizeShortcutOptions,
|
||
} from './utils/shortcuts';
|
||
|
||
const DEFAULT_APPEARANCE = { enabled: true, opacity: 1.0, blur: 0 };
|
||
const DEFAULT_UI_SCALE = 1.0;
|
||
const MIN_UI_SCALE = 0.8;
|
||
const MAX_UI_SCALE = 1.25;
|
||
const DEFAULT_FONT_SIZE = 14;
|
||
const MIN_FONT_SIZE = 12;
|
||
const MAX_FONT_SIZE = 20;
|
||
const DEFAULT_STARTUP_FULLSCREEN = false;
|
||
const LEGACY_DEFAULT_OPACITY = 0.95;
|
||
const OPACITY_EPSILON = 1e-6;
|
||
const MAX_URI_LENGTH = 4096;
|
||
const MAX_HOST_ENTRY_LENGTH = 512;
|
||
const MAX_HOST_ENTRIES = 64;
|
||
const DEFAULT_TIMEOUT_SECONDS = 30;
|
||
const MAX_TIMEOUT_SECONDS = 3600;
|
||
const PERSIST_VERSION = 6;
|
||
const DEFAULT_CONNECTION_TYPE = 'mysql';
|
||
const DEFAULT_GLOBAL_PROXY: GlobalProxyConfig = {
|
||
enabled: false,
|
||
type: 'socks5',
|
||
host: '',
|
||
port: 1080,
|
||
user: '',
|
||
password: '',
|
||
};
|
||
const SUPPORTED_CONNECTION_TYPES = new Set([
|
||
'mysql',
|
||
'mariadb',
|
||
'doris',
|
||
'diros',
|
||
'sphinx',
|
||
'clickhouse',
|
||
'postgres',
|
||
'redis',
|
||
'tdengine',
|
||
'oracle',
|
||
'dameng',
|
||
'kingbase',
|
||
'sqlserver',
|
||
'mongodb',
|
||
'highgo',
|
||
'vastbase',
|
||
'sqlite',
|
||
'duckdb',
|
||
'custom',
|
||
]);
|
||
const SSL_SUPPORTED_CONNECTION_TYPES = new Set([
|
||
'mysql',
|
||
'mariadb',
|
||
'diros',
|
||
'sphinx',
|
||
'dameng',
|
||
'clickhouse',
|
||
'postgres',
|
||
'sqlserver',
|
||
'oracle',
|
||
'kingbase',
|
||
'highgo',
|
||
'vastbase',
|
||
'mongodb',
|
||
'redis',
|
||
'tdengine',
|
||
]);
|
||
|
||
const getDefaultPortByType = (type: string): number => {
|
||
switch (type) {
|
||
case 'mysql':
|
||
case 'mariadb':
|
||
return 3306;
|
||
case 'doris':
|
||
case 'diros':
|
||
return 9030;
|
||
case 'duckdb':
|
||
return 0;
|
||
case 'sphinx':
|
||
return 9306;
|
||
case 'clickhouse':
|
||
return 9000;
|
||
case 'postgres':
|
||
case 'vastbase':
|
||
return 5432;
|
||
case 'redis':
|
||
return 6379;
|
||
case 'tdengine':
|
||
return 6041;
|
||
case 'oracle':
|
||
return 1521;
|
||
case 'dameng':
|
||
return 5236;
|
||
case 'kingbase':
|
||
return 54321;
|
||
case 'sqlserver':
|
||
return 1433;
|
||
case 'mongodb':
|
||
return 27017;
|
||
case 'highgo':
|
||
return 5866;
|
||
default:
|
||
return 3306;
|
||
}
|
||
};
|
||
|
||
const toTrimmedString = (value: unknown, fallback = ''): string => {
|
||
if (typeof value === 'string') {
|
||
return value.trim();
|
||
}
|
||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||
return String(value).trim();
|
||
}
|
||
return fallback;
|
||
};
|
||
|
||
const normalizePort = (value: unknown, fallbackPort: number): number => {
|
||
const parsed = Number(value);
|
||
if (!Number.isFinite(parsed)) return fallbackPort;
|
||
const port = Math.trunc(parsed);
|
||
if (port <= 0 || port > 65535) return fallbackPort;
|
||
return port;
|
||
};
|
||
|
||
const normalizeIntegerInRange = (value: unknown, fallbackValue: number, min: number, max: number): number => {
|
||
const parsed = Number(value);
|
||
if (!Number.isFinite(parsed)) return fallbackValue;
|
||
const normalized = Math.trunc(parsed);
|
||
if (normalized < min || normalized > max) return fallbackValue;
|
||
return normalized;
|
||
};
|
||
|
||
const normalizeFloatInRange = (value: unknown, fallbackValue: number, min: number, max: number): number => {
|
||
const parsed = Number(value);
|
||
if (!Number.isFinite(parsed)) return fallbackValue;
|
||
if (parsed < min || parsed > max) return fallbackValue;
|
||
return parsed;
|
||
};
|
||
|
||
const isValidHostEntry = (entry: string): boolean => {
|
||
if (!entry) return false;
|
||
if (entry.length > MAX_HOST_ENTRY_LENGTH) return false;
|
||
if (/[()\\/\s]/.test(entry)) return false;
|
||
return true;
|
||
};
|
||
|
||
const sanitizeStringArray = (value: unknown, maxLength = 256): string[] => {
|
||
if (!Array.isArray(value)) return [];
|
||
const seen = new Set<string>();
|
||
const result: string[] = [];
|
||
value.forEach((entry) => {
|
||
const normalized = toTrimmedString(entry);
|
||
if (!normalized || normalized.length > maxLength) return;
|
||
if (seen.has(normalized)) return;
|
||
seen.add(normalized);
|
||
result.push(normalized);
|
||
});
|
||
return result;
|
||
};
|
||
|
||
const sanitizeNumberArray = (value: unknown, min: number, max: number): number[] => {
|
||
if (!Array.isArray(value)) return [];
|
||
const seen = new Set<number>();
|
||
const result: number[] = [];
|
||
value.forEach((entry) => {
|
||
const parsed = Number(entry);
|
||
if (!Number.isFinite(parsed)) return;
|
||
const num = Math.trunc(parsed);
|
||
if (num < min || num > max) return;
|
||
if (seen.has(num)) return;
|
||
seen.add(num);
|
||
result.push(num);
|
||
});
|
||
return result;
|
||
};
|
||
|
||
const sanitizeAddressList = (value: unknown): string[] => {
|
||
const all = sanitizeStringArray(value, MAX_HOST_ENTRY_LENGTH)
|
||
.filter((entry) => isValidHostEntry(entry));
|
||
return all.slice(0, MAX_HOST_ENTRIES);
|
||
};
|
||
|
||
const normalizeConnectionType = (value: unknown): string => {
|
||
const type = toTrimmedString(value).toLowerCase();
|
||
if (type === 'doris') {
|
||
return 'diros';
|
||
}
|
||
return SUPPORTED_CONNECTION_TYPES.has(type) ? type : DEFAULT_CONNECTION_TYPE;
|
||
};
|
||
|
||
const sanitizeConnectionConfig = (value: unknown): ConnectionConfig => {
|
||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||
const type = normalizeConnectionType(raw.type);
|
||
const defaultPort = getDefaultPortByType(type);
|
||
const savePassword = typeof raw.savePassword === 'boolean' ? raw.savePassword : true;
|
||
const mongoSrv = !!raw.mongoSrv;
|
||
const sslCapable = SSL_SUPPORTED_CONNECTION_TYPES.has(type);
|
||
const sslModeRaw = toTrimmedString(raw.sslMode, 'preferred').toLowerCase();
|
||
const sslMode: 'preferred' | 'required' | 'skip-verify' | 'disable' =
|
||
sslModeRaw === 'required'
|
||
? 'required'
|
||
: sslModeRaw === 'skip-verify'
|
||
? 'skip-verify'
|
||
: sslModeRaw === 'disable'
|
||
? 'disable'
|
||
: 'preferred';
|
||
|
||
const sshRaw = (raw.ssh && typeof raw.ssh === 'object') ? raw.ssh as Record<string, unknown> : {};
|
||
const ssh = {
|
||
host: toTrimmedString(sshRaw.host),
|
||
port: normalizePort(sshRaw.port, 22),
|
||
user: toTrimmedString(sshRaw.user),
|
||
password: toTrimmedString(sshRaw.password),
|
||
keyPath: toTrimmedString(sshRaw.keyPath),
|
||
};
|
||
const proxyRaw = (raw.proxy && typeof raw.proxy === 'object') ? raw.proxy as Record<string, unknown> : {};
|
||
const proxyTypeRaw = toTrimmedString(proxyRaw.type, 'socks5').toLowerCase();
|
||
const proxyType: 'socks5' | 'http' = proxyTypeRaw === 'http' ? 'http' : 'socks5';
|
||
const proxy = {
|
||
type: proxyType,
|
||
host: toTrimmedString(proxyRaw.host),
|
||
port: normalizePort(proxyRaw.port, proxyTypeRaw === 'http' ? 8080 : 1080),
|
||
user: toTrimmedString(proxyRaw.user),
|
||
password: toTrimmedString(proxyRaw.password),
|
||
};
|
||
const httpTunnelRaw = (raw.httpTunnel && typeof raw.httpTunnel === 'object')
|
||
? raw.httpTunnel as Record<string, unknown>
|
||
: ((raw.HTTPTunnel && typeof raw.HTTPTunnel === 'object') ? raw.HTTPTunnel as Record<string, unknown> : {});
|
||
const httpTunnel = {
|
||
host: toTrimmedString(httpTunnelRaw.host ?? raw.httpTunnelHost),
|
||
port: normalizePort(httpTunnelRaw.port ?? raw.httpTunnelPort, 8080),
|
||
user: toTrimmedString(httpTunnelRaw.user ?? raw.httpTunnelUser),
|
||
password: toTrimmedString(httpTunnelRaw.password ?? raw.httpTunnelPassword),
|
||
};
|
||
const supportsNetworkTunnel = type !== 'sqlite' && type !== 'duckdb';
|
||
const useHttpTunnel = supportsNetworkTunnel && (raw.useHttpTunnel === true || raw.UseHTTPTunnel === true);
|
||
const useProxy = supportsNetworkTunnel && !!raw.useProxy && !useHttpTunnel;
|
||
|
||
const safeConfig: ConnectionConfig & Record<string, unknown> = {
|
||
...raw,
|
||
type,
|
||
host: toTrimmedString(raw.host, 'localhost') || 'localhost',
|
||
port: normalizePort(raw.port, defaultPort),
|
||
user: toTrimmedString(raw.user),
|
||
password: savePassword ? toTrimmedString(raw.password) : '',
|
||
savePassword,
|
||
database: toTrimmedString(raw.database),
|
||
useSSL: sslCapable ? !!raw.useSSL : false,
|
||
sslMode: sslCapable ? sslMode : 'disable',
|
||
sslCertPath: sslCapable ? toTrimmedString(raw.sslCertPath) : '',
|
||
sslKeyPath: sslCapable ? toTrimmedString(raw.sslKeyPath) : '',
|
||
useSSH: !!raw.useSSH,
|
||
ssh,
|
||
useProxy,
|
||
proxy,
|
||
useHttpTunnel,
|
||
httpTunnel,
|
||
uri: toTrimmedString(raw.uri).slice(0, MAX_URI_LENGTH),
|
||
hosts: sanitizeAddressList(raw.hosts),
|
||
topology: raw.topology === 'replica' ? 'replica' : (raw.topology === 'cluster' ? 'cluster' : 'single'),
|
||
mysqlReplicaUser: toTrimmedString(raw.mysqlReplicaUser),
|
||
mysqlReplicaPassword: savePassword ? toTrimmedString(raw.mysqlReplicaPassword) : '',
|
||
replicaSet: toTrimmedString(raw.replicaSet),
|
||
authSource: toTrimmedString(raw.authSource),
|
||
readPreference: toTrimmedString(raw.readPreference),
|
||
mongoSrv,
|
||
mongoAuthMechanism: toTrimmedString(raw.mongoAuthMechanism),
|
||
mongoReplicaUser: toTrimmedString(raw.mongoReplicaUser),
|
||
mongoReplicaPassword: savePassword ? toTrimmedString(raw.mongoReplicaPassword) : '',
|
||
timeout: normalizeIntegerInRange(raw.timeout, DEFAULT_TIMEOUT_SECONDS, 1, MAX_TIMEOUT_SECONDS),
|
||
};
|
||
|
||
if (type === 'redis') {
|
||
safeConfig.redisDB = normalizeIntegerInRange(raw.redisDB, 0, 0, 15);
|
||
}
|
||
|
||
if (type === 'custom') {
|
||
safeConfig.driver = toTrimmedString(raw.driver);
|
||
safeConfig.dsn = toTrimmedString(raw.dsn).slice(0, MAX_URI_LENGTH);
|
||
}
|
||
|
||
return safeConfig;
|
||
};
|
||
|
||
const resolveConnectionConfigPayload = (raw: Record<string, unknown>): unknown => {
|
||
if (raw.config && typeof raw.config === 'object') {
|
||
return raw.config;
|
||
}
|
||
// 兼容历史/导入场景:连接对象可能是扁平结构(无 config 包装)。
|
||
const hasLegacyFlatConfig =
|
||
raw.type !== undefined ||
|
||
raw.host !== undefined ||
|
||
raw.port !== undefined ||
|
||
raw.user !== undefined ||
|
||
raw.database !== undefined;
|
||
if (hasLegacyFlatConfig) {
|
||
return raw;
|
||
}
|
||
return undefined;
|
||
};
|
||
|
||
const sanitizeSavedConnection = (value: unknown, index: number): SavedConnection | null => {
|
||
if (!value || typeof value !== 'object') return null;
|
||
const raw = value as Record<string, unknown>;
|
||
const config = sanitizeConnectionConfig(resolveConnectionConfigPayload(raw));
|
||
const id = toTrimmedString(raw.id, `conn-${index + 1}`) || `conn-${index + 1}`;
|
||
const displayType = config.type === 'diros' ? 'doris' : config.type;
|
||
const fallbackName = config.host ? `${displayType}-${config.host}` : `连接-${index + 1}`;
|
||
const name = toTrimmedString(raw.name, fallbackName) || fallbackName;
|
||
const includeDatabases = sanitizeStringArray(raw.includeDatabases, 256);
|
||
const includeRedisDatabases = sanitizeNumberArray(raw.includeRedisDatabases, 0, 15);
|
||
|
||
return {
|
||
id,
|
||
name,
|
||
config,
|
||
includeDatabases: includeDatabases.length > 0 ? includeDatabases : undefined,
|
||
includeRedisDatabases: includeRedisDatabases.length > 0 ? includeRedisDatabases : undefined,
|
||
};
|
||
};
|
||
|
||
const sanitizeConnections = (value: unknown): SavedConnection[] => {
|
||
if (!Array.isArray(value)) return [];
|
||
const result: SavedConnection[] = [];
|
||
const idSet = new Set<string>();
|
||
|
||
value.forEach((entry, index) => {
|
||
const conn = sanitizeSavedConnection(entry, index);
|
||
if (!conn) return;
|
||
let nextId = conn.id;
|
||
if (idSet.has(nextId)) {
|
||
nextId = `${nextId}-${index + 1}`;
|
||
}
|
||
idSet.add(nextId);
|
||
result.push({ ...conn, id: nextId });
|
||
});
|
||
|
||
return result;
|
||
};
|
||
|
||
const sanitizeConnectionTags = (value: unknown): ConnectionTag[] => {
|
||
if (!Array.isArray(value)) return [];
|
||
const result: ConnectionTag[] = [];
|
||
const idSet = new Set<string>();
|
||
|
||
value.forEach((entry, index) => {
|
||
if (!entry || typeof entry !== 'object') return;
|
||
const raw = entry as Record<string, unknown>;
|
||
const id = toTrimmedString(raw.id, `tag-${index + 1}`) || `tag-${index + 1}`;
|
||
if (idSet.has(id)) return;
|
||
idSet.add(id);
|
||
|
||
const name = toTrimmedString(raw.name, `标签-${index + 1}`) || `标签-${index + 1}`;
|
||
const connectionIds = sanitizeStringArray(raw.connectionIds, 256);
|
||
|
||
result.push({ id, name, connectionIds });
|
||
});
|
||
|
||
return result;
|
||
};
|
||
|
||
const isLegacyDefaultAppearance = (appearance: Partial<{ opacity: number; blur: number }> | undefined): boolean => {
|
||
if (!appearance) {
|
||
return true;
|
||
}
|
||
const opacity = typeof appearance.opacity === 'number' ? appearance.opacity : LEGACY_DEFAULT_OPACITY;
|
||
const blur = typeof appearance.blur === 'number' ? appearance.blur : 0;
|
||
return Math.abs(opacity - LEGACY_DEFAULT_OPACITY) < OPACITY_EPSILON && blur === 0;
|
||
};
|
||
|
||
export interface SqlLog {
|
||
id: string;
|
||
timestamp: number;
|
||
sql: string;
|
||
status: 'success' | 'error';
|
||
duration: number;
|
||
message?: string;
|
||
dbName?: string;
|
||
affectedRows?: number;
|
||
}
|
||
|
||
export interface QueryOptions {
|
||
maxRows: number;
|
||
showColumnComment: boolean;
|
||
showColumnType: boolean;
|
||
}
|
||
|
||
export interface GlobalProxyConfig extends ProxyConfig {
|
||
enabled: boolean;
|
||
}
|
||
|
||
interface AppState {
|
||
connections: SavedConnection[];
|
||
connectionTags: ConnectionTag[];
|
||
tabs: TabData[];
|
||
activeTabId: string | null;
|
||
activeContext: { connectionId: string; dbName: string } | null;
|
||
savedQueries: SavedQuery[];
|
||
theme: 'light' | 'dark';
|
||
appearance: { enabled: boolean; opacity: number; blur: number };
|
||
uiScale: number;
|
||
fontSize: number;
|
||
startupFullscreen: boolean;
|
||
globalProxy: GlobalProxyConfig;
|
||
sqlFormatOptions: { keywordCase: 'upper' | 'lower' };
|
||
queryOptions: QueryOptions;
|
||
shortcutOptions: ShortcutOptions;
|
||
sqlLogs: SqlLog[];
|
||
tableAccessCount: Record<string, number>;
|
||
tableSortPreference: Record<string, 'name' | 'frequency'>;
|
||
tableColumnOrders: Record<string, string[]>;
|
||
enableColumnOrderMemory: boolean;
|
||
tableHiddenColumns: Record<string, string[]>;
|
||
enableHiddenColumnMemory: boolean;
|
||
|
||
addConnection: (conn: SavedConnection) => void;
|
||
updateConnection: (conn: SavedConnection) => void;
|
||
removeConnection: (id: string) => void;
|
||
|
||
addConnectionTag: (tag: ConnectionTag) => void;
|
||
updateConnectionTag: (tag: ConnectionTag) => void;
|
||
removeConnectionTag: (id: string) => void;
|
||
moveConnectionToTag: (connectionId: string, targetTagId: string | null) => void;
|
||
reorderTags: (tagIds: string[]) => void;
|
||
|
||
addTab: (tab: TabData) => void;
|
||
closeTab: (id: string) => void;
|
||
closeOtherTabs: (id: string) => void;
|
||
closeTabsToLeft: (id: string) => void;
|
||
closeTabsToRight: (id: string) => void;
|
||
closeTabsByConnection: (connectionId: string) => void;
|
||
closeTabsByDatabase: (connectionId: string, dbName: string) => void;
|
||
moveTab: (sourceId: string, targetId: string) => void;
|
||
closeAllTabs: () => void;
|
||
setActiveTab: (id: string) => void;
|
||
setActiveContext: (context: { connectionId: string; dbName: string } | null) => void;
|
||
|
||
saveQuery: (query: SavedQuery) => void;
|
||
deleteQuery: (id: string) => void;
|
||
|
||
setTheme: (theme: 'light' | 'dark') => void;
|
||
setAppearance: (appearance: Partial<{ enabled: boolean; opacity: number; blur: number }>) => void;
|
||
setUiScale: (scale: number) => void;
|
||
setFontSize: (size: number) => void;
|
||
setStartupFullscreen: (enabled: boolean) => void;
|
||
setGlobalProxy: (proxy: Partial<GlobalProxyConfig>) => void;
|
||
setSqlFormatOptions: (options: { keywordCase: 'upper' | 'lower' }) => void;
|
||
setQueryOptions: (options: Partial<QueryOptions>) => void;
|
||
updateShortcut: (action: ShortcutAction, binding: Partial<ShortcutBinding>) => void;
|
||
resetShortcutOptions: () => void;
|
||
|
||
addSqlLog: (log: SqlLog) => void;
|
||
clearSqlLogs: () => void;
|
||
|
||
recordTableAccess: (connectionId: string, dbName: string, tableName: string) => void;
|
||
setTableSortPreference: (connectionId: string, dbName: string, sortBy: 'name' | 'frequency') => void;
|
||
setTableColumnOrder: (connectionId: string, dbName: string, tableName: string, order: string[]) => void;
|
||
setEnableColumnOrderMemory: (enabled: boolean) => void;
|
||
clearTableColumnOrder: (connectionId: string, dbName: string, tableName: string) => void;
|
||
|
||
setTableHiddenColumns: (connectionId: string, dbName: string, tableName: string, hiddenColumns: string[]) => void;
|
||
setEnableHiddenColumnMemory: (enabled: boolean) => void;
|
||
clearTableHiddenColumns: (connectionId: string, dbName: string, tableName: string) => void;
|
||
}
|
||
|
||
const sanitizeSavedQueries = (value: unknown): SavedQuery[] => {
|
||
if (!Array.isArray(value)) return [];
|
||
const result: SavedQuery[] = [];
|
||
value.forEach((entry, index) => {
|
||
if (!entry || typeof entry !== 'object') return;
|
||
const raw = entry as Record<string, unknown>;
|
||
const id = toTrimmedString(raw.id, `query-${index + 1}`) || `query-${index + 1}`;
|
||
const sql = toTrimmedString(raw.sql);
|
||
const connectionId = toTrimmedString(raw.connectionId);
|
||
const dbName = toTrimmedString(raw.dbName);
|
||
if (!sql || !connectionId || !dbName) return;
|
||
result.push({
|
||
id,
|
||
name: toTrimmedString(raw.name, `查询-${index + 1}`) || `查询-${index + 1}`,
|
||
sql,
|
||
connectionId,
|
||
dbName,
|
||
createdAt: Number.isFinite(Number(raw.createdAt)) ? Number(raw.createdAt) : Date.now(),
|
||
});
|
||
});
|
||
return result;
|
||
};
|
||
|
||
const sanitizeTheme = (value: unknown): 'light' | 'dark' => (value === 'dark' ? 'dark' : 'light');
|
||
|
||
const sanitizeSqlFormatOptions = (value: unknown): { keywordCase: 'upper' | 'lower' } => {
|
||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||
return { keywordCase: raw.keywordCase === 'lower' ? 'lower' : 'upper' };
|
||
};
|
||
|
||
const sanitizeQueryOptions = (value: unknown): QueryOptions => {
|
||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||
const maxRows = Number(raw.maxRows);
|
||
const showColumnComment = typeof raw.showColumnComment === 'boolean' ? raw.showColumnComment : true;
|
||
const showColumnType = typeof raw.showColumnType === 'boolean' ? raw.showColumnType : true;
|
||
if (!Number.isFinite(maxRows) || maxRows <= 0) {
|
||
return { maxRows: 5000, showColumnComment, showColumnType };
|
||
}
|
||
return { maxRows: Math.min(50000, Math.trunc(maxRows)), showColumnComment, showColumnType };
|
||
};
|
||
|
||
const sanitizeTableAccessCount = (value: unknown): Record<string, number> => {
|
||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||
const result: Record<string, number> = {};
|
||
Object.entries(raw).forEach(([key, count]) => {
|
||
const parsed = Number(count);
|
||
if (!Number.isFinite(parsed) || parsed < 0) return;
|
||
result[key] = Math.trunc(parsed);
|
||
});
|
||
return result;
|
||
};
|
||
|
||
const sanitizeTableSortPreference = (value: unknown): Record<string, 'name' | 'frequency'> => {
|
||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||
const result: Record<string, 'name' | 'frequency'> = {};
|
||
Object.entries(raw).forEach(([key, preference]) => {
|
||
result[key] = preference === 'frequency' ? 'frequency' : 'name';
|
||
});
|
||
return result;
|
||
};
|
||
|
||
const sanitizeTableColumnOrders = (value: unknown): Record<string, string[]> => {
|
||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||
const result: Record<string, string[]> = {};
|
||
Object.entries(raw).forEach(([key, orderArray]) => {
|
||
if (Array.isArray(orderArray)) {
|
||
result[key] = orderArray.map(col => String(col));
|
||
}
|
||
});
|
||
return result;
|
||
};
|
||
|
||
const sanitizeTableHiddenColumns = (value: unknown): Record<string, string[]> => {
|
||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||
const result: Record<string, string[]> = {};
|
||
Object.entries(raw).forEach(([key, hiddenArray]) => {
|
||
if (Array.isArray(hiddenArray)) {
|
||
result[key] = hiddenArray.map(col => String(col));
|
||
}
|
||
});
|
||
return result;
|
||
};
|
||
|
||
const sanitizeAppearance = (
|
||
appearance: Partial<{ enabled: boolean; opacity: number; blur: number }> | undefined,
|
||
version: number
|
||
): { enabled: boolean; opacity: number; blur: number } => {
|
||
if (!appearance || typeof appearance !== 'object') {
|
||
return { ...DEFAULT_APPEARANCE };
|
||
}
|
||
const nextAppearance = {
|
||
enabled: typeof appearance.enabled === 'boolean' ? appearance.enabled : DEFAULT_APPEARANCE.enabled,
|
||
opacity: typeof appearance.opacity === 'number' ? appearance.opacity : DEFAULT_APPEARANCE.opacity,
|
||
blur: typeof appearance.blur === 'number' ? appearance.blur : DEFAULT_APPEARANCE.blur,
|
||
};
|
||
if (version < 2 && isLegacyDefaultAppearance(appearance)) {
|
||
return { ...DEFAULT_APPEARANCE };
|
||
}
|
||
return nextAppearance;
|
||
};
|
||
|
||
const sanitizeStartupFullscreen = (value: unknown): boolean => {
|
||
return value === true;
|
||
};
|
||
|
||
const sanitizeUiScale = (value: unknown): number => {
|
||
return normalizeFloatInRange(value, DEFAULT_UI_SCALE, MIN_UI_SCALE, MAX_UI_SCALE);
|
||
};
|
||
|
||
const sanitizeFontSize = (value: unknown): number => {
|
||
return normalizeIntegerInRange(value, DEFAULT_FONT_SIZE, MIN_FONT_SIZE, MAX_FONT_SIZE);
|
||
};
|
||
|
||
const sanitizeGlobalProxy = (value: unknown): GlobalProxyConfig => {
|
||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||
const typeRaw = toTrimmedString(raw.type, DEFAULT_GLOBAL_PROXY.type).toLowerCase();
|
||
const type: 'socks5' | 'http' = typeRaw === 'http' ? 'http' : 'socks5';
|
||
const fallbackPort = type === 'http' ? 8080 : 1080;
|
||
return {
|
||
enabled: raw.enabled === true,
|
||
type,
|
||
host: toTrimmedString(raw.host),
|
||
port: normalizePort(raw.port, fallbackPort),
|
||
user: toTrimmedString(raw.user),
|
||
password: toTrimmedString(raw.password),
|
||
};
|
||
};
|
||
|
||
const unwrapPersistedAppState = (persistedState: unknown): Record<string, unknown> => {
|
||
if (!persistedState || typeof persistedState !== 'object') {
|
||
return {};
|
||
}
|
||
const raw = persistedState as Record<string, unknown>;
|
||
if (raw.state && typeof raw.state === 'object') {
|
||
return raw.state as Record<string, unknown>;
|
||
}
|
||
return raw;
|
||
};
|
||
|
||
export const useStore = create<AppState>()(
|
||
persist(
|
||
(set) => ({
|
||
connections: [],
|
||
connectionTags: [],
|
||
tabs: [],
|
||
activeTabId: null,
|
||
activeContext: null,
|
||
savedQueries: [],
|
||
theme: 'light',
|
||
appearance: { ...DEFAULT_APPEARANCE },
|
||
uiScale: DEFAULT_UI_SCALE,
|
||
fontSize: DEFAULT_FONT_SIZE,
|
||
startupFullscreen: DEFAULT_STARTUP_FULLSCREEN,
|
||
globalProxy: { ...DEFAULT_GLOBAL_PROXY },
|
||
sqlFormatOptions: { keywordCase: 'upper' },
|
||
queryOptions: { maxRows: 5000, showColumnComment: true, showColumnType: true },
|
||
shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS),
|
||
sqlLogs: [],
|
||
tableAccessCount: {},
|
||
tableSortPreference: {},
|
||
tableColumnOrders: {},
|
||
enableColumnOrderMemory: true,
|
||
tableHiddenColumns: {},
|
||
enableHiddenColumnMemory: true,
|
||
|
||
addConnection: (conn) => set((state) => ({ connections: [...state.connections, conn] })),
|
||
updateConnection: (conn) => set((state) => ({
|
||
connections: state.connections.map(c => c.id === conn.id ? conn : c)
|
||
})),
|
||
removeConnection: (id) => set((state) => ({
|
||
connections: state.connections.filter(c => c.id !== id),
|
||
connectionTags: state.connectionTags.map(tag => ({
|
||
...tag,
|
||
connectionIds: tag.connectionIds.filter(cid => cid !== id)
|
||
}))
|
||
})),
|
||
|
||
addConnectionTag: (tag) => set((state) => ({ connectionTags: [...state.connectionTags, tag] })),
|
||
updateConnectionTag: (tag) => set((state) => ({
|
||
connectionTags: state.connectionTags.map(t => t.id === tag.id ? tag : t)
|
||
})),
|
||
removeConnectionTag: (id) => set((state) => ({
|
||
connectionTags: state.connectionTags.filter(t => t.id !== id)
|
||
})),
|
||
moveConnectionToTag: (connectionId, targetTagId) => set((state) => {
|
||
const newTags = state.connectionTags.map(tag => {
|
||
//先从所有tag中移除该connection
|
||
const filteredIds = tag.connectionIds.filter(id => id !== connectionId);
|
||
if (tag.id === targetTagId) {
|
||
return { ...tag, connectionIds: [...filteredIds, connectionId] };
|
||
}
|
||
return { ...tag, connectionIds: filteredIds };
|
||
});
|
||
return { connectionTags: newTags };
|
||
}),
|
||
reorderTags: (tagIds) => set((state) => {
|
||
const tagMap = new Map(state.connectionTags.map(t => [t.id, t]));
|
||
const newTags: ConnectionTag[] = [];
|
||
tagIds.forEach(id => {
|
||
const tag = tagMap.get(id);
|
||
if (tag) {
|
||
newTags.push(tag);
|
||
tagMap.delete(id);
|
||
}
|
||
});
|
||
// 追加未指定的tag(如果有的话)
|
||
newTags.push(...Array.from(tagMap.values()));
|
||
return { connectionTags: newTags };
|
||
}),
|
||
|
||
addTab: (tab) => set((state) => {
|
||
const index = state.tabs.findIndex(t => t.id === tab.id);
|
||
if (index !== -1) {
|
||
// Update existing tab with new data (e.g. switch initialTab)
|
||
const newTabs = [...state.tabs];
|
||
newTabs[index] = { ...newTabs[index], ...tab };
|
||
return { tabs: newTabs, activeTabId: tab.id };
|
||
}
|
||
return { tabs: [...state.tabs, tab], activeTabId: tab.id };
|
||
}),
|
||
|
||
closeTab: (id) => set((state) => {
|
||
const newTabs = state.tabs.filter(t => t.id !== id);
|
||
let newActiveId = state.activeTabId;
|
||
if (state.activeTabId === id) {
|
||
newActiveId = newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null;
|
||
}
|
||
return { tabs: newTabs, activeTabId: newActiveId };
|
||
}),
|
||
|
||
closeOtherTabs: (id) => set((state) => {
|
||
const keep = state.tabs.find(t => t.id === id);
|
||
if (!keep) return state;
|
||
return { tabs: [keep], activeTabId: id };
|
||
}),
|
||
|
||
closeTabsToLeft: (id) => set((state) => {
|
||
const index = state.tabs.findIndex(t => t.id === id);
|
||
if (index === -1) return state;
|
||
const newTabs = state.tabs.slice(index);
|
||
const activeStillExists = state.activeTabId ? newTabs.some(t => t.id === state.activeTabId) : false;
|
||
return { tabs: newTabs, activeTabId: activeStillExists ? state.activeTabId : id };
|
||
}),
|
||
|
||
closeTabsToRight: (id) => set((state) => {
|
||
const index = state.tabs.findIndex(t => t.id === id);
|
||
if (index === -1) return state;
|
||
const newTabs = state.tabs.slice(0, index + 1);
|
||
const activeStillExists = state.activeTabId ? newTabs.some(t => t.id === state.activeTabId) : false;
|
||
return { tabs: newTabs, activeTabId: activeStillExists ? state.activeTabId : id };
|
||
}),
|
||
|
||
closeTabsByConnection: (connectionId) => set((state) => {
|
||
const targetConnectionId = String(connectionId || '').trim();
|
||
if (!targetConnectionId) return state;
|
||
const newTabs = state.tabs.filter(t => String(t.connectionId || '').trim() !== targetConnectionId);
|
||
const activeStillExists = state.activeTabId ? newTabs.some(t => t.id === state.activeTabId) : false;
|
||
const nextActiveTabId = activeStillExists
|
||
? state.activeTabId
|
||
: (newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null);
|
||
const nextActiveContext = state.activeContext?.connectionId === targetConnectionId ? null : state.activeContext;
|
||
return {
|
||
tabs: newTabs,
|
||
activeTabId: nextActiveTabId,
|
||
activeContext: nextActiveContext,
|
||
};
|
||
}),
|
||
|
||
closeTabsByDatabase: (connectionId, dbName) => set((state) => {
|
||
const targetConnectionId = String(connectionId || '').trim();
|
||
const targetDbName = String(dbName || '').trim();
|
||
if (!targetConnectionId || !targetDbName) return state;
|
||
const newTabs = state.tabs.filter((tab) => {
|
||
const sameConnection = String(tab.connectionId || '').trim() === targetConnectionId;
|
||
const sameDb = String(tab.dbName || '').trim() === targetDbName;
|
||
return !(sameConnection && sameDb);
|
||
});
|
||
const activeStillExists = state.activeTabId ? newTabs.some(t => t.id === state.activeTabId) : false;
|
||
const nextActiveTabId = activeStillExists
|
||
? state.activeTabId
|
||
: (newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null);
|
||
const sameActiveContext = state.activeContext
|
||
&& state.activeContext.connectionId === targetConnectionId
|
||
&& state.activeContext.dbName === targetDbName;
|
||
return {
|
||
tabs: newTabs,
|
||
activeTabId: nextActiveTabId,
|
||
activeContext: sameActiveContext ? null : state.activeContext,
|
||
};
|
||
}),
|
||
|
||
moveTab: (sourceId, targetId) => set((state) => {
|
||
const fromId = String(sourceId || '').trim();
|
||
const toId = String(targetId || '').trim();
|
||
if (!fromId || !toId || fromId === toId) {
|
||
return state;
|
||
}
|
||
const fromIndex = state.tabs.findIndex((tab) => tab.id === fromId);
|
||
const toIndex = state.tabs.findIndex((tab) => tab.id === toId);
|
||
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) {
|
||
return state;
|
||
}
|
||
const nextTabs = [...state.tabs];
|
||
const [movingTab] = nextTabs.splice(fromIndex, 1);
|
||
nextTabs.splice(toIndex, 0, movingTab);
|
||
return { tabs: nextTabs };
|
||
}),
|
||
|
||
closeAllTabs: () => set(() => ({ tabs: [], activeTabId: null })),
|
||
|
||
setActiveTab: (id) => set({ activeTabId: id }),
|
||
setActiveContext: (context) => set({ activeContext: context }),
|
||
|
||
saveQuery: (query) => set((state) => {
|
||
// If query with same ID exists, update it
|
||
const existing = state.savedQueries.find(q => q.id === query.id);
|
||
if (existing) {
|
||
return { savedQueries: state.savedQueries.map(q => q.id === query.id ? query : q) };
|
||
}
|
||
return { savedQueries: [...state.savedQueries, query] };
|
||
}),
|
||
|
||
deleteQuery: (id) => set((state) => ({ savedQueries: state.savedQueries.filter(q => q.id !== id) })),
|
||
|
||
setTheme: (theme) => set({ theme }),
|
||
setAppearance: (appearance) => set((state) => ({ appearance: { ...state.appearance, ...appearance } })),
|
||
setUiScale: (scale) => set({ uiScale: sanitizeUiScale(scale) }),
|
||
setFontSize: (size) => set({ fontSize: sanitizeFontSize(size) }),
|
||
setStartupFullscreen: (enabled) => set({ startupFullscreen: !!enabled }),
|
||
setGlobalProxy: (proxy) => set((state) => ({ globalProxy: sanitizeGlobalProxy({ ...state.globalProxy, ...proxy }) })),
|
||
setSqlFormatOptions: (options) => set({ sqlFormatOptions: options }),
|
||
setQueryOptions: (options) => set((state) => ({ queryOptions: { ...state.queryOptions, ...options } })),
|
||
updateShortcut: (action, binding) => set((state) => ({
|
||
shortcutOptions: {
|
||
...state.shortcutOptions,
|
||
[action]: {
|
||
...state.shortcutOptions[action],
|
||
...binding,
|
||
},
|
||
},
|
||
})),
|
||
resetShortcutOptions: () => set({ shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS) }),
|
||
|
||
addSqlLog: (log) => set((state) => ({ sqlLogs: [log, ...state.sqlLogs].slice(0, 1000) })), // Keep last 1000 logs
|
||
clearSqlLogs: () => set({ sqlLogs: [] }),
|
||
|
||
recordTableAccess: (connectionId, dbName, tableName) => set((state) => {
|
||
const key = `${connectionId}-${dbName}-${tableName}`;
|
||
const currentCount = state.tableAccessCount[key] || 0;
|
||
return {
|
||
tableAccessCount: {
|
||
...state.tableAccessCount,
|
||
[key]: currentCount + 1
|
||
}
|
||
};
|
||
}),
|
||
|
||
setTableSortPreference: (connectionId, dbName, sortBy) => set((state) => {
|
||
const key = `${connectionId}-${dbName}`;
|
||
return {
|
||
tableSortPreference: {
|
||
...state.tableSortPreference,
|
||
[key]: sortBy
|
||
}
|
||
};
|
||
}),
|
||
|
||
setTableColumnOrder: (connectionId, dbName, tableName, order) => set((state) => {
|
||
const key = `${connectionId}-${dbName}-${tableName}`;
|
||
return {
|
||
tableColumnOrders: {
|
||
...state.tableColumnOrders,
|
||
[key]: order
|
||
}
|
||
};
|
||
}),
|
||
|
||
clearTableColumnOrder: (connectionId, dbName, tableName) => set((state) => {
|
||
const key = `${connectionId}-${dbName}-${tableName}`;
|
||
const newOrders = { ...state.tableColumnOrders };
|
||
delete newOrders[key];
|
||
return { tableColumnOrders: newOrders };
|
||
}),
|
||
|
||
setEnableColumnOrderMemory: (enabled) => set({ enableColumnOrderMemory: !!enabled }),
|
||
|
||
setTableHiddenColumns: (connectionId, dbName, tableName, hiddenColumns) => set((state) => {
|
||
const key = `${connectionId}-${dbName}-${tableName}`;
|
||
return {
|
||
tableHiddenColumns: {
|
||
...state.tableHiddenColumns,
|
||
[key]: hiddenColumns
|
||
}
|
||
};
|
||
}),
|
||
|
||
clearTableHiddenColumns: (connectionId, dbName, tableName) => set((state) => {
|
||
const key = `${connectionId}-${dbName}-${tableName}`;
|
||
const newHidden = { ...state.tableHiddenColumns };
|
||
delete newHidden[key];
|
||
return { tableHiddenColumns: newHidden };
|
||
}),
|
||
|
||
setEnableHiddenColumnMemory: (enabled) => set({ enableHiddenColumnMemory: !!enabled }),
|
||
}),
|
||
{
|
||
name: 'lite-db-storage', // name of the item in the storage (must be unique)
|
||
version: PERSIST_VERSION,
|
||
migrate: (persistedState: unknown, version: number) => {
|
||
const state = unwrapPersistedAppState(persistedState) as Partial<AppState>;
|
||
const nextState: Partial<AppState> = { ...state };
|
||
nextState.connections = sanitizeConnections(state.connections);
|
||
if (version < 5) {
|
||
nextState.connectionTags = sanitizeConnectionTags(state.connectionTags);
|
||
} else {
|
||
nextState.connectionTags = sanitizeConnectionTags(state.connectionTags);
|
||
}
|
||
nextState.savedQueries = sanitizeSavedQueries(state.savedQueries);
|
||
nextState.theme = sanitizeTheme(state.theme);
|
||
nextState.appearance = sanitizeAppearance(state.appearance, version);
|
||
nextState.uiScale = sanitizeUiScale(state.uiScale);
|
||
nextState.fontSize = sanitizeFontSize(state.fontSize);
|
||
nextState.startupFullscreen = sanitizeStartupFullscreen(state.startupFullscreen);
|
||
nextState.globalProxy = sanitizeGlobalProxy(state.globalProxy);
|
||
nextState.sqlFormatOptions = sanitizeSqlFormatOptions(state.sqlFormatOptions);
|
||
nextState.queryOptions = sanitizeQueryOptions(state.queryOptions);
|
||
nextState.shortcutOptions = sanitizeShortcutOptions(state.shortcutOptions);
|
||
nextState.tableAccessCount = sanitizeTableAccessCount(state.tableAccessCount);
|
||
nextState.tableSortPreference = sanitizeTableSortPreference(state.tableSortPreference);
|
||
// 新增的列排序记忆状态不需要做版本特殊兼容,直接做基本的类型保护
|
||
const safeOrders = sanitizeTableColumnOrders(state.tableColumnOrders);
|
||
nextState.tableColumnOrders = safeOrders;
|
||
nextState.enableColumnOrderMemory = state.enableColumnOrderMemory !== false;
|
||
const safeHidden = sanitizeTableHiddenColumns(state.tableHiddenColumns);
|
||
nextState.tableHiddenColumns = safeHidden;
|
||
nextState.enableHiddenColumnMemory = state.enableHiddenColumnMemory !== false;
|
||
return nextState as AppState;
|
||
},
|
||
merge: (persistedState, currentState) => {
|
||
const state = unwrapPersistedAppState(persistedState) as Partial<AppState>;
|
||
return {
|
||
...currentState,
|
||
...state,
|
||
connections: sanitizeConnections(state.connections),
|
||
connectionTags: sanitizeConnectionTags(state.connectionTags),
|
||
savedQueries: sanitizeSavedQueries(state.savedQueries),
|
||
theme: sanitizeTheme(state.theme),
|
||
appearance: sanitizeAppearance(state.appearance, PERSIST_VERSION),
|
||
uiScale: sanitizeUiScale(state.uiScale),
|
||
fontSize: sanitizeFontSize(state.fontSize),
|
||
startupFullscreen: sanitizeStartupFullscreen(state.startupFullscreen),
|
||
globalProxy: sanitizeGlobalProxy(state.globalProxy),
|
||
tableSortPreference: sanitizeTableSortPreference(state.tableSortPreference),
|
||
tableColumnOrders: sanitizeTableColumnOrders(state.tableColumnOrders),
|
||
enableColumnOrderMemory: state.enableColumnOrderMemory !== false,
|
||
tableHiddenColumns: sanitizeTableHiddenColumns(state.tableHiddenColumns),
|
||
enableHiddenColumnMemory: state.enableHiddenColumnMemory !== false,
|
||
|
||
sqlFormatOptions: sanitizeSqlFormatOptions(state.sqlFormatOptions),
|
||
queryOptions: sanitizeQueryOptions(state.queryOptions),
|
||
shortcutOptions: sanitizeShortcutOptions(state.shortcutOptions),
|
||
tableAccessCount: sanitizeTableAccessCount(state.tableAccessCount),
|
||
};
|
||
},
|
||
partialize: (state) => ({
|
||
connections: state.connections,
|
||
connectionTags: state.connectionTags,
|
||
savedQueries: state.savedQueries,
|
||
theme: state.theme,
|
||
appearance: state.appearance,
|
||
uiScale: state.uiScale,
|
||
fontSize: state.fontSize,
|
||
startupFullscreen: state.startupFullscreen,
|
||
globalProxy: state.globalProxy,
|
||
sqlFormatOptions: state.sqlFormatOptions,
|
||
queryOptions: state.queryOptions,
|
||
shortcutOptions: state.shortcutOptions,
|
||
tableAccessCount: state.tableAccessCount,
|
||
tableSortPreference: state.tableSortPreference,
|
||
tableColumnOrders: state.tableColumnOrders,
|
||
enableColumnOrderMemory: state.enableColumnOrderMemory,
|
||
tableHiddenColumns: state.tableHiddenColumns,
|
||
enableHiddenColumnMemory: state.enableHiddenColumnMemory
|
||
}), // Don't persist logs
|
||
}
|
||
)
|
||
);
|