@@ -2,6 +2,7 @@ import 'react-datepicker/dist/react-datepicker.css';
22
33import { SerdeUsage , TopicMessageConsuming } from 'generated-sources' ;
44import React , { ChangeEvent , useMemo , useState } from 'react' ;
5+ import { format } from 'date-fns' ;
56import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled' ;
67import Select from 'components/common/Select/Select' ;
78import { Button } from 'components/common/Button/Button' ;
@@ -18,6 +19,7 @@ import EditIcon from 'components/common/Icons/EditIcon';
1819import CloseIcon from 'components/common/Icons/CloseIcon' ;
1920import FlexBox from 'components/common/FlexBox/FlexBox' ;
2021import { useMessageFiltersStore } from 'lib/hooks/useMessageFiltersStore' ;
22+ import useDataSaver from 'lib/hooks/useDataSaver' ;
2123
2224import * as S from './Filters.styled' ;
2325import {
@@ -30,18 +32,37 @@ import {
3032import FiltersSideBar from './FiltersSideBar' ;
3133import FiltersMetrics from './FiltersMetrics' ;
3234
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+
3352export interface FiltersProps {
3453 phaseMessage ?: string ;
3554 consumptionStats ?: TopicMessageConsuming ;
3655 isFetching : boolean ;
3756 abortFetchData : ( ) => void ;
57+ messages ?: any [ ] ; // Add messages prop for download functionality
3858}
3959
4060const Filters : React . FC < FiltersProps > = ( {
4161 consumptionStats,
4262 isFetching,
4363 abortFetchData,
4464 phaseMessage,
65+ messages = [ ] ,
4566} ) => {
4667 const { clusterName, topicName } = useAppParams < RouteParamsClusterTopic > ( ) ;
4768
@@ -69,6 +90,76 @@ const Filters: React.FC<FiltersProps> = ({
6990 const [ createdEditedSmartId , setCreatedEditedSmartId ] = useState < string > ( ) ;
7091 const remove = useMessageFiltersStore ( ( state ) => state . remove ) ;
7192
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+
72163 const partitions = useMemo ( ( ) => {
73164 return ( topic ?. partitions || [ ] ) . reduce < {
74165 dict : Record < string , { label : string ; value : number } > ;
@@ -187,7 +278,76 @@ const Filters: React.FC<FiltersProps> = ({
187278 </ Button >
188279 </ FlexBox >
189280
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 >
191351 </ FlexBox >
192352 < FlexBox
193353 gap = "10px"
@@ -245,3 +405,4 @@ const Filters: React.FC<FiltersProps> = ({
245405} ;
246406
247407export default Filters ;
408+
0 commit comments