Skip to content

Commit e4dd830

Browse files
Merge pull request #7 from anedyaio/dev
add export button
2 parents dc349b9 + 033e1f1 commit e4dd830

1 file changed

Lines changed: 90 additions & 10 deletions

File tree

src/components/dashboard-builder/widgets/DataTableWidget.tsx

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React from 'react';
1+
import React, { useRef, useCallback, useEffect } from 'react';
22
import { WidgetConfig } from '@/store/useBuilderStore';
33
import { useDevices } from '@/hooks/useDevices';
44
import { useMultiNodeLatestData } from '@/hooks/useMultiNodeLatestData';
55
import { useDeviceHistoricalData } from '@/hooks/useDeviceHistoricalData';
66
import { Skeleton } from '@/components/ui/skeleton';
77
import { Button } from '@/components/ui/button';
8-
import { ChevronLeft, ChevronRight, AlertTriangle, Table2, RefreshCw, Clock, Layers } from 'lucide-react';
8+
import { ChevronLeft, ChevronRight, AlertTriangle, Table2, RefreshCw, Clock, Layers, Download, ArrowDownToLine } from 'lucide-react';
99
import { cn } from '@/lib/utils';
1010

1111
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -63,6 +63,20 @@ function formatTimestamp(ts: number): string {
6363
});
6464
}
6565

66+
function downloadCsv(filename: string, csvData: string) {
67+
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
68+
const link = document.createElement('a');
69+
if (link.download !== undefined) {
70+
const url = URL.createObjectURL(blob);
71+
link.setAttribute('href', url);
72+
link.setAttribute('download', filename);
73+
link.style.visibility = 'hidden';
74+
document.body.appendChild(link);
75+
link.click();
76+
document.body.removeChild(link);
77+
}
78+
}
79+
6680
// ─── Shared Table Header ──────────────────────────────────────────────────────
6781

