mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-31 10:29:39 +08:00
🐛 fix(redis): 修复命令页暗色主题显示异常
- 主题适配:Redis 命令输入区、工具栏、拖拽条和输出区统一接入 workbench 主题 - 编辑器修复:Monaco 命令输入框按暗色/亮色切换 transparent 主题 - 输出修复:暗色主题下输出区使用深色背景与可见文字颜色 - 布局修复:限制输入区拖拽高度,避免压缩底部输出区 - 测试覆盖:新增 Redis 命令页布局回归测试
This commit is contained in:
131
frontend/src/components/RedisCommandEditor.layout.test.tsx
Normal file
131
frontend/src/components/RedisCommandEditor.layout.test.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import RedisCommandEditor, {
|
||||
REDIS_COMMAND_EDITOR_MIN_HEIGHT,
|
||||
REDIS_COMMAND_OUTPUT_MIN_HEIGHT,
|
||||
REDIS_COMMAND_RESIZER_HEIGHT,
|
||||
clampRedisCommandEditorHeight,
|
||||
} from './RedisCommandEditor';
|
||||
|
||||
const storeState = vi.hoisted((): any => ({
|
||||
connections: [
|
||||
{
|
||||
id: 'redis-1',
|
||||
name: 'redis',
|
||||
config: {
|
||||
type: 'redis',
|
||||
host: '127.0.0.1',
|
||||
port: 6379,
|
||||
password: '',
|
||||
database: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
theme: 'dark',
|
||||
appearance: {
|
||||
enabled: true,
|
||||
opacity: 1,
|
||||
blur: 0,
|
||||
uiVersion: 'v2',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../store', () => ({
|
||||
useStore: (selector?: (state: typeof storeState) => any) => (
|
||||
selector ? selector(storeState) : storeState
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@monaco-editor/react', async () => {
|
||||
const React = await import('react');
|
||||
return {
|
||||
loader: { config: vi.fn() },
|
||||
default: ({ theme, language }: any) => React.createElement(
|
||||
'div',
|
||||
{
|
||||
'data-monaco-editor': 'true',
|
||||
'data-monaco-theme': theme,
|
||||
'data-language': language,
|
||||
},
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@ant-design/icons', async () => {
|
||||
const React = await import('react');
|
||||
const Icon = () => React.createElement('span', { 'data-icon': 'true' });
|
||||
return {
|
||||
ClearOutlined: Icon,
|
||||
PlayCircleOutlined: Icon,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('antd', async () => {
|
||||
const React = await import('react');
|
||||
const Button = ({ children, icon, loading, size, type, ...props }: any) => React.createElement(
|
||||
'button',
|
||||
props,
|
||||
icon,
|
||||
children,
|
||||
);
|
||||
const Space = ({ children }: any) => React.createElement('div', { 'data-space': 'true' }, children);
|
||||
return {
|
||||
Button,
|
||||
Space,
|
||||
message: {
|
||||
warning: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('RedisCommandEditor layout', () => {
|
||||
beforeEach(() => {
|
||||
storeState.theme = 'dark';
|
||||
storeState.appearance = {
|
||||
enabled: true,
|
||||
opacity: 1,
|
||||
blur: 0,
|
||||
uiVersion: 'v2',
|
||||
};
|
||||
});
|
||||
|
||||
it('renders command input and output panes with dark theme surfaces', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<RedisCommandEditor connectionId="redis-1" redisDB={0} />,
|
||||
);
|
||||
|
||||
expect(markup).toContain('data-redis-command-editor="true"');
|
||||
expect(markup).toContain('data-redis-command-input-pane="true"');
|
||||
expect(markup).toContain('data-redis-command-output-pane="true"');
|
||||
expect(markup).toContain('data-redis-command-output-terminal="true"');
|
||||
expect(markup).toContain('data-monaco-theme="transparent-dark"');
|
||||
expect(markup).toContain('background:#111418');
|
||||
expect(markup).not.toContain('background:#fff');
|
||||
expect(markup).not.toContain('background:#fdfdfd');
|
||||
});
|
||||
|
||||
it('uses the light transparent Monaco theme outside dark mode', () => {
|
||||
storeState.theme = 'light';
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<RedisCommandEditor connectionId="redis-1" redisDB={0} />,
|
||||
);
|
||||
|
||||
expect(markup).toContain('data-monaco-theme="transparent-light"');
|
||||
expect(markup).toContain('color:#0f172a');
|
||||
});
|
||||
|
||||
it('keeps output visible when the command editor is resized', () => {
|
||||
const containerHeight = 900;
|
||||
const maxEditorHeight = containerHeight
|
||||
- REDIS_COMMAND_OUTPUT_MIN_HEIGHT
|
||||
- REDIS_COMMAND_RESIZER_HEIGHT;
|
||||
|
||||
expect(clampRedisCommandEditorHeight(60, containerHeight)).toBe(REDIS_COMMAND_EDITOR_MIN_HEIGHT);
|
||||
expect(clampRedisCommandEditorHeight(700, containerHeight)).toBe(maxEditorHeight);
|
||||
expect(clampRedisCommandEditorHeight(360, containerHeight)).toBe(360);
|
||||
expect(clampRedisCommandEditorHeight(900, undefined)).toBe(800);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,16 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||
import { Button, Space, message } from 'antd';
|
||||
import { PlayCircleOutlined, ClearOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import Editor, { type OnMount } from './MonacoEditor';
|
||||
import {
|
||||
isMacLikePlatform,
|
||||
normalizeBlurForPlatform,
|
||||
normalizeOpacityForPlatform,
|
||||
resolveAppearanceValues,
|
||||
} from '../utils/appearance';
|
||||
import { buildRedisWorkbenchTheme } from './redisViewerWorkbenchTheme';
|
||||
|
||||
interface RedisCommandEditorProps {
|
||||
connectionId: string;
|
||||
@@ -18,6 +25,26 @@ interface CommandResult {
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export const REDIS_COMMAND_EDITOR_MIN_HEIGHT = 120;
|
||||
export const REDIS_COMMAND_OUTPUT_MIN_HEIGHT = 240;
|
||||
export const REDIS_COMMAND_RESIZER_HEIGHT = 8;
|
||||
|
||||
export const clampRedisCommandEditorHeight = (
|
||||
requestedHeight: number,
|
||||
containerHeight: number | undefined,
|
||||
): number => {
|
||||
const minHeight = REDIS_COMMAND_EDITOR_MIN_HEIGHT;
|
||||
const fallbackMaxHeight = 800;
|
||||
const maxHeight = containerHeight
|
||||
? Math.max(
|
||||
minHeight,
|
||||
containerHeight - REDIS_COMMAND_OUTPUT_MIN_HEIGHT - REDIS_COMMAND_RESIZER_HEIGHT,
|
||||
)
|
||||
: fallbackMaxHeight;
|
||||
|
||||
return Math.min(Math.max(requestedHeight, minHeight), maxHeight);
|
||||
};
|
||||
|
||||
// 智能解析 Redis 脚本块,保护多行引号内的换行符
|
||||
function parseRedisScriptBlocks(script: string): string[] {
|
||||
const blocks: string[] = [];
|
||||
@@ -79,8 +106,19 @@ function parseRedisScriptBlocks(script: string): string[] {
|
||||
}
|
||||
|
||||
const RedisCommandEditor: React.FC<RedisCommandEditorProps> = ({ connectionId, redisDB }) => {
|
||||
const { connections } = useStore();
|
||||
const connections = useStore(state => state.connections);
|
||||
const theme = useStore(state => state.theme);
|
||||
const appearance = useStore(state => state.appearance);
|
||||
const connection = connections.find(c => c.id === connectionId);
|
||||
const darkMode = theme === 'dark';
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const blur = normalizeBlurForPlatform(resolvedAppearance.blur);
|
||||
const disableLocalBackdropFilter = isMacLikePlatform();
|
||||
const workbenchTheme = useMemo(
|
||||
() => buildRedisWorkbenchTheme({ darkMode, opacity, blur, disableBackdropFilter: disableLocalBackdropFilter }),
|
||||
[blur, darkMode, disableLocalBackdropFilter, opacity, appearance.uiVersion],
|
||||
);
|
||||
|
||||
const [command, setCommand] = useState('');
|
||||
const [results, setResults] = useState<CommandResult[]>([]);
|
||||
@@ -294,11 +332,11 @@ const RedisCommandEditor: React.FC<RedisCommandEditorProps> = ({ connectionId, r
|
||||
const delta = e.clientY - dragRef.current.startY;
|
||||
let newHeight = dragRef.current.startHeight + delta;
|
||||
|
||||
// 限制高度
|
||||
const minHeight = 100;
|
||||
const maxHeight = containerRef.current ? containerRef.current.clientHeight - 100 : 800;
|
||||
if (newHeight < minHeight) newHeight = minHeight;
|
||||
if (newHeight > maxHeight) newHeight = maxHeight;
|
||||
// 限制输入区高度,避免拖拽后压缩掉底部输出区。
|
||||
newHeight = clampRedisCommandEditorHeight(
|
||||
newHeight,
|
||||
containerRef.current?.clientHeight,
|
||||
);
|
||||
|
||||
setEditorHeight(newHeight);
|
||||
|
||||
@@ -323,13 +361,36 @@ const RedisCommandEditor: React.FC<RedisCommandEditorProps> = ({ connectionId, r
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', background: '#fff' }}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
data-redis-command-editor="true"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
background: workbenchTheme.appBg,
|
||||
color: workbenchTheme.textPrimary,
|
||||
backdropFilter: workbenchTheme.backdropFilter,
|
||||
WebkitBackdropFilter: workbenchTheme.backdropFilter,
|
||||
}}
|
||||
>
|
||||
{/* Editor Top Pane */}
|
||||
<div style={{ height: editorHeight, minHeight: 100, display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#fdfdfd' }}>
|
||||
<div
|
||||
data-redis-command-input-pane="true"
|
||||
style={{
|
||||
height: editorHeight,
|
||||
minHeight: REDIS_COMMAND_EDITOR_MIN_HEIGHT,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: workbenchTheme.panelBg,
|
||||
borderBottom: workbenchTheme.panelBorder,
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '8px 12px', borderBottom: workbenchTheme.panelBorder, display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: workbenchTheme.panelBgStrong }}>
|
||||
<Space>
|
||||
<span style={{ fontWeight: 600 }}>Redis Console</span>
|
||||
<span style={{ color: '#888', fontSize: 13, background: '#f0f0f0', padding: '2px 8px', borderRadius: 12 }}>db{redisDB}</span>
|
||||
<span style={{ fontWeight: 600, color: workbenchTheme.textPrimary }}>Redis Console</span>
|
||||
<span style={{ color: workbenchTheme.textSecondary, fontSize: 13, background: workbenchTheme.statusTagMutedBg, border: workbenchTheme.statusTagMutedBorder, padding: '2px 8px', borderRadius: 12 }}>db{redisDB}</span>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button
|
||||
@@ -342,8 +403,9 @@ const RedisCommandEditor: React.FC<RedisCommandEditorProps> = ({ connectionId, r
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
|
||||
<Editor
|
||||
theme={darkMode ? 'transparent-dark' : 'transparent-light'}
|
||||
defaultLanguage="redis"
|
||||
language="redis"
|
||||
value={command}
|
||||
@@ -366,34 +428,57 @@ const RedisCommandEditor: React.FC<RedisCommandEditorProps> = ({ connectionId, r
|
||||
{/* Resizer Handle */}
|
||||
<div
|
||||
className="horizontal-resizer"
|
||||
data-redis-command-resizer="true"
|
||||
onMouseDown={handleDragStart}
|
||||
style={{
|
||||
height: 8,
|
||||
height: REDIS_COMMAND_RESIZER_HEIGHT,
|
||||
cursor: 'row-resize',
|
||||
background: '#f0f0f0',
|
||||
borderTop: '1px solid #e0e0e0',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
background: workbenchTheme.panelBgStrong,
|
||||
borderTop: workbenchTheme.panelBorder,
|
||||
borderBottom: workbenchTheme.panelBorder,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 40, height: 4, background: '#ccc', borderRadius: 2 }} />
|
||||
<div style={{ width: 40, height: 4, background: workbenchTheme.textMuted, borderRadius: 2, opacity: 0.6 }} />
|
||||
</div>
|
||||
|
||||
{/* Results Terminal Bottom Pane */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div style={{ padding: '4px 12px', background: '#252526', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #333' }}>
|
||||
<span style={{ color: '#ccc', fontSize: 12 }}>Execution Output</span>
|
||||
<Button type="text" size="small" icon={<ClearOutlined />} onClick={handleClear} style={{ color: '#aaa' }}>清空控制台</Button>
|
||||
<div
|
||||
data-redis-command-output-pane="true"
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: REDIS_COMMAND_OUTPUT_MIN_HEIGHT,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
background: darkMode ? '#111418' : workbenchTheme.panelBg,
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '4px 12px', background: darkMode ? '#1b1f27' : workbenchTheme.panelBgStrong, display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: workbenchTheme.panelBorder }}>
|
||||
<span style={{ color: workbenchTheme.textSecondary, fontSize: 12 }}>Execution Output</span>
|
||||
<Button type="text" size="small" icon={<ClearOutlined />} onClick={handleClear} style={{ color: workbenchTheme.textSecondary }}>清空控制台</Button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto', background: '#1e1e1e', color: '#d4d4d4', fontFamily: '"Consolas", "Courier New", monospace', fontSize: 13, padding: 12 }}>
|
||||
<div
|
||||
data-redis-command-output-terminal="true"
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflow: 'auto',
|
||||
background: darkMode ? '#111418' : '#f8fafc',
|
||||
color: darkMode ? '#d4d4d4' : '#0f172a',
|
||||
fontFamily: '"Consolas", "Courier New", monospace',
|
||||
fontSize: 13,
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
{results.length === 0 ? (
|
||||
<div style={{ color: '#666', textAlign: 'center', marginTop: 40 }}>
|
||||
<div style={{ color: workbenchTheme.textMuted, textAlign: 'center', marginTop: 40 }}>
|
||||
<div>在此终端执行命令,结果会以原样输出</div>
|
||||
<div style={{ fontSize: 12, marginTop: 12 }}>
|
||||
Tips: <code>选中任意行</code> 按 <code style={{ color: '#999' }}>Ctrl + Enter</code> 仅执行选中段落
|
||||
Tips: <code>选中任意行</code> 按 <code style={{ color: workbenchTheme.textSecondary }}>Ctrl + Enter</code> 仅执行选中段落
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -402,7 +487,7 @@ const RedisCommandEditor: React.FC<RedisCommandEditorProps> = ({ connectionId, r
|
||||
<div style={{ color: '#569cd6', marginBottom: 6, fontWeight: 'bold' }}>
|
||||
<span style={{ color: '#4CAF50', marginRight: 8 }}>➜</span>
|
||||
{item.command}
|
||||
<span style={{ color: '#666', fontSize: 11, marginLeft: 12, fontWeight: 'normal' }}>[{item.durationMs}ms]</span>
|
||||
<span style={{ color: workbenchTheme.textMuted, fontSize: 11, marginLeft: 12, fontWeight: 'normal' }}>[{item.durationMs}ms]</span>
|
||||
</div>
|
||||
|
||||
<div style={{ paddingLeft: 20 }}>
|
||||
|
||||
Reference in New Issue
Block a user