From 3aeb7d675d68e8706a9a8e0b3c98ddcb4b1dee46 Mon Sep 17 00:00:00 2001 From: DBT Date: Thu, 26 Feb 2026 02:40:59 +0000 Subject: [PATCH] webui config: hot-only field filter, array sub-editors, and save diff preview --- webui/src/components/RecursiveConfig.tsx | 17 +++++- webui/src/pages/Config.tsx | 76 +++++++++++++++++++++++- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/webui/src/components/RecursiveConfig.tsx b/webui/src/components/RecursiveConfig.tsx index d4e9ed7..498aff1 100644 --- a/webui/src/components/RecursiveConfig.tsx +++ b/webui/src/components/RecursiveConfig.tsx @@ -6,9 +6,19 @@ interface RecursiveConfigProps { labels: Record; path?: string; onChange: (path: string, val: any) => void; + hotPaths?: string[]; + onlyHot?: boolean; } const isPrimitive = (v: any) => ['string', 'number', 'boolean'].includes(typeof v) || v === null; +const isPathHot = (currentPath: string, hotPaths: string[]) => { + if (!hotPaths.length) return true; + return hotPaths.some((hp) => { + const p = String(hp || '').replace(/\.\*$/, ''); + if (!p) return false; + return currentPath === p || currentPath.startsWith(`${p}.`) || p.startsWith(`${currentPath}.`); + }); +}; const PrimitiveArrayEditor: React.FC<{ value: any[]; @@ -96,7 +106,7 @@ const PrimitiveArrayEditor: React.FC<{ ); }; -const RecursiveConfig: React.FC = ({ data, labels, path = '', onChange }) => { +const RecursiveConfig: React.FC = ({ data, labels, path = '', onChange, hotPaths = [], onlyHot = false }) => { const { t } = useTranslation(); if (typeof data !== 'object' || data === null) return null; @@ -106,6 +116,9 @@ const RecursiveConfig: React.FC = ({ data, labels, path = {Object.entries(data).map(([key, value]) => { const currentPath = path ? `${path}.${key}` : key; const label = labels[key] || key.replace(/_/g, ' '); + if (onlyHot && !isPathHot(currentPath, hotPaths)) { + return null; + } if (Array.isArray(value)) { const allPrimitive = value.every(isPrimitive); @@ -149,7 +162,7 @@ const RecursiveConfig: React.FC = ({ data, labels, path = {label}
- +
); diff --git a/webui/src/pages/Config.tsx b/webui/src/pages/Config.tsx index 0dff63d..3752042 100644 --- a/webui/src/pages/Config.tsx +++ b/webui/src/pages/Config.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { RefreshCw, Save } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; @@ -47,6 +47,42 @@ const Config: React.FC = () => { const [selectedTop, setSelectedTop] = useState(''); const activeTop = filteredTopKeys.includes(selectedTop) ? selectedTop : (filteredTopKeys[0] || ''); + const [baseline, setBaseline] = useState(null); + const [showDiff, setShowDiff] = useState(false); + + const currentPayload = useMemo(() => { + if (showRaw) { + try { return JSON.parse(cfgRaw); } catch { return cfg; } + } + return cfg; + }, [showRaw, cfgRaw, cfg]); + + const diffRows = useMemo(() => { + const out: Array<{ path: string; before: any; after: any }> = []; + const walk = (a: any, b: any, p: string) => { + const keys = new Set([...(a && typeof a === 'object' ? Object.keys(a) : []), ...(b && typeof b === 'object' ? Object.keys(b) : [])]); + if (keys.size === 0) { + if (JSON.stringify(a) !== JSON.stringify(b)) out.push({ path: p || '(root)', before: a, after: b }); + return; + } + keys.forEach((k) => { + const pa = p ? `${p}.${k}` : k; + const av = a ? a[k] : undefined; + const bv = b ? b[k] : undefined; + const bothObj = av && bv && typeof av === 'object' && typeof bv === 'object' && !Array.isArray(av) && !Array.isArray(bv); + if (bothObj) walk(av, bv, pa); + else if (JSON.stringify(av) !== JSON.stringify(bv)) out.push({ path: pa, before: av, after: bv }); + }); + }; + walk(baseline || {}, currentPayload || {}, ''); + return out; + }, [baseline, currentPayload]); + + useEffect(() => { + if (baseline == null && cfg && Object.keys(cfg).length > 0) { + setBaseline(JSON.parse(JSON.stringify(cfg))); + } + }, [cfg, baseline]); async function saveConfig() { try { @@ -55,6 +91,8 @@ const Config: React.FC = () => { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); alert(await r.text()); + setBaseline(JSON.parse(JSON.stringify(payload))); + setShowDiff(false); } catch (e) { alert('Failed to save config: ' + e); } @@ -71,9 +109,10 @@ const Config: React.FC = () => {
- + @@ -123,6 +162,8 @@ const Config: React.FC = () => { data={(cfg as any)?.[activeTop] || {}} labels={t('configLabels', { returnObjects: true }) as Record} path={activeTop} + hotPaths={hotReloadFieldDetails.map((x) => x.path)} + onlyHot={hotOnly} onChange={(path, val) => setCfg(v => setPath(v, path, val))} /> ) : ( @@ -139,6 +180,37 @@ const Config: React.FC = () => { /> )}
+ + {showDiff && ( +
+
+
+
配置差异预览({diffRows.length}项)
+ +
+
+ + + + + + + + + + {diffRows.map((r, i) => ( + + + + + + ))} + +
PathBeforeAfter
{r.path}{JSON.stringify(r.before)}{JSON.stringify(r.after)}
+
+
+
+ )} ); };