From 471978a66662c16aa604e817cdb963d02386e2c3 Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:31:31 +0800 Subject: [PATCH 1/3] test: add share config utilities --- public/locales/en/translation.json | 4 ++ public/locales/zh/translation.json | 4 ++ src/App.jsx | 70 +++++++++++++++++++++++++ src/utils/__tests__/shareConfig.test.js | 18 +++++++ src/utils/shareConfig.js | 20 +++++++ 5 files changed, 116 insertions(+) create mode 100644 src/utils/__tests__/shareConfig.test.js create mode 100644 src/utils/shareConfig.js diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 1aa629c..9afc026 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -29,6 +29,10 @@ "chart.diffStats": "Difference Statistics", "chart": "chart", "resetConfig": "Reset Config", + "shareConfig": "Share Config", + "shareConfig.copied": "URL copied to clipboard", + "importConfig": "Import Config", + "importConfig.error": "Invalid configuration file", "useStepKeyword": "Use Step keyword", "placeholder.step": "step:", "resize.drag": "Drag to resize chart height", diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 8ae331d..486ef83 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -29,6 +29,10 @@ "chart.diffStats": "差值统计", "chart": "图表", "resetConfig": "重置配置", + "shareConfig": "分享配置", + "shareConfig.copied": "链接已复制到剪贴板", + "importConfig": "导入配置", + "importConfig.error": "配置文件无效", "useStepKeyword": "使用 Step 关键字", "placeholder.step": "step:", "resize.drag": "拖拽调整图表高度", diff --git a/src/App.jsx b/src/App.jsx index 961d291..a95558f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,6 +10,7 @@ import { ThemeToggle } from './components/ThemeToggle'; import { Header } from './components/Header'; import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'; import { mergeFilesWithReplacement } from './utils/mergeFiles.js'; +import { encodeConfig, decodeConfig } from './utils/shareConfig.js'; // Default global parsing configuration export const DEFAULT_GLOBAL_PARSING_CONFIG = { @@ -56,6 +57,19 @@ function App() { const [sidebarVisible, setSidebarVisible] = useState(true); const savingDisabledRef = useRef(false); + // Load config from URL if present + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const cfg = params.get('config'); + if (cfg) { + const data = decodeConfig(cfg); + if (data?.globalParsingConfig && data?.uploadedFiles) { + setGlobalParsingConfig(data.globalParsingConfig); + setUploadedFiles(data.uploadedFiles); + } + } + }, []); + // Persist configuration to localStorage useEffect(() => { if (savingDisabledRef.current) return; @@ -182,6 +196,41 @@ function App() { }, 0); }, []); + const handleShareConfig = useCallback(() => { + const data = encodeConfig({ globalParsingConfig, uploadedFiles }); + const url = `${window.location.origin}${window.location.pathname}?config=${data}`; + if (navigator?.clipboard?.writeText) { + navigator.clipboard.writeText(url).then(() => { + alert(t('shareConfig.copied')); + }).catch(() => { + window.prompt('', url); + }); + } else { + window.prompt('', url); + } + }, [globalParsingConfig, uploadedFiles, t]); + + const handleImportConfigFile = useCallback((e) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + try { + const json = JSON.parse(ev.target.result); + if (json.globalParsingConfig && json.uploadedFiles) { + setGlobalParsingConfig(json.globalParsingConfig); + setUploadedFiles(json.uploadedFiles); + } else { + alert(t('importConfig.error')); + } + } catch { + alert(t('importConfig.error')); + } + }; + reader.readAsText(file); + e.target.value = ''; + }, [t]); + // Global drag event handlers const handleGlobalDragEnter = useCallback((e) => { e.preventDefault(); @@ -366,6 +415,27 @@ function App() { > {t('resetConfig')} + + + diff --git a/src/utils/__tests__/shareConfig.test.js b/src/utils/__tests__/shareConfig.test.js new file mode 100644 index 0000000..764c497 --- /dev/null +++ b/src/utils/__tests__/shareConfig.test.js @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest'; +import { encodeConfig, decodeConfig } from '../shareConfig.js'; + +describe('shareConfig', () => { + it('encodes and decodes data', () => { + const data = { + globalParsingConfig: { metrics: [], useStepKeyword: false, stepKeyword: 'step:' }, + uploadedFiles: [{ name: 'a.log', content: 'loss:1', enabled: true }] + }; + const encoded = encodeConfig(data); + const decoded = decodeConfig(encoded); + expect(decoded).toEqual(data); + }); + + it('returns null for invalid input', () => { + expect(decodeConfig('%')).toBeNull(); + }); +}); diff --git a/src/utils/shareConfig.js b/src/utils/shareConfig.js new file mode 100644 index 0000000..3386d6e --- /dev/null +++ b/src/utils/shareConfig.js @@ -0,0 +1,20 @@ +export function encodeConfig(data) { + const json = JSON.stringify(data); + const base64 = typeof btoa === 'function' + ? btoa(json) + : Buffer.from(json, 'utf-8').toString('base64'); + return encodeURIComponent(base64); +} + +export function decodeConfig(encoded) { + try { + const base64 = decodeURIComponent(encoded); + const json = typeof atob === 'function' + ? atob(base64) + : Buffer.from(base64, 'base64').toString('utf-8'); + return JSON.parse(json); + } catch (e) { + console.error('Failed to decode config', e); + return null; + } +} From 95e873f898ff66449f4deb612bdf84d5b20a91df Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:10:24 +0800 Subject: [PATCH 2/3] fix: load shared config from URL hash --- src/App.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index a95558f..b623656 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -57,9 +57,10 @@ function App() { const [sidebarVisible, setSidebarVisible] = useState(true); const savingDisabledRef = useRef(false); - // Load config from URL if present + // Load config from URL hash if present useEffect(() => { - const params = new URLSearchParams(window.location.search); + const hash = window.location.hash.slice(1); // remove leading '#' + const params = new URLSearchParams(hash); const cfg = params.get('config'); if (cfg) { const data = decodeConfig(cfg); @@ -198,7 +199,7 @@ function App() { const handleShareConfig = useCallback(() => { const data = encodeConfig({ globalParsingConfig, uploadedFiles }); - const url = `${window.location.origin}${window.location.pathname}?config=${data}`; + const url = `${window.location.origin}${window.location.pathname}#config=${data}`; if (navigator?.clipboard?.writeText) { navigator.clipboard.writeText(url).then(() => { alert(t('shareConfig.copied')); From 23b5f5f112510885a37725ff6965d57db79c0af2 Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Tue, 26 Aug 2025 18:47:41 +0800 Subject: [PATCH 3/3] chore: compress shared config URLs --- package-lock.json | 3 +-- package.json | 3 ++- src/utils/__tests__/shareConfig.test.js | 9 +++++++++ src/utils/shareConfig.js | 14 +++++--------- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d2fb4d..1312fc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "chartjs-plugin-zoom": "^2.2.0", "i18next": "^25.4.2", "lucide-react": "^0.522.0", + "lz-string": "^1.5.0", "postcss": "^8.5.6", "react": "^19.1.0", "react-chartjs-2": "^5.3.0", @@ -4029,9 +4030,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } diff --git a/package.json b/package.json index 693739c..b3c7a52 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", "react-i18next": "^15.7.2", - "tailwindcss": "^3.4.4" + "tailwindcss": "^3.4.4", + "lz-string": "^1.5.0" }, "devDependencies": { "@eslint/js": "^9.25.0", diff --git a/src/utils/__tests__/shareConfig.test.js b/src/utils/__tests__/shareConfig.test.js index 764c497..4c54f9c 100644 --- a/src/utils/__tests__/shareConfig.test.js +++ b/src/utils/__tests__/shareConfig.test.js @@ -12,6 +12,15 @@ describe('shareConfig', () => { expect(decoded).toEqual(data); }); + it('compresses data compared to base64', () => { + const data = { text: 'a'.repeat(1000) }; + const encoded = encodeConfig(data); + const json = JSON.stringify(data); + const base64 = Buffer.from(json, 'utf-8').toString('base64'); + const raw = encodeURIComponent(base64); + expect(encoded.length).toBeLessThan(raw.length); + }); + it('returns null for invalid input', () => { expect(decodeConfig('%')).toBeNull(); }); diff --git a/src/utils/shareConfig.js b/src/utils/shareConfig.js index 3386d6e..793edcc 100644 --- a/src/utils/shareConfig.js +++ b/src/utils/shareConfig.js @@ -1,18 +1,14 @@ +import LZString from 'lz-string'; + export function encodeConfig(data) { const json = JSON.stringify(data); - const base64 = typeof btoa === 'function' - ? btoa(json) - : Buffer.from(json, 'utf-8').toString('base64'); - return encodeURIComponent(base64); + return LZString.compressToEncodedURIComponent(json); } export function decodeConfig(encoded) { try { - const base64 = decodeURIComponent(encoded); - const json = typeof atob === 'function' - ? atob(base64) - : Buffer.from(base64, 'base64').toString('utf-8'); - return JSON.parse(json); + const json = LZString.decompressFromEncodedURIComponent(encoded); + return json ? JSON.parse(json) : null; } catch (e) { console.error('Failed to decode config', e); return null;