diff --git a/src/components/Drawer/Drawer.scss b/src/components/Drawer/Drawer.scss index f93c4517b..bbeff174d 100644 --- a/src/components/Drawer/Drawer.scss +++ b/src/components/Drawer/Drawer.scss @@ -23,7 +23,7 @@ top: 0; left: 0; - padding: var(--g-spacing-4) var(--g-spacing-4) 0 var(--g-spacing-6); + padding: var(--g-spacing-4) var(--g-spacing-4) 0 var(--g-spacing-4); background-color: var(--g-color-base-background); } @@ -34,5 +34,10 @@ flex-direction: column; height: 100%; + //split-pane resizer size + margin-left: var(--g-spacing-2); + } + &__click-handler { + display: contents; } } diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx index 329c45030..2688cb59c 100644 --- a/src/components/Drawer/Drawer.tsx +++ b/src/components/Drawer/Drawer.tsx @@ -17,6 +17,10 @@ const b = cn('ydb-drawer'); import './Drawer.scss'; +type DrawerEvent = MouseEvent & { + _capturedInsideDrawer?: boolean; +}; + interface DrawerPaneContentWrapperProps { isVisible: boolean; onClose: () => void; @@ -64,7 +68,11 @@ const DrawerPaneContentWrapper = ({ return undefined; } - const handleClickOutside = (event: MouseEvent) => { + const handleClickOutside = (event: DrawerEvent) => { + //skip if event is captured inside drawer or not triggered by user + if (event._capturedInsideDrawer || !event.isTrusted) { + return; + } if ( isVisible && drawerRef.current && @@ -91,6 +99,10 @@ const DrawerPaneContentWrapper = ({ } }; + const handleClickInsideDrawer = (event: React.MouseEvent) => { + (event.nativeEvent as DrawerEvent)._capturedInsideDrawer = true; + }; + return ( - {children} +
+ {children} +
); @@ -184,7 +198,11 @@ export const DrawerWrapper = ({ } return ( - + {title} {controls} diff --git a/src/components/EnableFullscreenButton/EnableFullscreenButton.tsx b/src/components/EnableFullscreenButton/EnableFullscreenButton.tsx index 7f13fe581..8e3f46288 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 fbb5c411e..8112820b0 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 000000000..b9962ea2e --- /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 000000000..4511a1b64 --- /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 000000000..bfdefdcc2 --- /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 000000000..b62ad0f8d --- /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 5be8951b6..7f3e9d1d3 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 f2230f669..7a99c0bbb 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 36ee21b3b..18f5a0a3b 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 ecbc71682..2c1cc012a 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 108c9d421..6ae8e777a 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 4f8873880..b2334bcf4 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 436696a54..79e782373 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 909c557f4..81bd20850 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 2280ff9e9..335c0a910 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 3e131c6af..829ea1304 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 f07840c7f..0a94a0840 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 d23546451..03d6089d5 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 '';