mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-10 23:11:40 +08:00
🐛 fix(frontend): 修复 Redis 搜索匹配与输入交互体验
- Redis Key 搜索默认补全包含匹配并支持 ASCII 大小写不敏感 - Redis 标签页增加连接名与 host 摘要,区分同名 db 标签 - 抽取 inputAutoCap、redisSearchPattern、tabDisplay 共享工具并补充回归测试 - 覆盖连接配置、Redis 搜索、表设计、表概览和数据表筛选输入的自动纠正问题 - 在 macOS 文本输入面板关闭局部毛玻璃,修复输入法切换出现透明框
This commit is contained in:
@@ -4,7 +4,7 @@ import { DatabaseOutlined, ConsoleSqlOutlined, FileTextOutlined, CloudServerOutl
|
||||
import { getDbIcon, getDbDefaultColor, getDbIconLabel, DB_ICON_TYPES, PRESET_ICON_COLORS } from './DatabaseIcons';
|
||||
import { useStore } from '../store';
|
||||
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||||
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import {
|
||||
getStoredSecretPlaceholder,
|
||||
normalizeConnectionSecretErrorMessage,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../utils/connectionModalPresentation';
|
||||
import { resolveConnectionSecretDraft } from '../utils/connectionSecretDraft';
|
||||
import { getCustomConnectionDsnValidationMessage } from '../utils/customConnectionDsn';
|
||||
import { applyNoAutoCapAttributes, noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import { DBGetDatabases, GetDriverStatusList, MongoDiscoverMembers, TestConnection, RedisConnect, SelectDatabaseFile, SelectSSHKeyFile } from '../../wailsjs/go/app/App';
|
||||
import { ConnectionConfig, MongoMemberInfo, SavedConnection } from '../types';
|
||||
|
||||
@@ -24,21 +25,6 @@ const CONNECTION_MODAL_WIDTH = 960;
|
||||
const CONNECTION_MODAL_BODY_HEIGHT = 620;
|
||||
const STEP1_SIDEBAR_DIVIDER_DARK = 'rgba(255, 255, 255, 0.16)';
|
||||
const STEP1_SIDEBAR_DIVIDER_LIGHT = 'rgba(0, 0, 0, 0.08)';
|
||||
const noAutoCapInputProps = {
|
||||
autoCapitalize: 'none' as const,
|
||||
autoCorrect: 'off' as const,
|
||||
spellCheck: false,
|
||||
};
|
||||
|
||||
const applyNoAutoCapAttributes = (element: Element) => {
|
||||
if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement)) {
|
||||
return;
|
||||
}
|
||||
element.setAttribute('autocapitalize', 'none');
|
||||
element.setAttribute('autocorrect', 'off');
|
||||
element.setAttribute('spellcheck', 'false');
|
||||
};
|
||||
|
||||
type ConnectionSecretKey =
|
||||
| 'primaryPassword'
|
||||
| 'sshPassword'
|
||||
@@ -177,6 +163,7 @@ const ConnectionModal: React.FC<{
|
||||
const darkMode = theme === 'dark';
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const effectiveOpacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const disableLocalBackdropFilter = isMacLikePlatform();
|
||||
const mysqlTopology = Form.useWatch('mysqlTopology', form) || 'single';
|
||||
const mongoTopology = Form.useWatch('mongoTopology', form) || 'single';
|
||||
const mongoSrv = Form.useWatch('mongoSrv', form) || false;
|
||||
@@ -207,7 +194,10 @@ const ConnectionModal: React.FC<{
|
||||
const step1SidebarDividerColor = darkMode ? STEP1_SIDEBAR_DIVIDER_DARK : STEP1_SIDEBAR_DIVIDER_LIGHT;
|
||||
const step1SidebarActiveBg = darkMode ? 'rgba(246, 196, 83, 0.20)' : '#e6f4ff';
|
||||
const step1SidebarActiveColor = darkMode ? '#ffd666' : '#1677ff';
|
||||
const overlayTheme = useMemo(() => buildOverlayWorkbenchTheme(darkMode), [darkMode]);
|
||||
const overlayTheme = useMemo(
|
||||
() => buildOverlayWorkbenchTheme(darkMode, { disableBackdropFilter: disableLocalBackdropFilter }),
|
||||
[darkMode, disableLocalBackdropFilter],
|
||||
);
|
||||
|
||||
const tunnelSectionStyle: React.CSSProperties = {
|
||||
padding: '12px',
|
||||
@@ -3233,5 +3223,3 @@ const ConnectionModal: React.FC<{
|
||||
export default ConnectionModal;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
} from './dataGridCopyInsert';
|
||||
import { calculateAutoFitColumnWidth } from './dataGridAutoWidth';
|
||||
import { buildSelectedCellClipboardText } from './dataGridSelectionCopy';
|
||||
import { applyNoAutoCapAttributesWithin, noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
|
||||
// --- Error Boundary ---
|
||||
interface DataGridErrorBoundaryState {
|
||||
@@ -2234,6 +2235,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
// Filter State
|
||||
const [filterConditions, setFilterConditions] = useState<GridFilterCondition[]>([]);
|
||||
const [nextFilterId, setNextFilterId] = useState(1);
|
||||
const filterPanelRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const nextConditions = normalizeGridFilterConditions(appliedFilterConditions);
|
||||
@@ -2242,6 +2244,30 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
setNextFilterId(Math.max(1, maxId + 1));
|
||||
}, [appliedFilterConditions, normalizeGridFilterConditions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showFilter) {
|
||||
return;
|
||||
}
|
||||
const root = filterPanelRef.current;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
const apply = () => {
|
||||
applyNoAutoCapAttributesWithin(root);
|
||||
};
|
||||
apply();
|
||||
if (typeof MutationObserver === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
apply();
|
||||
});
|
||||
observer.observe(root, { childList: true, subtree: true });
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [showFilter]);
|
||||
|
||||
const selectedRowKeysRef = useRef(selectedRowKeys);
|
||||
const displayDataRef = useRef<any[]>([]);
|
||||
|
||||
@@ -5135,7 +5161,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
</div>
|
||||
|
||||
{showFilter && (
|
||||
<div style={{
|
||||
<div ref={filterPanelRef} style={{
|
||||
padding: `${filterTopPadding}px ${panelPaddingX}px ${panelPaddingY}px ${panelPaddingX}px`,
|
||||
background: 'transparent',
|
||||
boxSizing: 'border-box',
|
||||
@@ -5184,6 +5210,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
{cond.op === 'CUSTOM' ? (
|
||||
<Input.TextArea
|
||||
{...noAutoCapInputProps}
|
||||
style={{ flex: 1 }}
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
value={cond.value}
|
||||
@@ -5192,6 +5219,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
/>
|
||||
) : isListOp(cond.op) ? (
|
||||
<Input.TextArea
|
||||
{...noAutoCapInputProps}
|
||||
style={{ flex: 1 }}
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
value={cond.value}
|
||||
@@ -5201,12 +5229,14 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
) : isBetweenOp(cond.op) ? (
|
||||
<>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
style={{ width: 220 }}
|
||||
value={cond.value}
|
||||
onChange={e => updateFilter(cond.id, 'value', e.target.value)}
|
||||
placeholder="开始值"
|
||||
/>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
style={{ width: 220 }}
|
||||
value={cond.value2 || ''}
|
||||
onChange={e => updateFilter(cond.id, 'value2', e.target.value)}
|
||||
@@ -5214,9 +5244,10 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
/>
|
||||
</>
|
||||
) : isNoValueOp(cond.op) ? (
|
||||
<Input style={{ width: 220 }} value="" disabled placeholder="无需输入值" />
|
||||
<Input {...noAutoCapInputProps} style={{ width: 220 }} value="" disabled placeholder="无需输入值" />
|
||||
) : (
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
style={{ width: 280 }}
|
||||
value={cond.value}
|
||||
onChange={e => updateFilter(cond.id, 'value', e.target.value)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useStore } from '../store';
|
||||
import { DBGetDatabases, DBGetTables, DataSync, DataSyncAnalyze, DataSyncPreview } from '../../wailsjs/go/app/App';
|
||||
import { SavedConnection } from '../types';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues, resolveTextInputSafeBackdropFilter } from '../utils/appearance';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { formatLocalDateTimeLiteral, normalizeTemporalLiteralText } from './dataGridCopyInsert';
|
||||
|
||||
@@ -190,6 +190,7 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open,
|
||||
const darkMode = themeMode === 'dark';
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const effectiveOpacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const disableLocalBackdropFilter = isMacLikePlatform();
|
||||
|
||||
// Step 1: Config
|
||||
const [sourceConnId, setSourceConnId] = useState<string>('');
|
||||
@@ -630,8 +631,8 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open,
|
||||
: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(246,248,252,0.98) 100%)',
|
||||
border: darkMode ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(16,24,40,0.08)',
|
||||
boxShadow: darkMode ? '0 24px 56px rgba(0,0,0,0.36)' : '0 18px 44px rgba(15,23,42,0.14)',
|
||||
backdropFilter: darkMode ? 'blur(18px)' : 'none',
|
||||
}), [darkMode]);
|
||||
backdropFilter: resolveTextInputSafeBackdropFilter(darkMode ? 'blur(18px)' : 'none', disableLocalBackdropFilter),
|
||||
}), [darkMode, disableLocalBackdropFilter]);
|
||||
|
||||
const shellCardStyle = useMemo<React.CSSProperties>(() => ({
|
||||
borderRadius: 18,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { quoteIdentPart, escapeLiteral } from '../utils/sql';
|
||||
import { useStore } from '../store';
|
||||
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { isMacLikePlatform } from '../utils/appearance';
|
||||
|
||||
interface FindInDatabaseModalProps {
|
||||
open: boolean;
|
||||
@@ -67,14 +68,15 @@ const FindInDatabaseModal: React.FC<FindInDatabaseModalProps> = ({ open, onClose
|
||||
|
||||
const connections = useStore(state => state.connections);
|
||||
const theme = useStore(state => state.theme);
|
||||
const disableLocalBackdropFilter = isMacLikePlatform();
|
||||
|
||||
const conn = useMemo(() => connections.find(c => c.id === connectionId), [connections, connectionId]);
|
||||
const dbType = useMemo(() => (conn?.config?.type || 'mysql').toLowerCase(), [conn]);
|
||||
|
||||
const wt = useMemo(() => {
|
||||
const isDark = theme === 'dark';
|
||||
return buildOverlayWorkbenchTheme(isDark);
|
||||
}, [theme]);
|
||||
return buildOverlayWorkbenchTheme(isDark, { disableBackdropFilter: disableLocalBackdropFilter });
|
||||
}, [disableLocalBackdropFilter, theme]);
|
||||
|
||||
const buildConfig = useCallback(() => {
|
||||
if (!conn) return null;
|
||||
|
||||
@@ -6,7 +6,14 @@ import { useStore } from '../store';
|
||||
import { RedisKeyInfo, RedisValue, StreamEntry } from '../types';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import { blurToFilter, normalizeBlurForPlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import {
|
||||
blurToFilter,
|
||||
isMacLikePlatform,
|
||||
normalizeBlurForPlatform,
|
||||
normalizeOpacityForPlatform,
|
||||
resolveAppearanceValues,
|
||||
resolveTextInputSafeBackdropFilter,
|
||||
} from '../utils/appearance';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import {
|
||||
applyRenamedRedisKeyState,
|
||||
@@ -19,6 +26,8 @@ import {
|
||||
type RedisTreeDataNode,
|
||||
} from './redisViewerTree';
|
||||
import { buildRedisWorkbenchTheme } from './redisViewerWorkbenchTheme';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import { normalizeRedisSearchDraftChange, normalizeRedisSearchInput } from '../utils/redisSearchPattern';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
@@ -283,8 +292,16 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const blur = normalizeBlurForPlatform(resolvedAppearance.blur);
|
||||
const disableLocalBackdropFilter = isMacLikePlatform();
|
||||
const connection = connections.find(c => c.id === connectionId);
|
||||
const workbenchTheme = useMemo(() => buildRedisWorkbenchTheme({ darkMode, opacity, blur }), [blur, darkMode, opacity]);
|
||||
const workbenchTheme = useMemo(
|
||||
() => buildRedisWorkbenchTheme({ darkMode, opacity, blur, disableBackdropFilter: disableLocalBackdropFilter }),
|
||||
[blur, darkMode, disableLocalBackdropFilter, opacity],
|
||||
);
|
||||
const workbenchBackdropFilter = useMemo(
|
||||
() => resolveTextInputSafeBackdropFilter(blurToFilter(blur), disableLocalBackdropFilter),
|
||||
[blur, disableLocalBackdropFilter],
|
||||
);
|
||||
const keyAccentColor = workbenchTheme.accent;
|
||||
const jsonAccentColor = darkMode ? '#f6c453' : '#1890ff';
|
||||
const valueToolbarBg = workbenchTheme.panelBgStrong;
|
||||
@@ -293,6 +310,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
|
||||
const [keys, setKeys] = useState<RedisKeyInfo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [searchPattern, setSearchPattern] = useState('*');
|
||||
const [cursor, setCursor] = useState<string>('0');
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
@@ -467,13 +485,29 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
|
||||
useEffect(() => {
|
||||
loadKeys(searchPattern, '0', false, getRedisScanLoadCount(searchPattern, false));
|
||||
}, [redisDB]);
|
||||
}, [loadKeys, redisDB]);
|
||||
|
||||
const executeSearch = useCallback((value: string) => {
|
||||
const normalized = normalizeRedisSearchInput(value);
|
||||
setSearchInput(normalized.keyword);
|
||||
setSearchPattern(normalized.pattern);
|
||||
setCursor('0');
|
||||
loadKeys(normalized.pattern, '0', false, getRedisScanLoadCount(normalized.pattern, false));
|
||||
}, [loadKeys]);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
const pattern = value.trim() || '*';
|
||||
setSearchPattern(pattern);
|
||||
executeSearch(value);
|
||||
};
|
||||
|
||||
const handleSearchInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const normalized = normalizeRedisSearchDraftChange(event.target.value);
|
||||
setSearchInput(normalized.keyword);
|
||||
if (!normalized.shouldSearchImmediately) {
|
||||
return;
|
||||
}
|
||||
setSearchPattern(normalized.pattern);
|
||||
setCursor('0');
|
||||
loadKeys(pattern, '0', false, getRedisScanLoadCount(pattern, false));
|
||||
loadKeys(normalized.pattern, '0', false, getRedisScanLoadCount(normalized.pattern, false));
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
@@ -1214,9 +1248,9 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
title: '添加字段',
|
||||
content: (
|
||||
<Form id="add-hash-field-form" layout="vertical">
|
||||
<Form.Item label="字段名" name="field" rules={[{ required: true }]}>
|
||||
<Input id="new-hash-field" />
|
||||
</Form.Item>
|
||||
<Form.Item label="字段名" name="field" rules={[{ required: true }]}>
|
||||
<Input id="new-hash-field" {...noAutoCapInputProps} />
|
||||
</Form.Item>
|
||||
<Form.Item label="值" name="value" rules={[{ required: true }]}>
|
||||
<Input.TextArea id="new-hash-value" rows={4} />
|
||||
</Form.Item>
|
||||
@@ -1888,7 +1922,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
<div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label>ID(可选,默认 *):</label>
|
||||
<Input id="new-stream-id" placeholder="例如: * 或 1723110000000-0" />
|
||||
<Input id="new-stream-id" {...noAutoCapInputProps} placeholder="例如: * 或 1723110000000-0" />
|
||||
</div>
|
||||
<div>
|
||||
<label>字段 JSON:</label>
|
||||
@@ -2050,7 +2084,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="redis-viewer-workbench" style={{ display: 'flex', height: '100%', gap: 12, padding: 12, background: workbenchTheme.appBg, backdropFilter: blurToFilter(blur), WebkitBackdropFilter: blurToFilter(blur) }}>
|
||||
<div className="redis-viewer-workbench" style={{ display: 'flex', height: '100%', gap: 12, padding: 12, background: workbenchTheme.appBg, backdropFilter: workbenchBackdropFilter, WebkitBackdropFilter: workbenchBackdropFilter }}>
|
||||
{/* Left: Key List */}
|
||||
<div ref={leftPanelRef} style={{ width: leftPanelWidth, minWidth: 300, display: 'flex', flexDirection: 'column', flexShrink: 0, gap: 12 }}>
|
||||
<div style={{ ...workbenchCardStyle, padding: 12 }}>
|
||||
@@ -2063,9 +2097,12 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
</div>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Search
|
||||
placeholder="搜索 Key (支持 * 通配符)"
|
||||
defaultValue="*"
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="搜索 Key"
|
||||
value={searchInput}
|
||||
onChange={handleSearchInputChange}
|
||||
onSearch={handleSearch}
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
/>
|
||||
</Space.Compact>
|
||||
@@ -2177,7 +2214,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
>
|
||||
<Form form={newKeyForm} layout="vertical" initialValues={{ ttl: -1 }}>
|
||||
<Form.Item name="key" label="Key" rules={[{ required: true, message: '请输入 Key' }]}>
|
||||
<Input placeholder="key name" />
|
||||
<Input {...noAutoCapInputProps} placeholder="key name" />
|
||||
</Form.Item>
|
||||
<Form.Item name="value" label="值" rules={[{ required: true, message: '请输入值' }]}>
|
||||
<Input.TextArea rows={4} placeholder="value" />
|
||||
@@ -2207,7 +2244,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
rules={[{ required: true, message: '请输入新的 Key 名称' }]}
|
||||
extra={renameTargetKey ? `原始 Key:${renameTargetKey}` : undefined}
|
||||
>
|
||||
<Input placeholder="new:key:name" />
|
||||
<Input {...noAutoCapInputProps} placeholder="new:key:name" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -41,11 +41,13 @@ import { getDbIcon } from './DatabaseIcons';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, ExportTable, OpenSQLFile, ExecuteSQLFile, CancelSQLFileExecution, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView } from '../../wailsjs/go/app/App';
|
||||
import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
|
||||
import FindInDatabaseModal from './FindInDatabaseModal';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
|
||||
import { resolveConnectionHostTokens } from '../utils/tabDisplay';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
@@ -138,6 +140,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const darkMode = theme === 'dark';
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const disableLocalBackdropFilter = isMacLikePlatform();
|
||||
const autoFetchVisible = useAutoFetchVisibility();
|
||||
const [treeData, setTreeData] = useState<TreeNode[]>([]);
|
||||
|
||||
@@ -151,7 +154,10 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
};
|
||||
const bgMain = getBg('#141414');
|
||||
const overlayTheme = useMemo(() => buildOverlayWorkbenchTheme(darkMode), [darkMode]);
|
||||
const overlayTheme = useMemo(
|
||||
() => buildOverlayWorkbenchTheme(darkMode, { disableBackdropFilter: disableLocalBackdropFilter }),
|
||||
[darkMode, disableLocalBackdropFilter],
|
||||
);
|
||||
const modalPanelStyle = useMemo(() => ({
|
||||
background: overlayTheme.shellBg,
|
||||
border: overlayTheme.shellBorder,
|
||||
@@ -2858,51 +2864,10 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
);
|
||||
}, [darkMode, overlayTheme, searchScopes]);
|
||||
|
||||
const parseHostOnlyToken = (value: unknown): string[] => {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
let text = raw.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
|
||||
if (text.includes('/')) {
|
||||
text = text.split('/')[0];
|
||||
}
|
||||
if (text.includes('?')) {
|
||||
text = text.split('?')[0];
|
||||
}
|
||||
if (text.includes('@')) {
|
||||
text = text.split('@').pop() || '';
|
||||
}
|
||||
return text
|
||||
.split(',')
|
||||
.map((entry) => {
|
||||
const token = entry.trim();
|
||||
if (!token) return '';
|
||||
if (token.startsWith('[')) {
|
||||
const rightBracketIndex = token.indexOf(']');
|
||||
if (rightBracketIndex > 0) {
|
||||
return token.slice(0, rightBracketIndex + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
const colonIndex = token.lastIndexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
return token.slice(0, colonIndex).toLowerCase();
|
||||
}
|
||||
return token.toLowerCase();
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const getConnectionHostSearchText = (node: TreeNode): string => {
|
||||
if (node.type !== 'connection') return '';
|
||||
const config = node.dataRef?.config || {};
|
||||
const hostTokens = [
|
||||
...parseHostOnlyToken(config.host),
|
||||
...(Array.isArray(config.hosts) ? config.hosts.flatMap((entry: string) => parseHostOnlyToken(entry)) : []),
|
||||
...parseHostOnlyToken(config.uri),
|
||||
];
|
||||
const uniqueHosts = Array.from(new Set(hostTokens));
|
||||
return uniqueHosts.join(' ');
|
||||
return resolveConnectionHostTokens(config).join(' ');
|
||||
};
|
||||
|
||||
const getConnectionNameSearchText = (node: TreeNode): string => {
|
||||
@@ -3110,7 +3075,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
onClick: () => {
|
||||
addTab({
|
||||
id: `redis-cmd-${node.key}-${Date.now()}`,
|
||||
title: `命令 - ${node.title}`,
|
||||
title: '命令 - db0',
|
||||
type: 'redis-command',
|
||||
connectionId: node.key,
|
||||
redisDB: 0
|
||||
@@ -3124,7 +3089,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
onClick: () => {
|
||||
addTab({
|
||||
id: `redis-monitor-${node.key}-${Date.now()}`,
|
||||
title: `监控: ${node.title}`,
|
||||
title: '监控 - db0',
|
||||
type: 'redis-monitor',
|
||||
connectionId: node.key,
|
||||
redisDB: 0
|
||||
@@ -3386,7 +3351,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
onClick: () => {
|
||||
addTab({
|
||||
id: `redis-monitor-${id}-db${redisDB}-${Date.now()}`,
|
||||
title: `监控: ${connections.find(c => c.id === id)?.name || id}`,
|
||||
title: `监控 - db${redisDB}`,
|
||||
type: 'redis-monitor',
|
||||
connectionId: id,
|
||||
redisDB: redisDB
|
||||
@@ -3835,6 +3800,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ padding: '8px 14px', borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}` }}>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
ref={searchInputRef}
|
||||
placeholder="搜索..."
|
||||
onChange={onSearch}
|
||||
@@ -4026,7 +3992,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
>
|
||||
<Form form={createDbForm} layout="vertical">
|
||||
<Form.Item name="name" label="数据库名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input />
|
||||
<Input {...noAutoCapInputProps} />
|
||||
</Form.Item>
|
||||
{/* Charset option could be added here */}
|
||||
</Form>
|
||||
@@ -4044,7 +4010,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
>
|
||||
<Form form={renameDbForm} layout="vertical">
|
||||
<Form.Item name="newName" label="新数据库名称" rules={[{ required: true, message: '请输入新数据库名称' }]}>
|
||||
<Input />
|
||||
<Input {...noAutoCapInputProps} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
@@ -4061,7 +4027,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
>
|
||||
<Form form={renameTableForm} layout="vertical">
|
||||
<Form.Item name="newName" label="新表名" rules={[{ required: true, message: '请输入新表名' }]}>
|
||||
<Input />
|
||||
<Input {...noAutoCapInputProps} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
@@ -4078,7 +4044,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
>
|
||||
<Form form={renameViewForm} layout="vertical">
|
||||
<Form.Item name="newName" label="新视图名" rules={[{ required: true, message: '请输入新视图名' }]}>
|
||||
<Input />
|
||||
<Input {...noAutoCapInputProps} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -17,24 +17,7 @@ import TriggerViewer from './TriggerViewer';
|
||||
import DefinitionViewer from './DefinitionViewer';
|
||||
import TableOverview from './TableOverview';
|
||||
import type { TabData } from '../types';
|
||||
|
||||
const detectConnectionEnvLabel = (connectionName: string): string | null => {
|
||||
const tokens = connectionName.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
||||
if (tokens.includes('prod') || tokens.includes('production')) return 'PROD';
|
||||
if (tokens.includes('uat')) return 'UAT';
|
||||
if (tokens.includes('dev') || tokens.includes('development')) return 'DEV';
|
||||
if (tokens.includes('sit')) return 'SIT';
|
||||
if (tokens.includes('stg') || tokens.includes('stage') || tokens.includes('staging') || tokens.includes('pre')) return 'STG';
|
||||
if (tokens.includes('test') || tokens.includes('qa')) return 'TEST';
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildTabDisplayTitle = (tab: TabData, connectionName: string | undefined): string => {
|
||||
if (tab.type !== 'table' && tab.type !== 'design' && tab.type !== 'table-overview') return tab.title;
|
||||
if (!connectionName) return tab.title;
|
||||
const prefix = detectConnectionEnvLabel(connectionName) || connectionName;
|
||||
return `[${prefix}] ${tab.title}`;
|
||||
};
|
||||
import { buildTabDisplayTitle } from '../utils/tabDisplay';
|
||||
|
||||
type SortableTabLabelProps = {
|
||||
displayTitle: string;
|
||||
@@ -50,7 +33,7 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
|
||||
<span
|
||||
className="tab-dnd-label"
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
title="拖拽调整标签顺序"
|
||||
title={displayTitle}
|
||||
>
|
||||
{displayTitle}
|
||||
</span>
|
||||
@@ -198,8 +181,8 @@ const TabManager: React.FC = () => {
|
||||
);
|
||||
|
||||
const items = useMemo(() => tabs.map((tab, index) => {
|
||||
const connectionName = connections.find((conn) => conn.id === tab.connectionId)?.name;
|
||||
const displayTitle = buildTabDisplayTitle(tab, connectionName);
|
||||
const connection = connections.find((conn) => conn.id === tab.connectionId);
|
||||
const displayTitle = buildTabDisplayTitle(tab, connection);
|
||||
const tabIsActive = tab.id === activeTabId;
|
||||
let content;
|
||||
if (tab.type === 'query') {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, D
|
||||
import { hasIndexFormChanged, normalizeIndexFormFromRow, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils';
|
||||
import { buildAlterTablePreviewSql } from './tableDesignerSchemaSql';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
|
||||
interface EditableColumn extends ColumnDefinition {
|
||||
_key: string;
|
||||
@@ -546,7 +547,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
key: 'name',
|
||||
width: 180,
|
||||
render: (text: string, record: EditableColumn) => readOnly ? text : (
|
||||
<Input value={text} onChange={e => handleColumnChange(record._key, 'name', e.target.value)} variant="borderless" />
|
||||
<Input {...noAutoCapInputProps} value={text} onChange={e => handleColumnChange(record._key, 'name', e.target.value)} variant="borderless" />
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -2492,6 +2493,7 @@ END;`;
|
||||
{isNewTable && (
|
||||
<>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="请输入表名"
|
||||
value={newTableName}
|
||||
onChange={e => setNewTableName(e.target.value)}
|
||||
@@ -2805,6 +2807,7 @@ END;`;
|
||||
已选择字段:{selectedColumns.length}
|
||||
</div>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="请输入目标表名"
|
||||
value={copyTableName}
|
||||
onChange={e => setCopyTableName(e.target.value)}
|
||||
@@ -2865,6 +2868,7 @@ END;`;
|
||||
>
|
||||
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder={indexForm.kind === 'PRIMARY' ? '主键索引固定名称:PRIMARY' : '索引名(例如 idx_user_name)'}
|
||||
value={indexForm.name}
|
||||
onChange={(e) => setIndexForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
@@ -2934,6 +2938,7 @@ END;`;
|
||||
>
|
||||
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="外键约束名(例如 fk_order_user)"
|
||||
value={foreignKeyForm.constraintName}
|
||||
onChange={(e) => setForeignKeyForm(prev => ({ ...prev, constraintName: e.target.value }))}
|
||||
@@ -2949,6 +2954,7 @@ END;`;
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="参考表(支持 db.table)"
|
||||
value={foreignKeyForm.refTableName}
|
||||
onChange={(e) => setForeignKeyForm(prev => ({ ...prev, refTableName: e.target.value }))}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DBQuery, DBShowCreateTable, ExportTable, DropTable, RenameTable } from
|
||||
import type { TabData } from '../types';
|
||||
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
|
||||
|
||||
interface TableOverviewProps {
|
||||
@@ -344,6 +345,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
title: '重命名表',
|
||||
content: (
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
defaultValue={tableName}
|
||||
onChange={e => { newName = e.target.value; }}
|
||||
placeholder="输入新表名"
|
||||
@@ -417,6 +419,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="搜索表名或注释..."
|
||||
prefix={<SearchOutlined style={{ color: textMuted }} />}
|
||||
value={searchText}
|
||||
|
||||
@@ -25,4 +25,9 @@ describe('buildRedisWorkbenchTheme', () => {
|
||||
expect(lightTheme.statusTagBg).not.toBe(lightTheme.statusTagMutedBg);
|
||||
expect(lightTheme.backdropFilter).toBe('none');
|
||||
});
|
||||
|
||||
it('can disable redis workbench blur for macOS text-entry compatibility', () => {
|
||||
const darkTheme = buildRedisWorkbenchTheme({ darkMode: true, opacity: 0.72, blur: 14, disableBackdropFilter: true });
|
||||
expect(darkTheme.backdropFilter).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { resolveTextInputSafeBackdropFilter } from '../utils/appearance';
|
||||
|
||||
type RedisWorkbenchThemeInput = {
|
||||
darkMode: boolean;
|
||||
opacity: number;
|
||||
blur: number;
|
||||
disableBackdropFilter?: boolean;
|
||||
};
|
||||
|
||||
type RedisWorkbenchTheme = {
|
||||
@@ -43,10 +46,15 @@ export const buildRedisWorkbenchTheme = ({
|
||||
darkMode,
|
||||
opacity,
|
||||
blur,
|
||||
disableBackdropFilter,
|
||||
}: RedisWorkbenchThemeInput): RedisWorkbenchTheme => {
|
||||
const normalizedOpacity = clamp(opacity, 0.1, 1);
|
||||
const normalizedBlur = Math.max(0, Math.round(blur));
|
||||
const isTranslucent = normalizedOpacity < 0.999 || normalizedBlur > 0;
|
||||
const backdropFilter = resolveTextInputSafeBackdropFilter(
|
||||
normalizedBlur > 0 ? `blur(${normalizedBlur}px)` : 'none',
|
||||
disableBackdropFilter ?? false,
|
||||
);
|
||||
|
||||
if (darkMode) {
|
||||
const appTopAlpha = isTranslucent ? Math.max(0.08, Math.min(0.22, normalizedOpacity * 0.16)) : 0.92;
|
||||
@@ -84,7 +92,7 @@ export const buildRedisWorkbenchTheme = ({
|
||||
treeSelectedBorder: 'rgba(246, 196, 83, 0.24)',
|
||||
divider: 'rgba(255, 255, 255, 0.07)',
|
||||
shadow: '0 20px 48px rgba(0, 0, 0, 0.26)',
|
||||
backdropFilter: normalizedBlur > 0 ? `blur(${normalizedBlur}px)` : 'none',
|
||||
backdropFilter,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -122,7 +130,7 @@ export const buildRedisWorkbenchTheme = ({
|
||||
treeSelectedBorder: 'rgba(22, 119, 255, 0.18)',
|
||||
divider: 'rgba(15, 23, 42, 0.08)',
|
||||
shadow: '0 22px 52px rgba(15, 23, 42, 0.08)',
|
||||
backdropFilter: normalizedBlur > 0 ? `blur(${normalizedBlur}px)` : 'none',
|
||||
backdropFilter,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user