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;