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/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..b623656 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,20 @@ function App() {
const [sidebarVisible, setSidebarVisible] = useState(true);
const savingDisabledRef = useRef(false);
+ // Load config from URL hash if present
+ useEffect(() => {
+ 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);
+ if (data?.globalParsingConfig && data?.uploadedFiles) {
+ setGlobalParsingConfig(data.globalParsingConfig);
+ setUploadedFiles(data.uploadedFiles);
+ }
+ }
+ }, []);
+
// Persist configuration to localStorage
useEffect(() => {
if (savingDisabledRef.current) return;
@@ -182,6 +197,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 +416,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..4c54f9c
--- /dev/null
+++ b/src/utils/__tests__/shareConfig.test.js
@@ -0,0 +1,27 @@
+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('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
new file mode 100644
index 0000000..793edcc
--- /dev/null
+++ b/src/utils/shareConfig.js
@@ -0,0 +1,16 @@
+import LZString from 'lz-string';
+
+export function encodeConfig(data) {
+ const json = JSON.stringify(data);
+ return LZString.compressToEncodedURIComponent(json);
+}
+
+export function decodeConfig(encoded) {
+ try {
+ const json = LZString.decompressFromEncodedURIComponent(encoded);
+ return json ? JSON.parse(json) : null;
+ } catch (e) {
+ console.error('Failed to decode config', e);
+ return null;
+ }
+}