Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions public/locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"chart.diffStats": "差值统计",
"chart": "图表",
"resetConfig": "重置配置",
"shareConfig": "分享配置",
"shareConfig.copied": "链接已复制到剪贴板",
"importConfig": "导入配置",
"importConfig.error": "配置文件无效",
"useStepKeyword": "使用 Step 关键字",
"placeholder.step": "step:",
"resize.drag": "拖拽调整图表高度",
Expand Down
71 changes: 71 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -366,6 +416,27 @@ function App() {
>
{t('resetConfig')}
</button>
<button
onClick={handleShareConfig}
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
aria-label={t('shareConfig')}
>
{t('shareConfig')}
</button>
<input
id="import-config-input"
type="file"
accept="application/json"
onChange={handleImportConfigFile}
className="hidden"
/>
<button
onClick={() => document.getElementById('import-config-input').click()}
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
aria-label={t('importConfig')}
>
{t('importConfig')}
</button>
</div>
</div>

Expand Down
27 changes: 27 additions & 0 deletions src/utils/__tests__/shareConfig.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
16 changes: 16 additions & 0 deletions src/utils/shareConfig.js
Original file line number Diff line number Diff line change
@@ -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;
}
}