6882
function TableHeader({ firstColLabel, variables }: {
@@ -90,9 +104,9 @@ function TableHeader({ firstColLabel, variables }: {
90104

91105
// ─── Home Mode ───────────────────────────────────────────────────────────────
92106

93-
interface HomeTableProps { cfg: DataTableWidgetConfig; pollIntervalMs?: number; }
107+
interface HomeTableProps { cfg: DataTableWidgetConfig; pollIntervalMs?: number; exportRef: React.MutableRefObject<(() => void) | null>; }
94108

95-
function HomeDataTable({ cfg, pollIntervalMs }: HomeTableProps) {
109+
function HomeDataTable({ cfg, pollIntervalMs, exportRef }: HomeTableProps) {
96110
const { data: allDevices = [], isLoading: devicesLoading } = useDevices();
97111
const variables = cfg.variables ?? [];
98112

@@ -109,6 +123,25 @@ function HomeDataTable({ cfg, pollIntervalMs }: HomeTableProps) {
109123
const col5 = useMultiNodeLatestData(nodeIds, variables[5]?.variableKey, pollIntervalMs);
110124
const colDataArr = [col0, col1, col2, col3, col4, col5];
111125

126+
const handleExport = useCallback(() => {
127+
const header = ['Device', ...variables.map(v => `"${v.label}${v.unit ? ` (${v.unit})` : ''}"`)].join(',');
128+
const csvRows = devices.map(device => {
129+
const row = [`"${(device.title || device.node_id).replace(/"/g, '""')}"`];
130+
variables.forEach((col, colIdx) => {
131+
const val = colDataArr[colIdx]?.data?.[device.node_id]?.value;
132+
row.push(val === null || val === undefined ? '' : String(val));
133+
});
134+
return row.join(',');
135+
});
136+
const csvData = [header, ...csvRows].join('\n');
137+
downloadCsv(`latest_data_${new Date().getTime()}.csv`, csvData);
138+
}, [devices, variables, colDataArr]);
139+
140+
useEffect(() => {
141+
exportRef.current = handleExport;
142+
return () => { exportRef.current = null; };
143+
}, [exportRef, handleExport]);
144+
112145
const isRefreshing = colDataArr.slice(0, variables.length).some(c => c.isLoading);
113146

114147
if (devicesLoading) {
@@ -179,15 +212,35 @@ function HomeDataTable({ cfg, pollIntervalMs }: HomeTableProps) {
179212

180213
// ─── Device Mode ─────────────────────────────────────────────────────────────
181214

182-
interface DeviceTableProps { cfg: DataTableWidgetConfig; nodeId: string; pollIntervalMs?: number; }
215+
interface DeviceTableProps { cfg: DataTableWidgetConfig; nodeId: string; pollIntervalMs?: number; exportRef: React.MutableRefObject<(() => void) | null>; }
183216

184-
function DeviceDataTable({ cfg, nodeId, pollIntervalMs }: DeviceTableProps) {
217+
function DeviceDataTable({ cfg, nodeId, pollIntervalMs, exportRef }: DeviceTableProps) {
185218
const variables = cfg.variables ?? [];
186219
const pageSize = cfg.pageSize ?? 20;
187220

188221
const { rows, isLoading, error, page, totalPages, hasNext, hasPrev, goNext, goPrev } =
189222
useDeviceHistoricalData({ nodeId, variables, pageSize, pollIntervalMs });
190223

224+
const handleExport = useCallback(() => {
225+
const header = ['Timestamp', ...variables.map(v => `"${v.label}${v.unit ? ` (${v.unit})` : ''}"`)].join(',');
226+
const csvRows = rows.map(row => {
227+
const ts = formatTimestamp(row.timestamp);
228+
const csvRow = [`"${ts}"`];
229+
variables.forEach(col => {
230+
const val = row.values[col.variableKey];
231+
csvRow.push(val === null || val === undefined ? '' : String(val));
232+
});
233+
return csvRow.join(',');
234+
});
235+
const csvData = [header, ...csvRows].join('\n');
236+
downloadCsv(`historical_data_${new Date().getTime()}.csv`, csvData);
237+
}, [rows, variables]);
238+
239+
useEffect(() => {
240+
exportRef.current = handleExport;
241+
return () => { exportRef.current = null; };
242+
}, [exportRef, handleExport]);
243+
191244
if (variables.length === 0) {
192245
return (
193246
<div className="flex flex-col items-center justify-center h-full gap-3 text-muted-foreground py-10">
@@ -286,15 +339,42 @@ export function DataTableWidget({ config, nodeId, pollIntervalMs, isEditMode }:
286339
const mode: 'home' | 'device' = nodeId ? 'device' : 'home';
287340
const effectivePollMs = isEditMode ? 0 : (pollIntervalMs ?? cfg.refreshIntervalMs ?? 0);
288341

342+
const exportRef = useRef<(() => void) | null>(null);
343+
289344
const headerContent = (
290345
<div className="px-4 py-2.5 border-b border-border/60 flex items-center gap-2.5 flex-none bg-muted/20">
291346
<div className="flex items-center justify-center w-6 h-6 rounded bg-primary/10 flex-none">
292347
<Table2 className="h-3.5 w-3.5 text-primary/70" />
293348
</div>
294-
<span className="text-sm font-semibold truncate text-foreground/90">
349+
<span className="text-sm font-semibold truncate text-foreground/90 flex-1">
295350
{config.title || 'Data Table'}
296351
</span>
297-
352+
{!isEditMode && (
353+
<div className="ml-auto flex items-center gap-2">
354+
{mode === 'home' ? (
355+
<span className="flex items-center gap-1 text-[10px] font-medium text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full uppercase tracking-wider">
356+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
357+
live
358+
</span>
359+
) : (
360+
<span className="flex items-center gap-1 text-[10px] font-medium text-sky-400 bg-sky-500/10 border border-sky-500/20 px-2 py-0.5 rounded-full uppercase tracking-wider">
361+
<Clock className="h-2.5 w-2.5" />
362+
historical
363+
</span>
364+
)}
365+
<div className="w-px h-4 bg-border/60 mx-1" />
366+
<Button
367+
variant="outline"
368+
size="sm"
369+
onClick={() => exportRef.current?.()}
370+
className="gap-2 h-8"
371+
title="Export Data as CSV"
372+
>
373+
<ArrowDownToLine className="h-3 w-3" />
374+
Export
375+
</Button>
376+
</div>
377+
)}
298378
</div>
299379
);
300380

@@ -333,8 +413,8 @@ export function DataTableWidget({ config, nodeId, pollIntervalMs, isEditMode }:
333413
{headerContent}
334414
<div className="flex-1 overflow-hidden">
335415
{mode === 'home'
336-
? <HomeDataTable cfg={cfg} pollIntervalMs={effectivePollMs} />
337-
: <DeviceDataTable cfg={cfg} nodeId={nodeId!} pollIntervalMs={effectivePollMs} />
416+
? <HomeDataTable cfg={cfg} pollIntervalMs={effectivePollMs} exportRef={exportRef} />
417+
: <DeviceDataTable cfg={cfg} nodeId={nodeId!} pollIntervalMs={effectivePollMs} exportRef={exportRef} />
338418
}
339419
</div>
340420
</div>

0 commit comments

Comments
 (0)