From 0e06c74b540f715b482420050d9036e85201f610 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 27 Jul 2026 17:22:06 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20style(redis):=20=E6=89=81?= =?UTF-8?q?=E5=B9=B3=E5=8C=96=E5=B7=A5=E4=BD=9C=E5=8F=B0=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E5=88=86=E9=9A=94=E4=B8=8E=E6=82=AC=E6=B5=AE=E7=BB=86?= =?UTF-8?q?=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 V2 Redis 工作台改为连续网格布局并移除卡牌边框与阴影 - 让左右面板分隔线跨行对齐并同步网格宽度变量 - 扩大分隔线拖动热区且保持视觉线宽为 1px - 优化长 Key 省略布局和悬浮查看触发区域 Refs #727 --- .../components/RedisResizableDivider.test.tsx | 29 ++++ .../src/components/RedisResizableDivider.tsx | 18 ++- .../RedisViewer.interaction.test.tsx | 8 ++ frontend/src/components/RedisViewer.tsx | 126 ++++++++++-------- frontend/src/styles/v2-theme-workbench.css | 108 +++++++++++---- 5 files changed, 209 insertions(+), 80 deletions(-) diff --git a/frontend/src/components/RedisResizableDivider.test.tsx b/frontend/src/components/RedisResizableDivider.test.tsx index 33bba165..92992e5a 100644 --- a/frontend/src/components/RedisResizableDivider.test.tsx +++ b/frontend/src/components/RedisResizableDivider.test.tsx @@ -113,6 +113,8 @@ describe('RedisResizableDivider interaction cleanup', () => { }); it('removes the full-screen overlay and commits the width when the window blurs', () => { + expect(renderer?.root.findByType('div').props.className).toBe('redis-resizable-divider'); + startResize(); expect(overlays).toHaveLength(1); @@ -142,6 +144,33 @@ describe('RedisResizableDivider interaction cleanup', () => { expect(onResizeEnd).toHaveBeenCalledWith(420); }); + it('updates a container grid width variable while dragging', () => { + const setProperty = vi.fn(); + target.parentElement = { offsetWidth: 1200, style: { setProperty } } as any; + act(() => { + renderer?.unmount(); + renderer = create( + } + onResizeEnd={onResizeEnd} + maxReservedWidth={361} + containerWidthCssVariable="--redis-sidebar-width" + title="resize" + />, + ); + }); + + startResize(); + act(() => fakeDocument.dispatch('mousemove', { + buttons: 1, + clientX: 520, + preventDefault: vi.fn(), + })); + + expect(setProperty).toHaveBeenCalledWith('--redis-sidebar-width', '540px'); + expect(target.style.width).toBe(''); + }); + it('removes the overlay without updating state when unmounted mid-resize', () => { startResize(); diff --git a/frontend/src/components/RedisResizableDivider.tsx b/frontend/src/components/RedisResizableDivider.tsx index b60604ff..d3b36cd8 100644 --- a/frontend/src/components/RedisResizableDivider.tsx +++ b/frontend/src/components/RedisResizableDivider.tsx @@ -4,6 +4,8 @@ type RedisResizableDividerProps = { onResizeEnd: (newWidth: number) => void; targetRef: React.RefObject; minWidth?: number; + maxReservedWidth?: number; + containerWidthCssVariable?: `--${string}`; title: string; }; @@ -12,6 +14,8 @@ const RedisResizableDivider: React.FC = ({ onResizeEnd, targetRef, minWidth = 300, + maxReservedWidth = 350, + containerWidthCssVariable, title, }) => { const abortInteractionRef = useRef<(() => void) | null>(null); @@ -31,8 +35,9 @@ const RedisResizableDivider: React.FC = ({ const startX = event.clientX; const startWidth = target.offsetWidth; - const containerWidth = target.parentElement?.offsetWidth || window.innerWidth; - const maxWidth = containerWidth - 350; + const container = target.parentElement; + const containerWidth = container?.offsetWidth || window.innerWidth; + const maxWidth = containerWidth - maxReservedWidth; const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;cursor:col-resize;z-index:9999;'; document.body.appendChild(overlay); @@ -63,8 +68,12 @@ const RedisResizableDivider: React.FC = ({ moveEvent.preventDefault(); const delta = moveEvent.clientX - startX; currentWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + delta)); - target.style.width = `${currentWidth}px`; - target.style.flexBasis = `${currentWidth}px`; + if (containerWidthCssVariable && container) { + container.style.setProperty(containerWidthCssVariable, `${currentWidth}px`); + } else { + target.style.width = `${currentWidth}px`; + target.style.flexBasis = `${currentWidth}px`; + } }; abortInteractionRef.current = abortInteraction; @@ -75,6 +84,7 @@ const RedisResizableDivider: React.FC = ({ return (
{ await flushEffects(); const header = renderer!.root.findByProps({ className: 'redis-key-detail-header' }); + const top = renderer!.root.findByProps({ className: 'redis-key-detail-top' }); + const viewMode = renderer!.root.findByProps({ className: 'redis-key-view-mode' }); const summary = renderer!.root.findByProps({ className: 'redis-key-detail-summary' }); const identity = renderer!.root.findByProps({ className: 'redis-key-detail-identity' }); const metadata = renderer!.root.findByProps({ className: 'redis-key-detail-metadata' }); const actions = renderer!.root.findByProps({ className: 'redis-key-detail-actions' }); expect(header.props.style).toMatchObject({ flexDirection: 'column' }); + expect(header.parent).toBe(top); + expect(viewMode.parent).toBe(top); expect(summary.props.style).toMatchObject({ minWidth: 0, width: '100%' }); + expect(identity.props.style).toMatchObject({ minWidth: 0, width: '100%' }); expect(identity.parent).toBe(summary); expect(metadata.parent).toBe(summary); expect(actions.parent).toBe(summary); @@ -358,6 +363,9 @@ describe('RedisViewer tree interactions', () => { flexWrap: 'wrap', maxWidth: '100%', }); + const activeKey = renderer!.root.findByProps({ 'data-redis-active-key': 'true' }); + expect(activeKey.props.style).toMatchObject({ flex: '0 1 auto', minWidth: 0, textOverflow: 'ellipsis' }); + expect(activeKey.props.style).not.toHaveProperty('maxWidth'); expect(findButtonByText(renderer!, 'Set TTL')).toBeTruthy(); expect(findButtonByText(renderer!, 'Refresh')).toBeTruthy(); expect(findButtonByText(renderer!, 'Delete Key')).toBeTruthy(); diff --git a/frontend/src/components/RedisViewer.tsx b/frontend/src/components/RedisViewer.tsx index 58d2febf..9ab0aa2e 100644 --- a/frontend/src/components/RedisViewer.tsx +++ b/frontend/src/components/RedisViewer.tsx @@ -2131,58 +2131,60 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => { return (
-
-
- - {tr('redis_viewer.title.active_key')} - -
- - - {selectedKey} - - - -
-
- {keyValue.type} - } style={mutedPillTagStyle}>{formatTTL(keyValue.ttl)} - {keyValue.length > 0 && {tr('redis_viewer.label.length', { count: keyValue.length })}} -
-
- - - - - +
+
+
+ + {tr('redis_viewer.title.active_key')} + +
+ + + {selectedKey} + + + +
+
+ {keyValue.type} + } style={mutedPillTagStyle}>{formatTTL(keyValue.ttl)} + {keyValue.length > 0 && {tr('redis_viewer.label.length', { count: keyValue.length })}} +
+
+ + + + + +
-
-
- {tr('redis_viewer.view.title')} - setViewMode(e.target.value)}> - {tr('redis_viewer.view.auto')} - {tr('redis_viewer.view.text')} - UTF-8 - {tr('redis_viewer.view.hex')} - +
+ {tr('redis_viewer.view.title')} + setViewMode(e.target.value)}> + {tr('redis_viewer.view.auto')} + {tr('redis_viewer.view.text')} + UTF-8 + {tr('redis_viewer.view.hex')} + +
@@ -2203,7 +2205,19 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => { } return ( -
+
{/* Left: Key List */}
@@ -2332,12 +2346,18 @@ const RedisViewer: React.FC = ({ connectionId, redisDB }) => {
{/* Resizable Divider */} - + {/* Right: Value Viewer */}
{valueLoading ? ( -
{tr('common.loading')}...
+
{tr('common.loading')}...
) : ( renderValueEditor() )} diff --git a/frontend/src/styles/v2-theme-workbench.css b/frontend/src/styles/v2-theme-workbench.css index 8a78d14a..211855a5 100644 --- a/frontend/src/styles/v2-theme-workbench.css +++ b/frontend/src/styles/v2-theme-workbench.css @@ -740,14 +740,23 @@ body[data-ui-version="v2"] .gn-v2-table-designer .ant-table-container { /* ─── V2 Redis workbench ─ */ body[data-ui-version="v2"] .gn-v2-redis-workbench { - gap: 10px !important; - padding: 10px !important; - background: var(--gn-bg-app) !important; + display: grid !important; + grid-template-columns: clamp(320px, var(--gn-redis-sidebar-width, 50%), calc(100% - 361px)) 1px minmax(360px, 1fr); + grid-template-rows: max-content minmax(0, 1fr); + gap: 0 !important; + padding: 0 !important; + background: var(--gn-bg-panel) !important; } body[data-ui-version="v2"] .gn-v2-redis-sidebar { - min-width: 320px !important; - gap: 10px !important; + display: grid !important; + grid-column: 1; + grid-row: 1 / 3; + grid-template-rows: subgrid; + width: 100% !important; + min-width: 0 !important; + gap: 0 !important; + background: var(--gn-bg-panel) !important; } body[data-ui-version="v2"] .gn-v2-redis-header, @@ -755,14 +764,18 @@ body[data-ui-version="v2"] .gn-v2-redis-tree-card, body[data-ui-version="v2"] .gn-v2-redis-value-header, body[data-ui-version="v2"] .gn-v2-redis-value-card, body[data-ui-version="v2"] .gn-v2-redis-empty-value { - border-radius: 8px !important; - border: 0.5px solid var(--gn-br-1) !important; + border: 0 !important; + border-radius: 0 !important; background: var(--gn-bg-panel) !important; - box-shadow: var(--gn-shadow-card) !important; + box-shadow: none !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; } body[data-ui-version="v2"] .gn-v2-redis-header { - padding: 12px !important; + grid-row: 1; + padding: 14px !important; + border-bottom: 1px solid var(--gn-br-1) !important; } body[data-ui-version="v2"] .gn-v2-redis-toolbar { @@ -770,13 +783,17 @@ body[data-ui-version="v2"] .gn-v2-redis-toolbar { } body[data-ui-version="v2"] .gn-v2-redis-tree-card { - padding: 8px !important; + grid-row: 2; + padding: 10px 12px 12px !important; } body[data-ui-version="v2"] .gn-v2-redis-tree-shell { - border-radius: 7px !important; - border: 0.5px solid var(--gn-br-1) !important; - background: var(--gn-bg-panel-2) !important; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; + box-shadow: none !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; } body[data-ui-version="v2"] .gn-v2-redis-workbench .ant-tree-checkbox { @@ -784,15 +801,31 @@ body[data-ui-version="v2"] .gn-v2-redis-workbench .ant-tree-checkbox { } body[data-ui-version="v2"] .gn-v2-redis-value-pane { + display: contents !important; min-width: 360px !important; + background: var(--gn-bg-panel) !important; } body[data-ui-version="v2"] .gn-v2-redis-value-layout { - gap: 10px !important; + display: contents !important; + gap: 0 !important; + background: var(--gn-bg-panel) !important; +} + +body[data-ui-version="v2"] .gn-v2-redis-value-top { + display: flex; + grid-column: 3; + grid-row: 1; + height: 100%; + min-height: 0; + gap: 0 !important; + background: var(--gn-bg-panel); } body[data-ui-version="v2"] .gn-v2-redis-value-header { - padding: 14px !important; + flex: 1 1 auto !important; + padding: 14px 16px !important; + border-bottom: 0 !important; } body[data-ui-version="v2"] .gn-v2-redis-value-header strong { @@ -804,22 +837,51 @@ body[data-ui-version="v2"] .gn-v2-redis-value-actions, body[data-ui-version="v2"] .gn-v2-redis-view-mode, body[data-ui-version="v2"] .gn-v2-redis-value-subtoolbar, body[data-ui-version="v2"] .gn-v2-redis-value-actionbar { - border-radius: 7px !important; - border: 0.5px solid var(--gn-br-1) !important; - background: var(--gn-bg-panel-2) !important; + border: 0 !important; + border-radius: 0 !important; + background: var(--gn-bg-panel) !important; + box-shadow: none !important; } body[data-ui-version="v2"] .gn-v2-redis-value-subtoolbar { min-height: 30px; - border-width: 0 0 0.5px !important; - border-radius: 0 !important; + border-bottom: 1px solid var(--gn-br-1) !important; } body[data-ui-version="v2"] .gn-v2-redis-value-actionbar { - padding: 8px !important; + padding: 10px 12px !important; margin-bottom: 0 !important; - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; + border-bottom: 1px solid var(--gn-br-1) !important; +} + +body[data-ui-version="v2"] .gn-v2-redis-view-mode { + padding: 8px 16px !important; + border-bottom: 1px solid var(--gn-br-1) !important; +} + +body[data-ui-version="v2"] .gn-v2-redis-value-card { + grid-column: 3; + grid-row: 2; + padding: 0 !important; +} + +body[data-ui-version="v2"] .gn-v2-redis-empty-value { + grid-column: 3; + grid-row: 1 / 3; +} + +body[data-ui-version="v2"] .gn-v2-redis-workbench > .redis-resizable-divider { + grid-column: 2; + grid-row: 1 / 3; + position: relative; + width: 1px !important; + background: var(--gn-br-1) !important; +} + +body[data-ui-version="v2"] .gn-v2-redis-workbench > .redis-resizable-divider::before { + position: absolute; + inset: 0 -5px; + content: ''; } body[data-ui-version="v2"] .gn-v2-redis-data-section .ant-table-wrapper {