-
-
Couldn't load subscription status.
- Fork 193
FE: Messages: Implement messages export #740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 25 commits
b008fa9
c55f8c0
252c8a9
1aab310
9817ee5
628ecf1
b393c2d
d841a95
eef6107
cc31f80
578982d
0615fc5
f0f3508
8ef27ad
a5f925b
bff1aa0
f8aa582
38abcd0
66c67ee
297f754
7819ca6
416c57a
55a8fa3
b0c419c
72cc645
9ccacb8
e32d02e
4c00bb7
1346b6d
14ff067
8c13134
7479f01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,12 @@ | ||
| import 'react-datepicker/dist/react-datepicker.css'; | ||
|
|
||
| import { SerdeUsage, TopicMessageConsuming } from 'generated-sources'; | ||
| import { | ||
| SerdeUsage, | ||
| TopicMessageConsuming, | ||
| TopicMessage, | ||
| } from 'generated-sources'; | ||
| import React, { ChangeEvent, useMemo, useState } from 'react'; | ||
| import { format } from 'date-fns'; | ||
| import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled'; | ||
| import Select from 'components/common/Select/Select'; | ||
| import { Button } from 'components/common/Button/Button'; | ||
|
|
@@ -18,6 +23,7 @@ import EditIcon from 'components/common/Icons/EditIcon'; | |
| import CloseIcon from 'components/common/Icons/CloseIcon'; | ||
| import FlexBox from 'components/common/FlexBox/FlexBox'; | ||
| import { useMessageFiltersStore } from 'lib/hooks/useMessageFiltersStore'; | ||
| import useDataSaver from 'lib/hooks/useDataSaver'; | ||
|
|
||
| import * as S from './Filters.styled'; | ||
| import { | ||
|
|
@@ -30,18 +36,37 @@ import { | |
| import FiltersSideBar from './FiltersSideBar'; | ||
| import FiltersMetrics from './FiltersMetrics'; | ||
|
|
||
| interface MessageData { | ||
| Value: string | undefined; | ||
| Offset: number; | ||
| Key: string | undefined; | ||
| Partition: number; | ||
| Headers: { [key: string]: string | undefined } | undefined; | ||
| Timestamp: Date; | ||
| } | ||
|
|
||
| type DownloadFormat = 'json' | 'csv'; | ||
|
|
||
| function padCurrentDateTimeString(): string { | ||
| const now: Date = new Date(); | ||
| const dateTimeString: string = format(now, 'yyyy-MM-dd HH:mm:ss'); | ||
| return `_${dateTimeString}`; | ||
| } | ||
|
|
||
| export interface FiltersProps { | ||
| phaseMessage?: string; | ||
| consumptionStats?: TopicMessageConsuming; | ||
| isFetching: boolean; | ||
| abortFetchData: () => void; | ||
| messages?: TopicMessage[]; | ||
| } | ||
|
|
||
| const Filters: React.FC<FiltersProps> = ({ | ||
| consumptionStats, | ||
| isFetching, | ||
| abortFetchData, | ||
| phaseMessage, | ||
| messages = [], | ||
| }) => { | ||
| const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>(); | ||
|
|
||
|
|
@@ -67,7 +92,79 @@ const Filters: React.FC<FiltersProps> = ({ | |
|
|
||
| const { data: topic } = useTopicDetails({ clusterName, topicName }); | ||
| const [createdEditedSmartId, setCreatedEditedSmartId] = useState<string>(); | ||
| const remove = useMessageFiltersStore((state) => state.remove); | ||
| const remove = useMessageFiltersStore( | ||
| (state: { remove: (id: string) => void }) => state.remove | ||
| ); | ||
|
|
||
| // Download functionality | ||
| const [showFormatSelector, setShowFormatSelector] = useState(false); | ||
|
|
||
| const formatOptions = [ | ||
| { label: 'Export JSON', value: 'json' as DownloadFormat }, | ||
| { label: 'Export CSV', value: 'csv' as DownloadFormat }, | ||
| ]; | ||
|
|
||
| const baseFileName = `topic-messages${padCurrentDateTimeString()}`; | ||
|
|
||
| const savedMessagesJson: MessageData[] = messages.map( | ||
| (message: TopicMessage) => ({ | ||
| Value: message.value, | ||
| Offset: message.offset, | ||
| Key: message.key, | ||
| Partition: message.partition, | ||
| Headers: message.headers, | ||
| Timestamp: message.timestamp, | ||
| }) | ||
| ); | ||
|
|
||
| const convertToCSV = useMemo(() => { | ||
| return (messagesData: MessageData[]) => { | ||
| const headers = [ | ||
| 'Value', | ||
| 'Offset', | ||
| 'Key', | ||
| 'Partition', | ||
| 'Headers', | ||
| 'Timestamp', | ||
| ] as const; | ||
| const rows = messagesData.map((msg) => | ||
| headers | ||
| .map((header) => { | ||
| const value = msg[header]; | ||
| if (header === 'Headers') { | ||
| return JSON.stringify(value || {}); | ||
| } | ||
| return String(value ?? ''); | ||
| }) | ||
| .join(',') | ||
| ); | ||
| return [headers.join(','), ...rows].join('\n'); | ||
| }; | ||
| }, []); | ||
|
|
||
| const jsonSaver = useDataSaver( | ||
| `${baseFileName}.json`, | ||
| JSON.stringify(savedMessagesJson, null, '\t') | ||
| ); | ||
| const csvSaver = useDataSaver( | ||
| `${baseFileName}.csv`, | ||
| convertToCSV(savedMessagesJson) | ||
| ); | ||
|
|
||
| const handleFormatSelect = (downloadFormat: DownloadFormat) => { | ||
| setShowFormatSelector(false); | ||
|
|
||
| // Automatically download after format selection | ||
| if (downloadFormat === 'json') { | ||
| jsonSaver.saveFile(); | ||
| } else { | ||
| csvSaver.saveFile(); | ||
| } | ||
| }; | ||
|
|
||
| const handleDownloadClick = () => { | ||
| setShowFormatSelector(!showFormatSelector); | ||
| }; | ||
|
|
||
| const partitions = useMemo(() => { | ||
| return (topic?.partitions || []).reduce<{ | ||
|
|
@@ -187,7 +284,84 @@ const Filters: React.FC<FiltersProps> = ({ | |
| </Button> | ||
| </FlexBox> | ||
|
|
||
| <Search placeholder="Search" value={search} onChange={setSearch} /> | ||
| <FlexBox gap="8px" alignItems="center"> | ||
| <Search placeholder="Search" value={search} onChange={setSearch} /> | ||
| <div style={{ position: 'relative' }}> | ||
| <Button | ||
| disabled={isFetching || messages.length === 0} | ||
| buttonType="secondary" | ||
| buttonSize="M" | ||
| onClick={handleDownloadClick} | ||
| style={{ | ||
| minWidth: '40px', | ||
| padding: '8px', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }} | ||
| > | ||
| <svg | ||
| width="24" | ||
| height="24" | ||
| viewBox="0 0 18 18" | ||
| fill="currentColor" | ||
| > | ||
| <path d="M4.24 5.8a.75.75 0 001.06-.04l1.95-2.1v6.59a.75.75 0 001.5 0V3.66l1.95 2.1a.75.75 0 101.1-1.02l-3.25-3.5a.75.75 0 00-1.101.001L4.2 4.74a.75.75 0 00.04 1.06z" /> | ||
|
||
| <path d="M1.75 9a.75.75 0 01.75.75v3c0 .414.336.75.75.75h9.5a.75.75 0 00.75-.75v-3a.75.75 0 011.5 0v3A2.25 2.25 0 0112.75 15h-9.5A2.25 2.25 0 011 12.75v-3A.75.75 0 011.75 9z" /> | ||
| </svg>{' '} | ||
| Export | ||
| </Button> | ||
| {showFormatSelector && ( | ||
| <div | ||
| style={{ | ||
| position: 'absolute', | ||
| top: '100%', | ||
| right: '0', | ||
| zIndex: 1000, | ||
| backgroundColor: 'white', | ||
| border: '1px solid #ccc', | ||
| borderRadius: '4px', | ||
| boxShadow: '0 2px 8px rgba(0,0,0,0.1)', | ||
| padding: '8px', | ||
| minWidth: '120px', | ||
| }} | ||
| > | ||
| {formatOptions.map((option) => ( | ||
| <button | ||
| key={option.value} | ||
| type="button" | ||
| onClick={() => handleFormatSelect(option.value)} | ||
| onKeyDown={(e) => { | ||
| if (e.key === 'Enter' || e.key === ' ') { | ||
| handleFormatSelect(option.value); | ||
| } | ||
| }} | ||
| style={{ | ||
| padding: '8px 12px', | ||
| cursor: 'pointer', | ||
| borderRadius: '4px', | ||
| fontSize: '12px', | ||
| border: 'none', | ||
| background: 'transparent', | ||
| width: '100%', | ||
| textAlign: 'left', | ||
| }} | ||
| onMouseEnter={(e) => { | ||
| const target = e.currentTarget; | ||
| target.style.backgroundColor = '#f5f5f5'; | ||
| }} | ||
| onMouseLeave={(e) => { | ||
| const target = e.currentTarget; | ||
| target.style.backgroundColor = 'transparent'; | ||
| }} | ||
| > | ||
| {option.label} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| </FlexBox> | ||
| </FlexBox> | ||
| <FlexBox | ||
| gap="10px" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The icon is 16x16 in figma

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you adjust the size please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexanderlz Any chance you can resume this work?
I am looking forward to using this feature
Thanks again! 🎖️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, will do it during one of the upcoming days
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yeikel done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thank you for your hard work @alexanderlz
@Haarolean Any chance you can take a look again?
Thanks in advance 🫡