From de720171a4b92886addf4fe12f6523b235829d9d Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Thu, 8 May 2025 12:13:38 +0300 Subject: [PATCH 01/10] feat(TopicData): add topic message details in Drawer --- .../EnableFullscreenButton.tsx | 11 +- src/components/JsonViewer/JsonViewer.tsx | 9 +- src/components/JsonViewer/components/Cell.tsx | 21 +- src/components/ShortyString/ShortyString.scss | 2 +- .../PartitionsControls/PartitionsControls.tsx | 2 +- .../Partitions/columns/columns.tsx | 2 +- .../Diagnostics/TopicData/TopicData.scss | 5 + .../Diagnostics/TopicData/TopicData.tsx | 117 ++++-- .../TopicDataControls/TopicDataControls.tsx | 4 +- .../TopicMessageDetails.scss | 44 +++ .../TopicMessageDetails.tsx | 129 +++++++ .../components/TopicDataSection.tsx | 33 ++ .../components/TopicMessage.tsx | 134 +++++++ .../components/TopicMessageGeneralInfo.tsx | 53 +++ .../components/TopicMessageMetadata.tsx | 25 ++ .../TopicMessageDetails/components/fields.tsx | 65 ++++ .../TopicData/TopicMessageDetails/shared.ts | 5 + .../TopicData/columns/Columns.scss | 5 + .../Diagnostics/TopicData/columns/columns.tsx | 364 ++++++++++-------- .../Tenant/Diagnostics/TopicData/getData.ts | 3 +- .../Tenant/Diagnostics/TopicData/i18n/en.json | 7 +- .../TopicData/useTopicDataQueryParams.ts | 26 +- .../Diagnostics/TopicData/utils/constants.ts | 3 + .../Diagnostics/TopicData/utils/types.ts | 1 + src/containers/Tenant/TenantPages.tsx | 1 + src/store/reducers/capabilities/hooks.ts | 2 +- src/store/reducers/partitions/types.ts | 2 +- src/utils/downloadFile.ts | 9 +- src/utils/utils.ts | 2 +- 29 files changed, 869 insertions(+), 217 deletions(-) create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.scss create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicDataSection.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessageGeneralInfo.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessageMetadata.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/fields.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/shared.ts diff --git a/src/components/EnableFullscreenButton/EnableFullscreenButton.tsx b/src/components/EnableFullscreenButton/EnableFullscreenButton.tsx index 7f13fe5819..8e3f462887 100644 --- a/src/components/EnableFullscreenButton/EnableFullscreenButton.tsx +++ b/src/components/EnableFullscreenButton/EnableFullscreenButton.tsx @@ -1,4 +1,5 @@ import {SquareDashed} from '@gravity-ui/icons'; +import type {ButtonView} from '@gravity-ui/uikit'; import {Button, Icon} from '@gravity-ui/uikit'; import {enableFullscreen} from '../../store/reducers/fullscreen'; @@ -6,20 +7,16 @@ import {useTypedDispatch} from '../../utils/hooks'; interface EnableFullscreenButtonProps { disabled?: boolean; + view?: ButtonView; } -function EnableFullscreenButton({disabled}: EnableFullscreenButtonProps) { +function EnableFullscreenButton({disabled, view = 'flat-secondary'}: EnableFullscreenButtonProps) { const dispatch = useTypedDispatch(); const onEnableFullscreen = () => { dispatch(enableFullscreen()); }; return ( - ); diff --git a/src/components/JsonViewer/JsonViewer.tsx b/src/components/JsonViewer/JsonViewer.tsx index fbb5c411e3..8112820b0f 100644 --- a/src/components/JsonViewer/JsonViewer.tsx +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -33,6 +33,8 @@ interface JsonViewerCommonProps { tableSettings?: DT100.Settings; search?: boolean; collapsedInitially?: boolean; + maxValueWidth?: number; + toolbarClassName?: string; } interface JsonViewerProps extends JsonViewerCommonProps { @@ -60,7 +62,7 @@ const SETTINGS: DT100.Settings = { displayIndices: false, dynamicRender: true, sortable: false, - dynamicRenderMinSize: 100, + dynamicRenderMinSize: 50, }; function getCollapsedState(value: UnipikaValue) { @@ -114,6 +116,8 @@ function JsonViewerComponent({ search = true, extraTools, collapsedInitially, + maxValueWidth = 100, + toolbarClassName, }: JsonViewerComponentProps) { const [caseSensitiveSearch, setCaseSensitiveSearch] = useSetting( CASE_SENSITIVE_JSON_SEARCH, @@ -162,6 +166,7 @@ function JsonViewerComponent({ filter={filter} showFullText={onShowFullText} index={index} + maxValueWidth={maxValueWidth} /> ); }; @@ -295,7 +300,7 @@ function JsonViewerComponent({ const renderToolbar = () => { return ( - + + + + + ); + }; + + const truncated = safeParseNumber(size) > MESSAGE_SIZE_LIMIT; + + return ( + } + renderToolbar={renderToolbar} + className={b('message', {json: isJson})} + > + {messageContent} + + ); +} + +interface MessageTitleProps { + truncated?: boolean; +} + +function MessageTitle({truncated}: MessageTitleProps) { + return ( + + {i18n('label_message')} + {truncated && ( + + {' '} + + [ + {i18n('label_truncated', { + size: bytesToMB(MESSAGE_SIZE_LIMIT), + })} + ] + + + )} + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessageGeneralInfo.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessageGeneralInfo.tsx new file mode 100644 index 0000000000..b9962ea2ef --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessageGeneralInfo.tsx @@ -0,0 +1,53 @@ +import {DefinitionList, Flex} from '@gravity-ui/uikit'; + +import type {TopicMessage} from '../../../../../../types/api/topic'; +import {TOPIC_DATA_COLUMNS_IDS} from '../../utils/types'; +import {b} from '../shared'; + +import {fields} from './fields'; + +const dataGroups = [ + [ + {name: TOPIC_DATA_COLUMNS_IDS.PARTITION, copy: false}, + {name: TOPIC_DATA_COLUMNS_IDS.OFFSET, copy: false}, + {name: TOPIC_DATA_COLUMNS_IDS.SIZE, copy: false}, + ], + [ + {name: TOPIC_DATA_COLUMNS_IDS.TIMESTAMP_CREATE, copy: false}, + {name: TOPIC_DATA_COLUMNS_IDS.TIMESTAMP_WRITE, copy: false}, + {name: TOPIC_DATA_COLUMNS_IDS.TS_DIFF, copy: false}, + ], + [ + {name: TOPIC_DATA_COLUMNS_IDS.ORIGINAL_SIZE, copy: false}, + {name: TOPIC_DATA_COLUMNS_IDS.CODEC, copy: false}, + {name: TOPIC_DATA_COLUMNS_IDS.PRODUCERID, copy: true}, + {name: TOPIC_DATA_COLUMNS_IDS.SEQNO, copy: false}, + ], +]; + +interface TopicMessageGeneralInfoProps { + messageData: TopicMessage; +} + +export function TopicMessageGeneralInfo({messageData}: TopicMessageGeneralInfoProps) { + return ( + + {dataGroups.map((group, index) => ( + + {group.map((item) => { + const column = fields.find((c) => c.name === item.name); + return ( + + {column?.render?.({row: messageData})} + + ); + })} + + ))} + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessageMetadata.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessageMetadata.tsx new file mode 100644 index 0000000000..4511a1b640 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessageMetadata.tsx @@ -0,0 +1,25 @@ +import {DefinitionList} from '@gravity-ui/uikit'; + +import type {TopicMessageMetadataItem} from '../../../../../../types/api/topic'; +import i18n from '../../i18n'; +import {b} from '../shared'; + +import {TopicDataSection} from './TopicDataSection'; + +interface TopicMessageMetadataProps { + data: TopicMessageMetadataItem[]; +} + +export function TopicMessageMetadata({data}: TopicMessageMetadataProps) { + return ( + + + {data.map((item) => ( + + {item.Value} + + ))} + + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/fields.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/fields.tsx new file mode 100644 index 0000000000..bfdefdcc20 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/fields.tsx @@ -0,0 +1,65 @@ +import type {TopicMessageEnhanced} from '../../../../../../types/api/topic'; +import { + codecColumn, + messageColumn, + metadataColumn, + originalSizeColumn, + seqNoColumn, + sizeColumn, + timestampCreateColumn, + timestampWriteColumn, + tsDiffColumn, + valueOrPlaceholder, +} from '../../columns/columns'; +import {useTopicDataQueryParams} from '../../useTopicDataQueryParams'; +import {TOPIC_DATA_COLUMNS_TITLES} from '../../utils/constants'; +import {TOPIC_DATA_COLUMNS_IDS} from '../../utils/types'; + +type TopicMessageDetailsField = { + name: string; + header?: React.ReactNode; + render: (props: {row: TopicMessageEnhanced}) => React.ReactNode; +}; + +const partitionColumn: TopicMessageDetailsField = { + name: TOPIC_DATA_COLUMNS_IDS.PARTITION, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.PARTITION], + render: () => { + return ; + }, +}; +const offsetColumn: TopicMessageDetailsField = { + name: TOPIC_DATA_COLUMNS_IDS.OFFSET, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.OFFSET], + render: ({row}) => { + return valueOrPlaceholder(row.Offset); + }, +}; + +const producerIdColumn: TopicMessageDetailsField = { + name: TOPIC_DATA_COLUMNS_IDS.PRODUCERID, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.PRODUCERID], + render: ({row}) => { + return valueOrPlaceholder(row.ProducerId); + }, +}; + +function PartitionId() { + const {selectedPartition} = useTopicDataQueryParams(); + return selectedPartition; +} + +export const fields: TopicMessageDetailsField[] = [ + partitionColumn, + offsetColumn, + timestampCreateColumn, + timestampWriteColumn, + tsDiffColumn, + metadataColumn, + messageColumn, + sizeColumn, + originalSizeColumn, + codecColumn, + producerIdColumn, + seqNoColumn, +]; diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/shared.ts b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/shared.ts new file mode 100644 index 0000000000..b62ad0f8da --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/shared.ts @@ -0,0 +1,5 @@ +import {cn} from '../../../../../utils/cn'; + +export const b = cn('ydb-diagnostics-message-details'); + +export const MESSAGE_SIZE_LIMIT = 10_000_000; diff --git a/src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss b/src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss index 5be8951b63..7f3e9d1d3d 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss +++ b/src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss @@ -1,3 +1,5 @@ +@use '../../../../../styles/mixins.scss'; + .ydb-diagnostics-topic-data-columns { &__timestamp-ms { color: var(--g-color-text-secondary); @@ -19,4 +21,7 @@ &__truncated { font-style: italic; } + &__offset-link { + @extend .link; + } } diff --git a/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx b/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx index f2230f6690..7a99c0bbb4 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx @@ -3,10 +3,12 @@ import React from 'react'; import DataTable from '@gravity-ui/react-data-table'; import {Text} from '@gravity-ui/uikit'; import {isNil} from 'lodash'; +import {Link} from 'react-router-dom'; import {EntityStatus} from '../../../../../components/EntityStatus/EntityStatus'; import {MultilineTableHeader} from '../../../../../components/MultilineTableHeader/MultilineTableHeader'; import type {Column} from '../../../../../components/PaginatedTable'; +import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {TOPIC_MESSAGE_SIZE_LIMIT} from '../../../../../store/reducers/topic'; import type {TopicMessageEnhanced} from '../../../../../types/api/topic'; import {cn} from '../../../../../utils/cn'; @@ -14,7 +16,9 @@ import {EMPTY_DATA_PLACEHOLDER} from '../../../../../utils/constants'; import {formatBytes, formatTimestamp} from '../../../../../utils/dataFormatters/dataFormatters'; import {formatToMs} from '../../../../../utils/timeParsers'; import {safeParseNumber} from '../../../../../utils/utils'; +import {useDiagnosticsPageLinkGetter} from '../../DiagnosticsPages'; import i18n from '../i18n'; +import {useTopicDataQueryParams} from '../useTopicDataQueryParams'; import {TOPIC_DATA_COLUMNS_TITLES, codecNumberToName} from '../utils/constants'; import type {TopicDataColumnId} from '../utils/types'; import {TOPIC_DATA_COLUMNS_IDS} from '../utils/types'; @@ -23,160 +27,171 @@ import './Columns.scss'; const b = cn('ydb-diagnostics-topic-data-columns'); +export const offsetColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.OFFSET, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.OFFSET], + align: DataTable.LEFT, + render: ({row}) => { + const {Offset: offset, removed} = row; + + return ; + }, + width: 100, +}; + +export const timestampCreateColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.TIMESTAMP_CREATE, + header: ( + + ), + align: DataTable.LEFT, + render: ({row}) => , + width: 220, +}; + +export const timestampWriteColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.TIMESTAMP_WRITE, + header: ( + + ), + align: DataTable.LEFT, + render: ({row}) => , + width: 220, +}; + +export const tsDiffColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.TS_DIFF, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.TS_DIFF], + align: DataTable.RIGHT, + render: ({row}) => { + const numericValue = safeParseNumber(row.TimestampDiff); + return ( + = 100_000})}> + {formatToMs(numericValue)} + + ); + }, + width: 90, + note: i18n('context_ts-diff'), +}; + +export const metadataColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.METADATA, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.METADATA], + align: DataTable.LEFT, + render: ({row: {MessageMetadata}}) => { + if (!MessageMetadata) { + return EMPTY_DATA_PLACEHOLDER; + } + const prepared = MessageMetadata.map(({Key = '', Value = ''}) => `${Key}: ${Value}`); + return prepared.join(', '); + }, + width: 200, +}; + +export const messageColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.MESSAGE, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.MESSAGE], + align: DataTable.LEFT, + render: ({row: {Message, OriginalSize}}) => { + if (isNil(Message)) { + return EMPTY_DATA_PLACEHOLDER; + } + let encryptedMessage; + let invalid = false; + try { + encryptedMessage = atob(Message); + } catch { + encryptedMessage = i18n('description_failed-decode'); + invalid = true; + } + + const truncated = safeParseNumber(OriginalSize) > TOPIC_MESSAGE_SIZE_LIMIT; + return ( + + {encryptedMessage} + {truncated && ( + + {' '} + {i18n('description_truncated')} + + )} + + ); + }, + width: 500, +}; + +export const sizeColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.SIZE, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.SIZE], + align: DataTable.RIGHT, + render: ({row}) => formatBytes(row.StorageSize), + width: 100, +}; + +export const originalSizeColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.ORIGINAL_SIZE, + header: ( + + ), + align: DataTable.RIGHT, + render: ({row}) => formatBytes(row.OriginalSize), + width: 100, +}; + +export const codecColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.CODEC, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.CODEC], + align: DataTable.RIGHT, + render: ({row: {Codec}}) => { + if (isNil(Codec)) { + return EMPTY_DATA_PLACEHOLDER; + } + return codecNumberToName[Codec] ?? Codec; + }, + width: 70, +}; + +export const producerIdColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.PRODUCERID, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.PRODUCERID], + align: DataTable.LEFT, + render: ({row}) => , + width: 100, +}; + +export const seqNoColumn: Column = { + name: TOPIC_DATA_COLUMNS_IDS.SEQNO, + header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.SEQNO], + align: DataTable.RIGHT, + render: ({row}) => valueOrPlaceholder(row.SeqNo), + width: 70, +}; + export function getAllColumns() { const columns: Column[] = [ - { - name: TOPIC_DATA_COLUMNS_IDS.OFFSET, - header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.OFFSET], - align: DataTable.LEFT, - render: ({row}) => { - const {Offset, removed} = row; - return ( - - {valueOrPlaceholder(Offset)} - - ); - }, - width: 100, - }, - { - name: TOPIC_DATA_COLUMNS_IDS.TIMESTAMP_CREATE, - header: ( - - ), - align: DataTable.LEFT, - render: ({row}) => , - width: 220, - }, - { - name: TOPIC_DATA_COLUMNS_IDS.TIMESTAMP_WRITE, - header: ( - - ), - align: DataTable.LEFT, - render: ({row}) => , - width: 220, - }, - { - name: TOPIC_DATA_COLUMNS_IDS.TS_DIFF, - header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.TS_DIFF], - align: DataTable.RIGHT, - render: ({row}) => { - const numericValue = safeParseNumber(row.TimestampDiff); - return ( - = 100_000})}> - {formatToMs(numericValue)} - - ); - }, - width: 90, - note: i18n('context_ts-diff'), - }, - { - name: TOPIC_DATA_COLUMNS_IDS.METADATA, - header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.METADATA], - align: DataTable.LEFT, - render: ({row: {MessageMetadata}}) => { - if (!MessageMetadata) { - return EMPTY_DATA_PLACEHOLDER; - } - const prepared = MessageMetadata.map( - ({Key = '', Value = ''}) => `${Key}: ${Value}`, - ); - return prepared.join(', '); - }, - width: 200, - }, - { - name: TOPIC_DATA_COLUMNS_IDS.MESSAGE, - header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.MESSAGE], - align: DataTable.LEFT, - render: ({row: {Message, OriginalSize}}) => { - if (isNil(Message)) { - return EMPTY_DATA_PLACEHOLDER; - } - let encryptedMessage; - let invalid = false; - try { - encryptedMessage = atob(Message); - } catch { - encryptedMessage = i18n('description_failed-decode'); - invalid = true; - } - - const truncated = safeParseNumber(OriginalSize) > TOPIC_MESSAGE_SIZE_LIMIT; - return ( - - {encryptedMessage} - {truncated && ( - - {' '} - {i18n('description_truncated')} - - )} - - ); - }, - width: 500, - }, - { - name: TOPIC_DATA_COLUMNS_IDS.SIZE, - header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.SIZE], - align: DataTable.RIGHT, - render: ({row}) => formatBytes(row.StorageSize), - width: 100, - }, - { - name: TOPIC_DATA_COLUMNS_IDS.ORIGINAL_SIZE, - header: ( - - ), - align: DataTable.RIGHT, - render: ({row}) => formatBytes(row.OriginalSize), - width: 100, - }, - { - name: TOPIC_DATA_COLUMNS_IDS.CODEC, - header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.CODEC], - align: DataTable.RIGHT, - render: ({row: {Codec}}) => { - if (isNil(Codec)) { - return EMPTY_DATA_PLACEHOLDER; - } - return codecNumberToName[Codec] ?? Codec; - }, - width: 70, - }, - { - name: TOPIC_DATA_COLUMNS_IDS.PRODUCERID, - header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.PRODUCERID], - align: DataTable.LEFT, - render: ({row}) => ( - - ), - width: 100, - }, - { - name: TOPIC_DATA_COLUMNS_IDS.SEQNO, - header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.SEQNO], - align: DataTable.RIGHT, - render: ({row}) => valueOrPlaceholder(row.SeqNo), - width: 70, - }, + offsetColumn, + timestampCreateColumn, + timestampWriteColumn, + tsDiffColumn, + metadataColumn, + messageColumn, + sizeColumn, + originalSizeColumn, + codecColumn, + producerIdColumn, + seqNoColumn, ]; return columns; } @@ -208,9 +223,52 @@ function TopicDataTimestamp({timestamp}: TopicDataTimestampProps) { ); } -function valueOrPlaceholder( +export function valueOrPlaceholder( value: string | number | undefined, placeholder = EMPTY_DATA_PLACEHOLDER, ) { return isNil(value) ? placeholder : value; } + +interface PartitionIdProps { + offset?: string | number; + removed?: boolean; +} + +function Offset({offset, removed}: PartitionIdProps) { + const getDiagnosticsPageLink = useDiagnosticsPageLinkGetter(); + const {handleActiveOffsetChange} = useTopicDataQueryParams(); + + if (isNil(offset)) { + return EMPTY_DATA_PLACEHOLDER; + } + + if (removed) { + return ( + + {offset} + + ); + } + + const offsetLink = getDiagnosticsPageLink(TENANT_DIAGNOSTICS_TABS_IDS.topicData, { + activeOffset: String(offset), + }); + + const handleClick: React.MouseEventHandler = (e) => { + //if allow to navigate link, the table will be rerendered + e.stopPropagation(); + e.preventDefault(); + const stringOffset = String(offset); + + handleActiveOffsetChange(stringOffset); + }; + + return ( + + + {offset} + + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TopicData/getData.ts b/src/containers/Tenant/Diagnostics/TopicData/getData.ts index 36ee21b3b1..18f5a0a3b7 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/getData.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/getData.ts @@ -5,7 +5,6 @@ import {TOPIC_MESSAGE_SIZE_LIMIT} from '../../../../store/reducers/topic'; import type { TopicDataRequest, TopicDataResponse, - TopicMessage, TopicMessageEnhanced, } from '../../../../types/api/topic'; import {safeParseNumber} from '../../../../utils/utils'; @@ -62,7 +61,7 @@ export const generateTopicDataGetter = ({ setEndOffset, baseOffset = 0, }: GetTopicDataProps) => { - const getTopicData: FetchData = async ({ + const getTopicData: FetchData = async ({ limit, offset: tableOffset, filters, diff --git a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json index ecbc716825..2c1cc012aa 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json @@ -1,5 +1,6 @@ { "label_offset": "Offset", + "label_partition": "Partition ID", "label_timestamp-create": "Timestamp Create", "label_timestamp-write": "Timestamp Write", "label_ts_diff": "TS Diff", @@ -24,5 +25,9 @@ "action_scroll-selected": "Scroll to selected offset", "action_scroll-up": "Scroll to the start", "description_failed-decode": "Failed to decode message", - "description_truncated": "[truncated]" + "description_truncated": "[truncated]", + "context_message-not-found": "Message not found", + "context_get-data-error": "Failed to get message", + "label_download": "Save message to file", + "label_truncated": "Truncated {{size}}" } diff --git a/src/containers/Tenant/Diagnostics/TopicData/useTopicDataQueryParams.ts b/src/containers/Tenant/Diagnostics/TopicData/useTopicDataQueryParams.ts index 108c9d421f..6ae8e777a6 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/useTopicDataQueryParams.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/useTopicDataQueryParams.ts @@ -6,13 +6,16 @@ import type {TopicDataFilterValue} from './utils/types'; import {TopicDataFilterValueParam} from './utils/types'; export function useTopicDataQueryParams() { - const [{selectedPartition, selectedOffset, startTimestamp, topicDataFilter}, setQueryParams] = - useQueryParams({ - selectedPartition: StringParam, - selectedOffset: StringParam, - startTimestamp: NumberParam, - topicDataFilter: TopicDataFilterValueParam, - }); + const [ + {selectedPartition, selectedOffset, startTimestamp, topicDataFilter, activeOffset}, + setQueryParams, + ] = useQueryParams({ + selectedPartition: StringParam, + selectedOffset: StringParam, + startTimestamp: NumberParam, + topicDataFilter: TopicDataFilterValueParam, + activeOffset: StringParam, + }); const handleSelectedPartitionChange = React.useCallback( (value?: string) => { @@ -28,6 +31,13 @@ export function useTopicDataQueryParams() { [setQueryParams], ); + const handleActiveOffsetChange = React.useCallback( + (value?: string) => { + setQueryParams({activeOffset: value}, 'replaceIn'); + }, + [setQueryParams], + ); + const handleStartTimestampChange = React.useCallback( (value?: number) => { setQueryParams({startTimestamp: value}, 'replaceIn'); @@ -47,9 +57,11 @@ export function useTopicDataQueryParams() { selectedOffset, startTimestamp, topicDataFilter, + activeOffset, handleSelectedPartitionChange, handleSelectedOffsetChange, handleStartTimestampChange, handleTopicDataFilterChange, + handleActiveOffsetChange, }; } diff --git a/src/containers/Tenant/Diagnostics/TopicData/utils/constants.ts b/src/containers/Tenant/Diagnostics/TopicData/utils/constants.ts index 4f8873880f..b2334bcf4d 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/utils/constants.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/utils/constants.ts @@ -9,6 +9,9 @@ export const TOPIC_DATA_COLUMNS_TITLES: Record = { get offset() { return i18n('label_offset'); }, + get partition() { + return i18n('label_partition'); + }, get timestampCreate() { return i18n('label_timestamp-create'); }, diff --git a/src/containers/Tenant/Diagnostics/TopicData/utils/types.ts b/src/containers/Tenant/Diagnostics/TopicData/utils/types.ts index 436696a54e..79e782373d 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/utils/types.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/utils/types.ts @@ -4,6 +4,7 @@ import type {ValueOf} from '../../../../../types/common'; import i18n from '../i18n'; export const TOPIC_DATA_COLUMNS_IDS = { + PARTITION: 'partition', OFFSET: 'offset', TIMESTAMP_CREATE: 'timestampCreate', TIMESTAMP_WRITE: 'timestampWrite', diff --git a/src/containers/Tenant/TenantPages.tsx b/src/containers/Tenant/TenantPages.tsx index 909c557f46..81bd208501 100644 --- a/src/containers/Tenant/TenantPages.tsx +++ b/src/containers/Tenant/TenantPages.tsx @@ -13,6 +13,7 @@ type AdditionalQueryParams = { name?: string; backend?: string; selectedPartition?: string; + activeOffset?: string; }; export type TenantQuery = TenantQueryParams | AdditionalQueryParams; diff --git a/src/store/reducers/capabilities/hooks.ts b/src/store/reducers/capabilities/hooks.ts index 2280ff9e9c..335c0a910f 100644 --- a/src/store/reducers/capabilities/hooks.ts +++ b/src/store/reducers/capabilities/hooks.ts @@ -79,7 +79,7 @@ export const useStreamingAvailable = () => { }; export const useTopicDataAvailable = () => { - return useGetFeatureVersion('/viewer/topic_data') >= 1; + return useGetFeatureVersion('/viewer/topic_data') >= 2; }; const useGetSecuritySetting = (feature: SecuritySetting) => { diff --git a/src/store/reducers/partitions/types.ts b/src/store/reducers/partitions/types.ts index 3e131c6aff..829ea13043 100644 --- a/src/store/reducers/partitions/types.ts +++ b/src/store/reducers/partitions/types.ts @@ -2,7 +2,7 @@ import type {ProcessSpeedStats} from '../../../utils/bytesParsers'; // Fields that could be undefined corresponds to partitions without consumers export interface PreparedPartitionData { - partitionId: string; + partitionId: string | number; storeSize: string; writeSpeed: ProcessSpeedStats; diff --git a/src/utils/downloadFile.ts b/src/utils/downloadFile.ts index f07840c7fa..0a94a08409 100644 --- a/src/utils/downloadFile.ts +++ b/src/utils/downloadFile.ts @@ -7,11 +7,16 @@ export function downloadFile(url: string, filename: string) { document.body.removeChild(link); } -export const createAndDownloadJsonFile = (data: unknown, fileName: string) => { - const blob = new Blob([JSON.stringify(data, null, 2)], { +export const createAndDownloadStringifiedJsonFile = (data: string, fileName: string) => { + const blob = new Blob([data], { type: 'application/json', }); const url = URL.createObjectURL(blob); downloadFile(url, `${fileName}.json`); URL.revokeObjectURL(url); }; + +export const createAndDownloadJsonFile = (data: unknown, fileName: string) => { + const preparedData = JSON.stringify(data, null, 2); + createAndDownloadStringifiedJsonFile(preparedData, fileName); +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d235464519..03d6089d52 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -31,7 +31,7 @@ export function bytesToSize(bytes: number) { return val.toPrecision(3) + sizes[i]; } -function bytesToMB(bytes?: number | string) { +export function bytesToMB(bytes?: number | string) { const bytesNumber = Number(bytes); if (isNaN(bytesNumber)) { return ''; From 257662326d2a370517fe0ab81ac6dddbebca7d38 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Thu, 8 May 2025 13:28:41 +0300 Subject: [PATCH 02/10] fix: unipika max size --- .../TopicMessageDetails/TopicMessageDetails.scss | 1 + .../TopicMessageDetails/components/TopicMessage.tsx | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.scss b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.scss index f9b9b2bbfd..64e42691d9 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.scss @@ -39,6 +39,7 @@ &__string-message { overflow: hidden; + white-space: pre-wrap; word-break: break-all; } } diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx index c5b0553511..7a846f61a3 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx @@ -14,7 +14,7 @@ import {MESSAGE_SIZE_LIMIT, b} from '../shared'; import {TopicDataSection} from './TopicDataSection'; -const UNIPIKA_MAX_SIZE = 100_000; +const UNIPIKA_MAX_SIZE = 1_000_000; interface TopicMessageProps { message: string; @@ -47,6 +47,8 @@ export function TopicMessage({offset, size, message}: TopicMessageProps) { let convertedMessage; if (typeof preparedMessage === 'object' && safeParseNumber(size) <= UNIPIKA_MAX_SIZE) { convertedMessage = unipikaConvert(preparedMessage); + } else if (preparedMessage && typeof preparedMessage === 'object') { + preparedMessage = JSON.stringify(preparedMessage, null, 2); } return {preparedMessage, decodedMessage, convertedMessage}; @@ -66,11 +68,7 @@ export function TopicMessage({offset, size, message}: TopicMessageProps) { ) : (
{/* key is used to reset string's state when toggle fullscreen: otherwise if very long string is expanded, it may be performance issues on open fullscreen mode https://github.com/ydb-platform/ydb-embedded-ui/issues/2265 */} - +
); From 6269d074b26ed3c8585de905cdf20cab2079809b Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Tue, 13 May 2025 15:00:20 +0300 Subject: [PATCH 03/10] fixes --- .../Tenant/Diagnostics/TopicData/TopicData.scss | 3 +++ .../Tenant/Diagnostics/TopicData/TopicData.tsx | 10 +++++----- .../TopicDataControls/TopicDataControls.tsx | 13 +++++++++++++ .../TopicMessageDetails/TopicMessageDetails.tsx | 8 ++++++-- .../TopicMessageDetails/components/TopicMessage.tsx | 6 +++--- .../Tenant/Diagnostics/TopicData/i18n/en.json | 2 +- src/utils/downloadFile.ts | 8 ++++---- 7 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss b/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss index 48b346ea5b..dc90e5f5d3 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss @@ -14,6 +14,9 @@ &__date-picker { min-width: 265px; } + &__offset-input { + width: max-content; + } &__column-setup { margin-left: auto; } diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx index 8fc79deceb..2fdd755597 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -184,7 +184,7 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { ); //this variable is used to scroll to active offset the very first time on open page - const initialActiveOffset = React.useMemo(() => activeOffset, []); + const initialActiveOffset = React.useRef(activeOffset); React.useEffect(() => { if (isFetching) { @@ -192,19 +192,19 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { } let currentOffset: number | undefined; - - if (isNil(initialActiveOffset)) { + if (isNil(initialActiveOffset.current)) { const messages = currentData?.Messages; if (messages?.length) { currentOffset = safeParseNumber(messages[0].Offset); } } else { - currentOffset = safeParseNumber(initialActiveOffset); + currentOffset = safeParseNumber(initialActiveOffset.current); + initialActiveOffset.current = undefined; } if (!isNil(currentOffset)) { scrollToOffset(currentOffset); } - }, [currentData, isFetching, scrollToOffset, initialActiveOffset]); + }, [currentData, isFetching, scrollToOffset]); const renderControls: RenderControls = () => { return ( diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx index dccb3f5fe1..b0bd394728 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx @@ -119,11 +119,22 @@ function TopicDataStartControls({scrollToOffset}: TopicDataStartControlsProps) { selectedOffset, startTimestamp, topicDataFilter, + activeOffset, handleSelectedOffsetChange, handleStartTimestampChange, handleTopicDataFilterChange, } = useTopicDataQueryParams(); + const inputRef = React.useRef(null); + + const isDrawerVisible = !isNil(activeOffset); + + React.useEffect(() => { + if (isDrawerVisible) { + inputRef.current?.blur(); + } + }, [isDrawerVisible]); + const onFilterChange = React.useCallback( (value: TopicDataFilterValue) => { if (value === 'TIMESTAMP') { @@ -173,6 +184,8 @@ function TopicDataStartControls({scrollToOffset}: TopicDataStartControlsProps) { {topicDataFilter === 'OFFSET' && ( - {i18n('context_message-not-found')} + + {i18n('context_message-not-found', {offset: activeOffset})} +
); diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx index 7a846f61a3..1c1279a0c0 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx @@ -6,7 +6,7 @@ import {ActionTooltip, Button, ClipboardButton, Flex, Icon, Text} from '@gravity import {JsonViewer} from '../../../../../../components/JsonViewer/JsonViewer'; import {unipikaConvert} from '../../../../../../components/JsonViewer/unipika/unipika'; import ShortyString from '../../../../../../components/ShortyString/ShortyString'; -import {createAndDownloadStringifiedJsonFile} from '../../../../../../utils/downloadFile'; +import {createAndDownloadFile} from '../../../../../../utils/downloadFile'; import {useTypedSelector} from '../../../../../../utils/hooks'; import {bytesToMB, safeParseNumber} from '../../../../../../utils/utils'; import i18n from '../../i18n'; @@ -80,13 +80,13 @@ export function TopicMessage({offset, size, message}: TopicMessageProps) { view="flat-secondary" onClick={(e) => { e.stopPropagation(); - createAndDownloadStringifiedJsonFile( + createAndDownloadFile( decodedMessage, `topic-message-${offset ?? 'unknown-offset'}`, ); }} > - JSON + diff --git a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json index 2c1cc012aa..921c372288 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json @@ -26,7 +26,7 @@ "action_scroll-up": "Scroll to the start", "description_failed-decode": "Failed to decode message", "description_truncated": "[truncated]", - "context_message-not-found": "Message not found", + "context_message-not-found": "Message with offset {{offset}} not found", "context_get-data-error": "Failed to get message", "label_download": "Save message to file", "label_truncated": "Truncated {{size}}" diff --git a/src/utils/downloadFile.ts b/src/utils/downloadFile.ts index 0a94a08409..a80b2ad9b9 100644 --- a/src/utils/downloadFile.ts +++ b/src/utils/downloadFile.ts @@ -7,16 +7,16 @@ export function downloadFile(url: string, filename: string) { document.body.removeChild(link); } -export const createAndDownloadStringifiedJsonFile = (data: string, fileName: string) => { +export const createAndDownloadFile = (data: string, fileName: string, type?: string) => { const blob = new Blob([data], { - type: 'application/json', + type, }); const url = URL.createObjectURL(blob); - downloadFile(url, `${fileName}.json`); + downloadFile(url, fileName); URL.revokeObjectURL(url); }; export const createAndDownloadJsonFile = (data: unknown, fileName: string) => { const preparedData = JSON.stringify(data, null, 2); - createAndDownloadStringifiedJsonFile(preparedData, fileName); + createAndDownloadFile(preparedData, `${fileName}.json`, 'application/json'); }; From 3bd7554438a056b42fda6c9932ebcd3925c2411d Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Tue, 13 May 2025 15:05:02 +0300 Subject: [PATCH 04/10] fix --- .../TopicData/TopicMessageDetails/TopicMessageDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.tsx index e4add36d48..514ac3673b 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.tsx @@ -96,7 +96,7 @@ export function TopicMessageDetails({database, path}: TopicMessageDetailsProps) justifyContent="center" height="100%" > - + {i18n('context_message-not-found', {offset: activeOffset})} From 5c4489fe60b63a235736383f652a62069f4ec60a Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 14 May 2025 18:35:54 +0300 Subject: [PATCH 05/10] fixes --- src/components/EntityStatus/EntityStatus.tsx | 10 ++- src/components/JsonViewer/JsonViewer.scss | 2 + .../JsonViewer/components/FullValueDialog.tsx | 6 +- .../Diagnostics/TopicData/TopicData.scss | 6 +- .../Diagnostics/TopicData/TopicData.tsx | 65 ++++++++++++++++--- .../TopicDataControls/TopicDataControls.tsx | 2 +- .../TopicMessageDetails.scss | 18 +++-- .../components/TopicDataSection.tsx | 14 ++-- .../components/TopicMessage.tsx | 4 +- .../components/TopicMessageGeneralInfo.tsx | 38 +++++++---- .../TopicMessageDetails/components/fields.tsx | 14 +++- .../TopicData/columns/Columns.scss | 22 +++++-- .../Diagnostics/TopicData/columns/columns.tsx | 58 +++++++++++++---- .../Tenant/Diagnostics/TopicData/getData.ts | 14 ++-- .../Tenant/Diagnostics/TopicData/i18n/en.json | 4 +- src/store/reducers/topic.ts | 2 +- src/types/api/topic.ts | 1 + 17 files changed, 204 insertions(+), 76 deletions(-) diff --git a/src/components/EntityStatus/EntityStatus.tsx b/src/components/EntityStatus/EntityStatus.tsx index b4ac4b71c1..5e72b2bb50 100644 --- a/src/components/EntityStatus/EntityStatus.tsx +++ b/src/components/EntityStatus/EntityStatus.tsx @@ -16,6 +16,7 @@ const b = cn('entity-status'); interface EntityStatusProps { status?: EFlag; name?: string; + renderName?: (name?: string) => React.ReactNode; label?: string; path?: string; iconPath?: string; @@ -34,9 +35,14 @@ interface EntityStatusProps { className?: string; } +function defaultRenderName(name?: string) { + return name ?? ''; +} + export function EntityStatus({ status = EFlag.Grey, name = '', + renderName = defaultRenderName, label, path, iconPath, @@ -75,14 +81,14 @@ export function EntityStatus({ if (externalLink) { return ( - {name} + {renderName(name)} ); } return ( - {name} + {renderName(name)} ); } diff --git a/src/components/JsonViewer/JsonViewer.scss b/src/components/JsonViewer/JsonViewer.scss index 1286148b1d..9b3edc81de 100644 --- a/src/components/JsonViewer/JsonViewer.scss +++ b/src/components/JsonViewer/JsonViewer.scss @@ -4,6 +4,8 @@ --data-table-row-height: 20px; --toolbar-background-color: var(--g-color-base-background); + width: max-content; + &__toolbar { position: sticky; z-index: 2; diff --git a/src/components/JsonViewer/components/FullValueDialog.tsx b/src/components/JsonViewer/components/FullValueDialog.tsx index a71ac911f7..77645043a9 100644 --- a/src/components/JsonViewer/components/FullValueDialog.tsx +++ b/src/components/JsonViewer/components/FullValueDialog.tsx @@ -14,8 +14,12 @@ interface FullValueDialogProps { } export function FullValueDialog({onClose, text, starts, length}: FullValueDialogProps) { + //if dialog opens from Drawer, outside click should not close Drawer + const handleClickOutside = (e: MouseEvent) => { + e.stopPropagation(); + }; return ( - + diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss b/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss index dc90e5f5d3..56f3fcc13a 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss @@ -17,9 +17,6 @@ &__offset-input { width: max-content; } - &__column-setup { - margin-left: auto; - } &__row { &_active { @@ -33,4 +30,7 @@ color: var(--g-color-text-secondary); } } + &__scroll-button { + margin-right: var(--g-spacing-half); + } } diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx index 2fdd755597..30fcb78acb 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -46,6 +46,7 @@ interface TopicDataProps { database: string; parentRef: React.RefObject; } +const PAGINATED_TABLE_LIMIT = 50_000; const columns = getAllColumns(); @@ -106,19 +107,45 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { {pollingInterval: autoRefreshInterval}, ); + const calculateBoundOffsets = React.useCallback( + ({startOffset, endOffset}: {startOffset: number; endOffset: number}) => { + const normolizedNewStartOffset = Math.max( + endOffset - PAGINATED_TABLE_LIMIT, + safeParseNumber(startOffset), + ); + return {newStartOffset: normolizedNewStartOffset, newEndOffset: endOffset}; + }, + [], + ); + React.useEffect(() => { const selectedPartitionData = partitions?.find( ({partitionId}) => partitionId === selectedPartition, ); if (selectedPartitionData) { - if (!baseOffset) { - setBaseOffset(safeParseNumber(selectedPartitionData.startOffset)); - } + let endOffset = baseEndOffset; if (!baseEndOffset) { - setBaseEndOffset(safeParseNumber(selectedPartitionData.endOffset)); + endOffset = safeParseNumber(selectedPartitionData.endOffset); + setBaseEndOffset(endOffset); + } + if (!baseOffset) { + const {newStartOffset} = calculateBoundOffsets({ + startOffset: safeParseNumber(selectedPartitionData.startOffset), + endOffset: endOffset ?? 0, + }); + + setBaseOffset(newStartOffset); } } - }, [selectedPartition, partitions, baseOffset, baseEndOffset, startOffset, endOffset]); + }, [ + selectedPartition, + partitions, + baseOffset, + baseEndOffset, + startOffset, + endOffset, + calculateBoundOffsets, + ]); React.useEffect(() => { if (partitions && partitions.length && isNil(selectedPartition)) { @@ -137,6 +164,22 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { REQUIRED_TOPIC_DATA_COLUMNS, ); + const setBoundOffsets = React.useCallback( + ({startOffset, endOffset}: {startOffset: number; endOffset: number}) => { + const {newStartOffset, newEndOffset} = calculateBoundOffsets({ + startOffset, + endOffset, + }); + const normolizedNewStartOffset = Math.max( + newEndOffset - PAGINATED_TABLE_LIMIT, + safeParseNumber(newStartOffset), + ); + setStartOffset(normolizedNewStartOffset); + setEndOffset(newEndOffset); + }, + [calculateBoundOffsets], + ); + React.useEffect(() => { //values should be recalculated only when data is fetched if (isFetching || (!currentData && !error)) { @@ -148,10 +191,12 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { setEmptyData(true); } if (currentData) { - setStartOffset(safeParseNumber(currentData.StartOffset)); - setEndOffset(safeParseNumber(currentData.EndOffset)); + setBoundOffsets({ + startOffset: safeParseNumber(currentData.StartOffset), + endOffset: safeParseNumber(currentData.EndOffset), + }); } - }, [isFetching, currentData, error]); + }, [isFetching, currentData, error, setBoundOffsets]); const tableFilters = React.useMemo( () => ({ @@ -238,8 +283,8 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { }; const getTopicData = React.useMemo( - () => generateTopicDataGetter({setEndOffset, setStartOffset, baseOffset}), - [baseOffset], + () => generateTopicDataGetter({setBoundOffsets, baseOffset}), + [baseOffset, setBoundOffsets], ); const closeDrawer = React.useCallback(() => { diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx index b0bd394728..f0c4bf677b 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx @@ -99,7 +99,6 @@ export function TopicDataControls({ )}