@@ -2,6 +2,7 @@ import 'react-datepicker/dist/react-datepicker.css';
2
2
3
3
import { SerdeUsage , TopicMessageConsuming } from 'generated-sources' ;
4
4
import React , { ChangeEvent , useMemo , useState } from 'react' ;
5
+ import { format } from 'date-fns' ;
5
6
import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled' ;
6
7
import Select from 'components/common/Select/Select' ;
7
8
import { Button } from 'components/common/Button/Button' ;
@@ -18,6 +19,7 @@ import EditIcon from 'components/common/Icons/EditIcon';
18
19
import CloseIcon from 'components/common/Icons/CloseIcon' ;
19
20
import FlexBox from 'components/common/FlexBox/FlexBox' ;
20
21
import { useMessageFiltersStore } from 'lib/hooks/useMessageFiltersStore' ;
22
+ import useDataSaver from 'lib/hooks/useDataSaver' ;
21
23
22
24
import * as S from './Filters.styled' ;
23
25
import {
@@ -30,18 +32,37 @@ import {
30
32
import FiltersSideBar from './FiltersSideBar' ;
31
33
import FiltersMetrics from './FiltersMetrics' ;
32
34
35
+ interface MessageData {
36
+ Value : string | undefined ;
37
+ Offset : number ;
38
+ Key : string | undefined ;
39
+ Partition : number ;
40
+ Headers : { [ key : string ] : string | undefined } | undefined ;
41
+ Timestamp : Date ;
42
+ }
43
+
44
+ type DownloadFormat = 'json' | 'csv' ;
45
+
46
+ function padCurrentDateTimeString ( ) : string {
47
+ const now : Date = new Date ( ) ;
48
+ const dateTimeString : string = format ( now , 'yyyy-MM-dd HH:mm:ss' ) ;
49
+ return `_${ dateTimeString } ` ;
50
+ }
51
+
33
52
export interface FiltersProps {
34
53
phaseMessage ?: string ;
35
54
consumptionStats ?: TopicMessageConsuming ;
36
55
isFetching : boolean ;
37
56
abortFetchData : ( ) => void ;
57
+ messages ?: any [ ] ; // Add messages prop for download functionality
38
58
}
39
59
40
60
const Filters : React . FC < FiltersProps > = ( {
41
61
consumptionStats,
42
62
isFetching,
43
63
abortFetchData,
44
64
phaseMessage,
65
+ messages = [ ] ,
45
66
} ) => {
46
67
const { clusterName, topicName } = useAppParams < RouteParamsClusterTopic > ( ) ;
47
68
@@ -69,6 +90,76 @@ const Filters: React.FC<FiltersProps> = ({
69
90
const [ createdEditedSmartId , setCreatedEditedSmartId ] = useState < string > ( ) ;
70
91
const remove = useMessageFiltersStore ( ( state ) => state . remove ) ;
71
92
93
+ // Download functionality
94
+ const [ selectedFormat , setSelectedFormat ] = useState < DownloadFormat > ( 'json' ) ;
95
+ const [ showFormatSelector , setShowFormatSelector ] = useState ( false ) ;
96
+
97
+ const formatOptions = [
98
+ { label : 'Export JSON' , value : 'json' as DownloadFormat } ,
99
+ { label : 'Export CSV' , value : 'csv' as DownloadFormat } ,
100
+ ] ;
101
+
102
+ const baseFileName = `topic-messages${ padCurrentDateTimeString ( ) } ` ;
103
+
104
+ const savedMessagesJson : MessageData [ ] = messages . map ( ( message ) => ( {
105
+ Value : message . content ,
106
+ Offset : message . offset ,
107
+ Key : message . key ,
108
+ Partition : message . partition ,
109
+ Headers : message . headers ,
110
+ Timestamp : message . timestamp ,
111
+ } ) ) ;
112
+
113
+ const convertToCSV = useMemo ( ( ) => {
114
+ return ( messagesData : MessageData [ ] ) => {
115
+ const headers = [
116
+ 'Value' ,
117
+ 'Offset' ,
118
+ 'Key' ,
119
+ 'Partition' ,
120
+ 'Headers' ,
121
+ 'Timestamp' ,
122
+ ] as const ;
123
+ const rows = messagesData . map ( ( msg ) =>
124
+ headers
125
+ . map ( ( header ) => {
126
+ const value = msg [ header ] ;
127
+ if ( header === 'Headers' ) {
128
+ return JSON . stringify ( value || { } ) ;
129
+ }
130
+ return String ( value ?? '' ) ;
131
+ } )
132
+ . join ( ',' )
133
+ ) ;
134
+ return [ headers . join ( ',' ) , ...rows ] . join ( '\n' ) ;
135
+ } ;
136
+ } , [ ] ) ;
137
+
138
+ const jsonSaver = useDataSaver (
139
+ `${ baseFileName } .json` ,
140
+ JSON . stringify ( savedMessagesJson , null , '\t' )
141
+ ) ;
142
+ const csvSaver = useDataSaver (
143
+ `${ baseFileName } .csv` ,
144
+ convertToCSV ( savedMessagesJson )
145
+ ) ;
146
+
147
+ const handleFormatSelect = ( downloadFormat : DownloadFormat ) => {
148
+ setSelectedFormat ( downloadFormat ) ;
149
+ setShowFormatSelector ( false ) ;
150
+
151
+ // Automatically download after format selection
152
+ if ( downloadFormat === 'json' ) {
153
+ jsonSaver . saveFile ( ) ;
154
+ } else {
155
+ csvSaver . saveFile ( ) ;
156
+ }
157
+ } ;
158
+
159
+ const handleDownloadClick = ( ) => {
160
+ setShowFormatSelector ( ! showFormatSelector ) ;
161
+ } ;
162
+
72
163
const partitions = useMemo ( ( ) => {
73
164
return ( topic ?. partitions || [ ] ) . reduce < {
74
165
dict : Record < string , { label : string ; value : number } > ;
@@ -187,7 +278,76 @@ const Filters: React.FC<FiltersProps> = ({
187
278
</ Button >
188
279
</ FlexBox >
189
280
190
- < Search placeholder = "Search" value = { search } onChange = { setSearch } />
281
+ < FlexBox gap = "8px" alignItems = "center" >
282
+ < Search placeholder = "Search" value = { search } onChange = { setSearch } />
283
+ < div style = { { position : 'relative' } } >
284
+ < Button
285
+ disabled = { isFetching || messages . length === 0 }
286
+ buttonType = "secondary"
287
+ buttonSize = "M"
288
+ onClick = { handleDownloadClick }
289
+ style = { {
290
+ minWidth : '40px' ,
291
+ padding : '8px' ,
292
+ display : 'flex' ,
293
+ alignItems : 'center' ,
294
+ justifyContent : 'center' ,
295
+ } }
296
+ >
297
+ < svg
298
+ width = "16"
299
+ height = "16"
300
+ viewBox = "0 0 24 24"
301
+ fill = "none"
302
+ stroke = "currentColor"
303
+ strokeWidth = "2"
304
+ strokeLinecap = "round"
305
+ strokeLinejoin = "round"
306
+ >
307
+ < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
308
+ < polyline points = "7,10 12,15 17,10" />
309
+ < line x1 = "12" y1 = "15" x2 = "12" y2 = "3" />
310
+ </ svg > Export
311
+ </ Button >
312
+ { showFormatSelector && (
313
+ < div
314
+ style = { {
315
+ position : 'absolute' ,
316
+ top : '100%' ,
317
+ right : '0' ,
318
+ zIndex : 1000 ,
319
+ backgroundColor : 'white' ,
320
+ border : '1px solid #ccc' ,
321
+ borderRadius : '4px' ,
322
+ boxShadow : '0 2px 8px rgba(0,0,0,0.1)' ,
323
+ padding : '8px' ,
324
+ minWidth : '120px' ,
325
+ } }
326
+ >
327
+ { formatOptions . map ( ( option ) => (
328
+ < div
329
+ key = { option . value }
330
+ onClick = { ( ) => handleFormatSelect ( option . value ) }
331
+ style = { {
332
+ padding : '8px 12px' ,
333
+ cursor : 'pointer' ,
334
+ borderRadius : '4px' ,
335
+ fontSize : '12px' ,
336
+ } }
337
+ onMouseEnter = { ( e ) => {
338
+ e . currentTarget . style . backgroundColor = '#f5f5f5' ;
339
+ } }
340
+ onMouseLeave = { ( e ) => {
341
+ e . currentTarget . style . backgroundColor = 'transparent' ;
342
+ } }
343
+ >
344
+ { option . label }
345
+ </ div >
346
+ ) ) }
347
+ </ div >
348
+ ) }
349
+ </ div >
350
+ </ FlexBox >
191
351
</ FlexBox >
192
352
< FlexBox
193
353
gap = "10px"
@@ -245,3 +405,4 @@ const Filters: React.FC<FiltersProps> = ({
245
405
} ;
246
406
247
407
export default Filters ;
408
+
0 commit comments