-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathExportMenu.tsx
More file actions
88 lines (80 loc) · 3.58 KB
/
Copy pathExportMenu.tsx
File metadata and controls
88 lines (80 loc) · 3.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { useState, useRef, useEffect } from 'react'
import { Download, FileText, Braces, Image, Check, Loader2 } from 'lucide-react'
import type { LogEntry } from '../types'
import { exportEntries, saveSnapshot } from '../utils/export'
interface ExportMenuProps {
entries: LogEntry[]
/** Short label used in the snapshot filename, e.g. "metrics" or "logs". */
snapshotLabel: string
/** Whether to offer the PNG snapshot option (the current view is visual). */
allowSnapshot?: boolean
}
export function ExportMenu({ entries, snapshotLabel, allowSnapshot = true }: ExportMenuProps) {
const [open, setOpen] = useState(false)
const [busy, setBusy] = useState<string | null>(null)
const [done, setDone] = useState<string | null>(null)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!open) return
const onDown = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
}
document.addEventListener('mousedown', onDown)
return () => document.removeEventListener('mousedown', onDown)
}, [open])
const run = async (kind: string, fn: () => Promise<string | null>) => {
setBusy(kind)
try {
const result = await fn()
if (result) {
setDone(kind)
setTimeout(() => setDone(null), 2000)
}
} catch (err) {
console.error('export failed:', err)
} finally {
setBusy(null)
setOpen(false)
}
}
const disabled = entries.length === 0
const snapshotSupported = allowSnapshot && !!window.electronAPI?.captureSnapshot
const items = [
{ kind: 'csv', label: 'Export as CSV', sub: `${entries.length.toLocaleString()} rows`, icon: FileText, fn: () => exportEntries(entries, 'csv'), show: true },
{ kind: 'json', label: 'Export as JSON', sub: `${entries.length.toLocaleString()} entries`, icon: Braces, fn: () => exportEntries(entries, 'json'), show: true },
{ kind: 'png', label: 'Save snapshot (PNG)', sub: 'Picture of this view', icon: Image, fn: () => saveSnapshot(snapshotLabel), show: snapshotSupported },
].filter(i => i.show)
return (
<div className="relative" ref={ref} style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
<button
onClick={() => setOpen(o => !o)}
disabled={disabled}
className="flex items-center gap-2 px-3 py-1.5 bg-surface border border-border text-body text-xs rounded-lg hover:bg-panel transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
title={disabled ? 'Fetch logs first' : 'Export logs or save a snapshot'}
>
{done ? <Check size={12} className="text-ok" /> : <Download size={12} />}
{done ? 'Saved' : 'Export'}
</button>
{open && !disabled && (
<div className="absolute right-0 mt-1.5 w-56 bg-panel border border-border rounded-lg shadow-2xl py-1 z-50">
{items.map(({ kind, label, sub, icon: Icon, fn }) => (
<button
key={kind}
onClick={() => run(kind, fn)}
disabled={!!busy}
className="w-full flex items-center gap-3 px-3 py-2 text-left hover:bg-surface transition-colors disabled:opacity-50"
>
{busy === kind
? <Loader2 size={14} className="text-accent animate-spin shrink-0" />
: <Icon size={14} className="text-accent shrink-0" />}
<div className="min-w-0">
<div className="text-xs text-body font-medium">{label}</div>
<div className="text-[10px] text-dim">{sub}</div>
</div>
</button>
))}
</div>
)}
</div>
)
}