Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 5 additions & 6 deletions frontend/src/components/license/license-notification.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Alert, AlertDescription, AlertIcon, Box, Button, Flex } from '@redpanda-data/ui';
import { Link, useLocation } from '@tanstack/react-router';
import { useEffect } from 'react';
import { useStore } from 'zustand';

import {
coreHasEnterpriseFeatures,
Expand All @@ -13,15 +12,15 @@ import {
prettyLicenseType,
} from './license-utils';
import { License_Source, License_Type } from '../../protogen/redpanda/api/console/v1alpha1/license_pb';
import { api, useApiStore } from '../../state/backend-api';
import { api, useApiStoreHook } from '../../state/backend-api';
import { capitalizeFirst } from '../../utils/utils';

// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complex business logic
export const LicenseNotification = () => {
const licenses = useStore(useApiStore, (s) => s.licenses);
const licensesLoaded = useStore(useApiStore, (s) => s.licensesLoaded);
const licenseViolation = useStore(useApiStore, (s) => s.licenseViolation);
const enterpriseFeaturesUsed = useStore(useApiStore, (s) => s.enterpriseFeaturesUsed);
const licenses = useApiStoreHook((s) => s.licenses);
const licensesLoaded = useApiStoreHook((s) => s.licensesLoaded);
const licenseViolation = useApiStoreHook((s) => s.licenseViolation);
const enterpriseFeaturesUsed = useApiStoreHook((s) => s.enterpriseFeaturesUsed);
const location = useLocation();

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Alert, AlertDescription, AlertIcon, Box, Flex, Text } from '@redpanda-data/ui';
import { Link } from 'components/redpanda-ui/components/typography';
import { type FC, type ReactElement, useEffect, useState } from 'react';
import { useStore } from 'zustand';

import {
consoleHasEnterpriseFeature,
Expand All @@ -20,7 +19,7 @@ import {
} from './license-utils';
import { RegisterModal } from './register-modal';
import { type License, License_Type } from '../../protogen/redpanda/api/console/v1alpha1/license_pb';
import { api, useApiStore } from '../../state/backend-api';
import { api, useApiStoreHook } from '../../state/backend-api';

const getLicenseAlertContent = (
licenses: License[],
Expand Down Expand Up @@ -255,8 +254,8 @@ const getLicenseAlertContent = (
};

export const OverviewLicenseNotification: FC = () => {
const licenses = useStore(useApiStore, (s) => s.licenses);
const clusterOverview = useStore(useApiStore, (s) => s.clusterOverview);
const licenses = useApiStoreHook((s) => s.licenses);
const clusterOverview = useApiStoreHook((s) => s.clusterOverview);
const [registerModalOpen, setIsRegisterModalOpen] = useState(false);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
* by the Apache License, Version 2.0
*/

'use no memo';

import { DataTable } from '@redpanda-data/ui';
import { getRouteApi } from '@tanstack/react-router';

Expand All @@ -25,7 +23,7 @@ import { Skeleton } from 'components/redpanda-ui/components/skeleton';
import { Text } from 'components/redpanda-ui/components/typography';
import { Logs, RefreshCcw } from 'lucide-react';
import { PayloadEncoding } from 'protogen/redpanda/api/console/v1alpha1/common_pb';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { MessageSearch, MessageSearchRequest } from 'state/backend-api';
import { createMessageSearch } from 'state/backend-api';
import type { TopicMessage } from 'state/rest-interfaces';
Expand Down Expand Up @@ -136,34 +134,81 @@ export const RemoteMCPLogsTab = () => {
const interval = setInterval(() => {
const search = searchRef.current;
if (search) {
setLogState((prev) => ({ ...prev, messages: [...search.messages] }));
setLogState((prev) => {
if (prev.messages.length === search.messages.length) {
return prev;
}
return { ...prev, messages: [...search.messages] };
});
}
}, 200);
return () => clearInterval(interval);
}, []);

const messageTableColumns: ColumnDef<TopicMessage>[] = [
{
header: 'Timestamp',
accessorKey: 'timestamp',
cell: ({
row: {
original: { timestamp },
},
}) => <TimestampDisplay format="default" unixEpochMillisecond={timestamp} />,
size: 200,
},
{
header: 'Value',
accessorKey: 'value',
cell: ({ row: { original } }) => (
<MessagePreview isCompactTopic={false} msg={original} previewFields={() => []} />
),
size: Number.MAX_SAFE_INTEGER,
},
];

const filteredMessages = messages.filter((x) => isFilterMatch(logsQuickSearch, x));
const messageTableColumns: ColumnDef<TopicMessage>[] = useMemo(
() => [
{
header: 'Timestamp',
accessorKey: 'timestamp',
cell: ({
row: {
original: { timestamp },
},
}) => <TimestampDisplay format="default" unixEpochMillisecond={timestamp} />,
size: 200,
},
{
header: 'Value',
accessorKey: 'value',
cell: ({ row: { original } }) => (
<MessagePreview isCompactTopic={false} msg={original} previewFields={() => []} />
),
size: Number.MAX_SAFE_INTEGER,
},
],
[]
);

const loadLargeMessage = useCallback(() => Promise.resolve(), []);

const renderSubComponent = useCallback(
({ row: { original } }: { row: { original: TopicMessage } }) => (
<ExpandedMessage
loadLargeMessage={loadLargeMessage}
msg={original}
onDownloadRecord={() => {
const blob = new Blob(
[
JSON.stringify(
{
offset: original.offset,
key: original.keyJson,
value: original.valueJson,
timestamp: original.timestamp,
headers: original.headers,
},
null,
2
),
],
{ type: 'application/json' }
);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `mcp-log-${original.offset}.json`;
a.click();
URL.revokeObjectURL(url);
}}
/>
),
[loadLargeMessage]
);

const filteredMessages = useMemo(
() => messages.filter((x) => isFilterMatch(logsQuickSearch, x)),
[messages, logsQuickSearch]
);

return (
<Card className="px-0 py-0" size="full">
Expand Down Expand Up @@ -219,14 +264,7 @@ export const RemoteMCPLogsTab = () => {
loadingText="Loading... This can take several seconds."
onSortingChange={setSorting}
sorting={sorting}
subComponent={({ row: { original } }) => (
<ExpandedMessage
loadLargeMessage={
() => Promise.resolve() // No need to load large messages for this view
}
msg={original}
/>
)}
subComponent={renderSubComponent}
/>
);
})()}
Expand Down
113 changes: 64 additions & 49 deletions frontend/src/components/pages/rp-connect/pipelines-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@
* by the Apache License, Version 2.0
*/

'use no memo';

import { ConnectError } from '@connectrpc/connect';
import { Alert, AlertIcon, Box, Button, createStandaloneToast, DataTable, Flex, SearchField } from '@redpanda-data/ui';
import { Link } from '@tanstack/react-router';
import type { ColumnDef, SortingState } from '@tanstack/react-table';
import { isEmbedded, isFeatureFlagEnabled } from 'config';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { toast as sonnerToast } from 'sonner';
import { formatToastErrorMessageGRPC } from 'utils/toast.utils';

Expand Down Expand Up @@ -292,13 +290,18 @@ export const LogsTab = (p: { pipeline: Pipeline }) => {
const interval = setInterval(() => {
const search = searchRef.current;
if (search) {
setLogState((prev) => ({ ...prev, messages: [...search.messages] }));
setLogState((prev) => {
if (prev.messages.length === search.messages.length) {
return prev;
}
return { ...prev, messages: [...search.messages] };
});
}
}, 200);
return () => clearInterval(interval);
}, []);

