Skip to content

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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b008fa9
Merge pull request #1 from kafbat/main
alexanderlz Dec 29, 2024
c55f8c0
add an option to save all messages as csv/json, Issue #688
alexanderlz Dec 30, 2024
252c8a9
linter issues fix
alexanderlz Dec 30, 2024
1aab310
lint fixes
alexanderlz Dec 30, 2024
9817ee5
linter fixes + comments
alexanderlz Jan 5, 2025
628ecf1
forgot missing date-fns in package.json
alexanderlz Jan 5, 2025
b393c2d
forgot missing date-fns in pnpm
alexanderlz Jan 5, 2025
d841a95
linter
alexanderlz Jan 5, 2025
eef6107
linter warnings fix
alexanderlz Jan 5, 2025
cc31f80
linter warnings fix ->> single-quote
alexanderlz Jan 5, 2025
578982d
linter warnings fix (hopefully last)
alexanderlz Jan 5, 2025
0615fc5
Merge branch 'main' into issues/688
alexanderlz Jan 5, 2025
f0f3508
Merge branch 'main' into issues/688
alexanderlz Jan 22, 2025
8ef27ad
Merge branch 'main' into issues/688
alexanderlz Feb 12, 2025
a5f925b
Update MessagesTable.tsx
alexanderlz Feb 12, 2025
bff1aa0
Update MessagesTable.tsx
alexanderlz Feb 12, 2025
f8aa582
redesign the export messages button look and feel
alexanderlz Jul 13, 2025
38abcd0
redesign the export messages button look and feel, linter errors
alexanderlz Jul 13, 2025
66c67ee
redesign the export messages button look and feel, linter errors #2
alexanderlz Jul 13, 2025
297f754
Merge branch 'main' into issues/688
alexanderlz Jul 18, 2025
7819ca6
Update Filters.tsx
alexanderlz Jul 18, 2025
416c57a
Merge branch 'main' into issues/688
germanosin Jul 23, 2025
55a8fa3
replace the icon according to the discussion
alexanderlz Jul 31, 2025
b0c419c
Merge branch 'issues/688' of https://github.com/alexanderlz/kafka-ui …
alexanderlz Jul 31, 2025
b168638
linter warning
alexanderlz Aug 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@tanstack/react-table": "8.16.0",
"ace-builds": "1.33.0",
"ajv": "8.8.2",
"date-fns": "4.1.0",
"ajv-draft-04": "^1.0.0",
"ajv-formats": "3.0.1",
"json-schema-faker": "0.5.6",
Expand Down
8 changes: 8 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

183 changes: 180 additions & 3 deletions frontend/src/components/Topics/Topic/Messages/Filters/Filters.tsx
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';
Expand All @@ -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 {
Expand All @@ -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>();

Expand All @@ -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<{
Expand Down Expand Up @@ -187,7 +284,87 @@ 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 24 12"
fill="none"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M16,9.5v4A2.5,2.5,0,0,1,13.5,16H2.5A2.5,2.5,0,0,1,0,13.5V2.5A2.5,2.5,0,0,1,2.5,0h3A.5.5,0,0,1,6,.5a.5.5,0,0,1-.5.5h-3A1.5,1.5,0,0,0,1,2.5v11A1.5,1.5,0,0,0,2.5,15h11A1.5,1.5,0,0,0,15,13.5v-4a.5.5,0,0,1,1,0ZM5,9.5a.5.5,0,0,0,1,0v-2A3.5,3.5,0,0,1,9.5,4h4.79L12.15,6.15a.48.48,0,0,0,0,.7.48.48,0,0,0,.7,0l3-3A.36.36,0,0,0,16,3.69a.5.5,0,0,0,0-.38.36.36,0,0,0-.11-.16l-3-3a.48.48,0,0,0-.7,0,.48.48,0,0,0,0,.7L14.29,3H9.5A4.51,4.51,0,0,0,5,7.5Z" />
</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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const Messages: React.FC = () => {
isFetching={isFetching}
phaseMessage={phase}
abortFetchData={abortFetchData}
messages={messages}
/>
<MessagesTable messages={messages} isFetching={isFetching} />
</>
Expand Down
Loading