1- import React from 'react' ;
1+ import React , { useRef , useCallback , useEffect } from 'react' ;
22import { WidgetConfig } from '@/store/useBuilderStore' ;
33import { useDevices } from '@/hooks/useDevices' ;
44import { useMultiNodeLatestData } from '@/hooks/useMultiNodeLatestData' ;
55import { useDeviceHistoricalData } from '@/hooks/useDeviceHistoricalData' ;
66import { Skeleton } from '@/components/ui/skeleton' ;
77import { 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' ;
99import { 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
6882function 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