const loadLargeMessage = async (msgTopicName: string, partitionID: number, offset: number) => {
const loadLargeMessage = useCallback(async (msgTopicName: string, partitionID: number, offset: number) => {
const search = createMessageSearch();
const searchReq: MessageSearchRequest = {
filterInterpreterCode: '',
Expand All @@ -325,40 +328,63 @@ export const LogsTab = (p: { pipeline: Pipeline }) => {
} else {
throw new Error("LoadLargeMessage: Couldn't load the message content, the response was empty");
}
};
}, []);

const paginationParams = usePaginationParams(messages.length, 10);
const messageTableColumns: ColumnDef<TopicMessage>[] = [
{
header: 'Timestamp',
accessorKey: 'timestamp',
cell: ({
row: {
original: { timestamp },
},
}) => <TimestampDisplay format="default" unixEpochMillisecond={timestamp} />,
size: 30,
},
{
header: 'Value',
accessorKey: 'value',
cell: ({ row: { original } }) => (
<MessagePreview
isCompactTopic={topic ? topic.cleanupPolicy.includes('compact') : false}
msg={original}
previewFields={() => []}
/>
),
size: Number.MAX_SAFE_INTEGER,
},
];

const filteredMessages = messages.filter((x) => {
if (!logsQuickSearch) {
return true;
}
return isFilterMatch(logsQuickSearch, x);
});
const isCompactTopic = topic ? topic.cleanupPolicy.includes('compact') : false;
const messageTableColumns: ColumnDef<TopicMessage>[] = useMemo(
() => [
{
header: 'Timestamp',
accessorKey: 'timestamp',
cell: ({
row: {
original: { timestamp },
},
}) => <TimestampDisplay format="default" unixEpochMillisecond={timestamp} />,
size: 30,
},
{
header: 'Value',
accessorKey: 'value',
cell: ({ row: { original } }) => (
<MessagePreview
isCompactTopic={isCompactTopic}
msg={original}
previewFields={() => []}
/>
),
size: Number.MAX_SAFE_INTEGER,
},
],
[isCompactTopic]
);

const renderSubComponent = useCallback(
({ row: { original } }: { row: { original: TopicMessage } }) => (
<ExpandedMessage
loadLargeMessage={() =>
loadLargeMessage(
searchRef.current?.searchRequest?.topicName ?? '',
original.partitionID,
original.offset
)
}
msg={original}
/>
),
[loadLargeMessage]
);

const filteredMessages = useMemo(
() => messages.filter((x) => {
if (!logsQuickSearch) {
return true;
}
return isFilterMatch(logsQuickSearch, x);
}),
[messages, logsQuickSearch]
);

return (
<>
Expand All @@ -382,18 +408,7 @@ export const LogsTab = (p: { pipeline: Pipeline }) => {
sorting={sorting}
// todo: message rendering should be extracted from TopicMessagesTab into a standalone component, in its own folder,
// to make it clear that it does not depend on other functinoality from TopicMessagesTab
subComponent={({ row: { original } }) => (
<ExpandedMessage
loadLargeMessage={() =>
loadLargeMessage(
searchRef.current?.searchRequest?.topicName ?? '',
original.partitionID,
original.offset
)
}
msg={original}
/>
)}
subComponent={renderSubComponent}
/>
</Section>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

vi.mock('@redpanda-data/ui', async () => {
const React = await import('react');
Expand Down Expand Up @@ -170,6 +171,25 @@ describe('topic message rendering', () => {
expect(kowlJsonViewSpy).toHaveBeenCalledTimes(1);
});

test('calls onDownloadRecord when "Download Record" button is clicked', async () => {
const user = userEvent.setup();
const onDownloadRecord = vi.fn();
const msg = buildMessage();

render(
<ExpandedMessage
loadLargeMessage={() => Promise.resolve()}
msg={msg}
onDownloadRecord={onDownloadRecord}
/>
);

const downloadButton = screen.getByRole('button', { name: /download record/i });
await user.click(downloadButton);

expect(onDownloadRecord).toHaveBeenCalledTimes(1);
});

test('rerenders expanded object payloads when the message prop changes', () => {
const props = {
topicName: 'market-data',
Expand Down
Loading