Skip to content

Commit f8aa582

Browse files
committed
redesign the export messages button look and feel
1 parent bff1aa0 commit f8aa582

File tree

3 files changed

+166
-116
lines changed

3 files changed

+166
-116
lines changed

frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'react-datepicker/dist/react-datepicker.css';
22

33
import { SerdeUsage, TopicMessageConsuming } from 'generated-sources';
44
import React, { ChangeEvent, useMemo, useState } from 'react';
5+
import { format } from 'date-fns';
56
import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled';
67
import Select from 'components/common/Select/Select';
78
import { Button } from 'components/common/Button/Button';
@@ -18,6 +19,7 @@ import EditIcon from 'components/common/Icons/EditIcon';
1819
import CloseIcon from 'components/common/Icons/CloseIcon';
1920
import FlexBox from 'components/common/FlexBox/FlexBox';
2021
import { useMessageFiltersStore } from 'lib/hooks/useMessageFiltersStore';
22+
import useDataSaver from 'lib/hooks/useDataSaver';
2123

2224
import * as S from './Filters.styled';
2325
import {
@@ -30,18 +32,37 @@ import {
3032
import FiltersSideBar from './FiltersSideBar';
3133
import 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+
3352
export 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

4060
const 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

247407
export default Filters;
408+

frontend/src/components/Topics/Topic/Messages/Messages.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ const Messages: React.FC = () => {
2121
isFetching={isFetching}
2222
phaseMessage={phase}
2323
abortFetchData={abortFetchData}
24+
messages={messages}
2425
/>
2526
<MessagesTable messages={messages} isFetching={isFetching} />
2627
</>
2728
);
2829
};
2930

3031
export default Messages;
32+

frontend/src/components/Topics/Topic/Messages/MessagesTable.tsx

Lines changed: 2 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@ import PageLoader from 'components/common/PageLoader/PageLoader';
22
import { Table } from 'components/common/table/Table/Table.styled';
33
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
44
import { TopicMessage } from 'generated-sources';
5-
import { format } from 'date-fns';
6-
import React, { useCallback, useEffect, useState, useMemo } from 'react';
5+
import React, { useCallback, useEffect, useState } from 'react';
76
import { Button } from 'components/common/Button/Button';
87
import * as S from 'components/common/NewTable/Table.styled';
98
import { usePaginateTopics, useIsLiveMode } from 'lib/hooks/useMessagesFilters';
109
import { useMessageFiltersStore } from 'lib/hooks/useMessageFiltersStore';
11-
import useDataSaver from 'lib/hooks/useDataSaver';
12-
import Select, { SelectOption } from 'components/common/Select/Select';
1310
import useAppParams from 'lib/hooks/useAppParams';
1411
import { RouteParamsClusterTopic } from 'lib/paths';
1512
import { useLocalStorage } from 'lib/hooks/useLocalStorage';
@@ -22,24 +19,6 @@ export interface MessagesTableProps {
2219
isFetching: boolean;
2320
}
2421

25-
interface MessageData {
26-
Value: string | undefined;
27-
Offset: number;
28-
Key: string | undefined;
29-
Partition: number;
30-
Headers: { [key: string]: string | undefined } | undefined;
31-
Timestamp: Date;
32-
}
33-
34-
type DownloadFormat = 'json' | 'csv';
35-
36-
function padCurrentDateTimeString(): string {
37-
const now: Date = new Date();
38-
const dateTimeString: string = format(now, 'yyyy-MM-dd HH:mm:ss');
39-
40-
return `_${dateTimeString}`;
41-
}
42-
4322
interface MessagePreviewProps {
4423
[key: string]: {
4524
keyFilters: PreviewFilter[];
@@ -96,101 +75,8 @@ const MessagesTable: React.FC<MessagesTableProps> = ({
9675
[previewFor, messagesPreview, topicName]
9776
);
9877

99-
const [selectedFormat, setSelectedFormat] = useState<DownloadFormat>('json');
100-
101-
const formatOptions: SelectOption<DownloadFormat>[] = [
102-
{ label: 'JSON', value: 'json' },
103-
{ label: 'CSV', value: 'csv' },
104-
];
105-
106-
const baseFileName = `topic-messages${padCurrentDateTimeString()}`;
107-
108-
const savedMessagesJson: MessageData[] = messages.map((message) => ({
109-
Value: message.content,
110-
Offset: message.offset,
111-
Key: message.key,
112-
Partition: message.partition,
113-
Headers: message.headers,
114-
Timestamp: message.timestamp,
115-
}));
116-
117-
const convertToCSV = useMemo(() => {
118-
return (messagesData: MessageData[]) => {
119-
const headers = [
120-
'Value',
121-
'Offset',
122-
'Key',
123-
'Partition',
124-
'Headers',
125-
'Timestamp',
126-
] as const;
127-
const rows = messagesData.map((msg) =>
128-
headers
129-
.map((header) => {
130-
const value = msg[header];
131-
if (header === 'Headers') {
132-
return JSON.stringify(value || {});
133-
}
134-
return String(value ?? '');
135-
})
136-
.join(',')
137-
);
138-
return [headers.join(','), ...rows].join('\n');
139-
};
140-
}, []);
141-
142-
const jsonSaver = useDataSaver(
143-
`${baseFileName}.json`,
144-
JSON.stringify(savedMessagesJson, null, '\t')
145-
);
146-
const csvSaver = useDataSaver(
147-
`${baseFileName}.csv`,
148-
convertToCSV(savedMessagesJson)
149-
);
150-
151-
const handleFormatSelect = (downloadFormat: DownloadFormat) => {
152-
setSelectedFormat(downloadFormat);
153-
};
154-
155-
const handleDownload = () => {
156-
if (selectedFormat === 'json') {
157-
jsonSaver.saveFile();
158-
} else {
159-
csvSaver.saveFile();
160-
}
161-
};
162-
16378
return (
16479
<div style={{ position: 'relative' }}>
165-
<div
166-
style={{
167-
display: 'flex',
168-
gap: '8px',
169-
marginLeft: '1rem',
170-
marginBottom: '1rem',
171-
}}
172-
>
173-
<Select<DownloadFormat>
174-
id="download-format"
175-
name="download-format"
176-
onChange={handleFormatSelect}
177-
options={formatOptions}
178-
value={selectedFormat}
179-
minWidth="70px"
180-
selectSize="M"
181-
placeholder="Select format to download"
182-
disabled={isFetching || messages.length === 0}
183-
/>
184-
<Button
185-
disabled={isFetching || messages.length === 0}
186-
buttonType="secondary"
187-
buttonSize="M"
188-
onClick={handleDownload}
189-
>
190-
Download Current Messages
191-
</Button>
192-
</div>
193-
19480
{previewFor !== null && (
19581
<PreviewModal
19682
values={previewFor === 'key' ? keyFilters : contentFilters}
@@ -269,3 +155,4 @@ const MessagesTable: React.FC<MessagesTableProps> = ({
269155
};
270156

271157
export default MessagesTable;
158+

0 commit comments

Comments
 (0)