From 6d17e257d56d46e401420fe2f237da1fa30ba148 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 11 Apr 2025 16:53:25 +0300 Subject: [PATCH 01/21] refactor: new DebouncedInput component --- .../DebouncedInput/DebouncedInput.tsx | 35 +++++++++++++++++++ src/components/Search/Search.tsx | 35 ++++--------------- 2 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 src/components/DebouncedInput/DebouncedInput.tsx diff --git a/src/components/DebouncedInput/DebouncedInput.tsx b/src/components/DebouncedInput/DebouncedInput.tsx new file mode 100644 index 000000000..60df464b7 --- /dev/null +++ b/src/components/DebouncedInput/DebouncedInput.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import type {TextInputProps} from '@gravity-ui/uikit'; +import {TextInput} from '@gravity-ui/uikit'; + +interface SearchProps extends TextInputProps { + debounce?: number; +} + +export const DebouncedInput = ({onUpdate, value = '', debounce = 200, ...rest}: SearchProps) => { + const [currentValue, setCurrentValue] = React.useState(value); + + const timer = React.useRef(); + + React.useEffect(() => { + setCurrentValue((prevValue) => { + if (prevValue !== value) { + return value; + } + + return prevValue; + }); + }, [value]); + + const onSearchValueChange = (newValue: string) => { + setCurrentValue(newValue); + + window.clearTimeout(timer.current); + timer.current = window.setTimeout(() => { + onUpdate?.(newValue); + }, debounce); + }; + + return ; +}; diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search.tsx index d880013dc..8b4dc6e80 100644 --- a/src/components/Search/Search.tsx +++ b/src/components/Search/Search.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import {TextInput} from '@gravity-ui/uikit'; - import {cn} from '../../utils/cn'; +import {DebouncedInput} from '../DebouncedInput/DebouncedInput'; import './Search.scss'; @@ -23,43 +22,21 @@ export const Search = ({ value = '', width, className, - debounce = 200, + debounce, placeholder, inputRef, }: SearchProps) => { - const [searchValue, setSearchValue] = React.useState(value); - - const timer = React.useRef(); - - React.useEffect(() => { - setSearchValue((prevValue) => { - if (prevValue !== value) { - return value; - } - - return prevValue; - }); - }, [value]); - - const onSearchValueChange = (newValue: string) => { - setSearchValue(newValue); - - window.clearTimeout(timer.current); - timer.current = window.setTimeout(() => { - onChange?.(newValue); - }, debounce); - }; - return ( - ); }; From 6c8e83841b8a9e770bf9f1270b3538d6c14e16c0 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 11 Apr 2025 16:55:09 +0300 Subject: [PATCH 02/21] feat(Partitions): partition id is string now --- .../MultilineTableHeader.scss | 3 ++ .../MultilineTableHeader.tsx | 16 ++++++ .../Partitions/Headers/Headers.scss | 4 -- .../Partitions/Headers/Headers.tsx | 8 --- .../Diagnostics/Partitions/Partitions.scss | 14 +---- .../Diagnostics/Partitions/Partitions.tsx | 20 +++++--- .../Partitions/columns/columns.tsx | 51 +++++++++++++++---- 7 files changed, 74 insertions(+), 42 deletions(-) create mode 100644 src/components/MultilineTableHeader/MultilineTableHeader.scss create mode 100644 src/components/MultilineTableHeader/MultilineTableHeader.tsx diff --git a/src/components/MultilineTableHeader/MultilineTableHeader.scss b/src/components/MultilineTableHeader/MultilineTableHeader.scss new file mode 100644 index 000000000..ac2ded5ec --- /dev/null +++ b/src/components/MultilineTableHeader/MultilineTableHeader.scss @@ -0,0 +1,3 @@ +.ydb-mulitiline-table-header { + white-space: normal; +} diff --git a/src/components/MultilineTableHeader/MultilineTableHeader.tsx b/src/components/MultilineTableHeader/MultilineTableHeader.tsx new file mode 100644 index 000000000..daa94c5e4 --- /dev/null +++ b/src/components/MultilineTableHeader/MultilineTableHeader.tsx @@ -0,0 +1,16 @@ +import {cn} from '../../utils/cn'; + +import './MultilineTableHeader.scss'; + +const b = cn('ydb-mulitiline-table-header'); + +interface MultilineTableHeaderProps { + title?: string; +} + +export function MultilineTableHeader({title}: MultilineTableHeaderProps) { + if (!title) { + return null; + } + return
{title}
; +} diff --git a/src/containers/Tenant/Diagnostics/Partitions/Headers/Headers.scss b/src/containers/Tenant/Diagnostics/Partitions/Headers/Headers.scss index fa5c0c33d..80c58ebfd 100644 --- a/src/containers/Tenant/Diagnostics/Partitions/Headers/Headers.scss +++ b/src/containers/Tenant/Diagnostics/Partitions/Headers/Headers.scss @@ -1,8 +1,4 @@ .ydb-diagnostics-partitions-columns-header { - &__multiline { - white-space: normal; - } - &__read-session { width: 80px; diff --git a/src/containers/Tenant/Diagnostics/Partitions/Headers/Headers.tsx b/src/containers/Tenant/Diagnostics/Partitions/Headers/Headers.tsx index d30227f43..068e4978a 100644 --- a/src/containers/Tenant/Diagnostics/Partitions/Headers/Headers.tsx +++ b/src/containers/Tenant/Diagnostics/Partitions/Headers/Headers.tsx @@ -8,14 +8,6 @@ import './Headers.scss'; const b = cn('ydb-diagnostics-partitions-columns-header'); -interface MultilineHeaderProps { - title: string; -} - -export const MultilineHeader = ({title}: MultilineHeaderProps) => ( -
{title}
-); - export const ReadSessionHeader = () => (
{PARTITIONS_COLUMNS_TITLES[PARTITIONS_COLUMNS_IDS.READ_SESSION_ID]} diff --git a/src/containers/Tenant/Diagnostics/Partitions/Partitions.scss b/src/containers/Tenant/Diagnostics/Partitions/Partitions.scss index f94cec1dc..451ff518c 100644 --- a/src/containers/Tenant/Diagnostics/Partitions/Partitions.scss +++ b/src/containers/Tenant/Diagnostics/Partitions/Partitions.scss @@ -5,11 +5,12 @@ flex-grow: 1; height: 100%; + margin-right: -20px; @include mixins.flex-container(); &__controls { - @include mixins.controls(); + width: 100%; } &__consumer-select { @@ -30,17 +31,6 @@ } } - &__table-wrapper { - overflow: auto; - @include mixins.flex-container(); - } - - &__table-content { - overflow: auto; - - height: 100%; - } - &__table { @include mixins.freeze-nth-column(1); } diff --git a/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx b/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx index c32923596..cb0139568 100644 --- a/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx +++ b/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx @@ -5,9 +5,10 @@ import {skipToken} from '@reduxjs/toolkit/query'; import {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {TableSkeleton} from '../../../../components/TableSkeleton/TableSkeleton'; +import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; import {nodesListApi, selectNodesMap} from '../../../../store/reducers/nodesList'; import {partitionsApi, setSelectedConsumer} from '../../../../store/reducers/partitions/partitions'; -import {selectConsumersNames, topicApi} from '../../../../store/reducers/topic'; +import {selectConsumersNames, topicApi} from '../../../../store/reducers/topic/topic'; import {cn} from '../../../../utils/cn'; import {DEFAULT_TABLE_SETTINGS, PARTITIONS_HIDDEN_COLUMNS_KEY} from '../../../../utils/constants'; import { @@ -136,12 +137,15 @@ export const Partitions = ({path, database}: PartitionsProps) => { }; return ( -
-
{renderControls()}
- {error ? : null} -
-
{partitionsData ? renderContent() : null}
-
-
+ + + {renderControls()} + + + + {error ? : null} + {partitionsData ? renderContent() : null} + + ); }; diff --git a/src/containers/Tenant/Diagnostics/Partitions/columns/columns.tsx b/src/containers/Tenant/Diagnostics/Partitions/columns/columns.tsx index 0a9f677d9..ef4e8c5fb 100644 --- a/src/containers/Tenant/Diagnostics/Partitions/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/Partitions/columns/columns.tsx @@ -2,13 +2,16 @@ import type {Column} from '@gravity-ui/react-data-table'; import DataTable from '@gravity-ui/react-data-table'; import {EntityStatus} from '../../../../../components/EntityStatus/EntityStatus'; +import {MultilineTableHeader} from '../../../../../components/MultilineTableHeader/MultilineTableHeader'; import {SpeedMultiMeter} from '../../../../../components/SpeedMultiMeter'; +import {useTopicDataAvailable} from '../../../../../store/reducers/capabilities/hooks'; +import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {cn} from '../../../../../utils/cn'; import {formatBytes, formatMsToUptime} from '../../../../../utils/dataFormatters/dataFormatters'; import {isNumeric} from '../../../../../utils/utils'; import {getDefaultNodePath} from '../../../../Node/NodePages'; +import {useDiagnosticsPageLinkGetter} from '../../DiagnosticsPages'; import { - MultilineHeader, ReadLagsHeader, ReadSessionHeader, UncommitedMessagesHeader, @@ -36,19 +39,22 @@ export const allColumns: Column[] = [ { name: PARTITIONS_COLUMNS_IDS.PARTITION_ID, header: ( - ), sortAccessor: (row) => isNumeric(row.partitionId) && Number(row.partitionId), align: DataTable.LEFT, - render: ({row}) => row.partitionId, + render: ({row}) => , }, { name: PARTITIONS_COLUMNS_IDS.STORE_SIZE, header: ( - + ), + sortAccessor: (row) => isNumeric(row.storeSize) && Number(row.storeSize), align: DataTable.RIGHT, render: ({row}) => formatBytes(row.storeSize), }, @@ -137,7 +143,7 @@ export const allColumns: Column[] = [ { name: PARTITIONS_COLUMNS_IDS.START_OFFSET, header: ( - ), @@ -148,7 +154,9 @@ export const allColumns: Column[] = [ { name: PARTITIONS_COLUMNS_IDS.END_OFFSET, header: ( - + ), sortAccessor: (row) => isNumeric(row.endOffset) && Number(row.endOffset), align: DataTable.RIGHT, @@ -157,7 +165,7 @@ export const allColumns: Column[] = [ { name: PARTITIONS_COLUMNS_IDS.COMMITED_OFFSET, header: ( - ), @@ -180,7 +188,7 @@ export const allColumns: Column[] = [ { name: PARTITIONS_COLUMNS_IDS.READER_NAME, header: ( - ), @@ -196,7 +204,7 @@ export const allColumns: Column[] = [ { name: PARTITIONS_COLUMNS_IDS.PARTITION_HOST, header: ( - ), @@ -217,7 +225,7 @@ export const allColumns: Column[] = [ { name: PARTITIONS_COLUMNS_IDS.CONNECTION_HOST, header: ( - ), @@ -244,3 +252,26 @@ export const generalColumns = allColumns.filter((column) => { column.name as (typeof generalPartitionColumnsIds)[number], ); }); + +interface PartitionIdProps { + id: string; +} + +function PartitionId({id}: PartitionIdProps) { + const getDiagnosticsPageLink = useDiagnosticsPageLinkGetter(); + const topicDataAvailable = useTopicDataAvailable(); + + return ( + + ); +} From 05a07c0a1335899918cbcbf4e5c34cfd200771ca Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 11 Apr 2025 16:56:13 +0300 Subject: [PATCH 03/21] refactor: move convertToNumber into utils --- src/components/MetricChart/getDefaultDataFormatter.ts | 11 +---------- src/utils/utils.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/MetricChart/getDefaultDataFormatter.ts b/src/components/MetricChart/getDefaultDataFormatter.ts index a1a21c50d..70593d98c 100644 --- a/src/components/MetricChart/getDefaultDataFormatter.ts +++ b/src/components/MetricChart/getDefaultDataFormatter.ts @@ -2,7 +2,7 @@ import {formatBytes} from '../../utils/bytesParsers'; import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; import {roundToPrecision} from '../../utils/dataFormatters/dataFormatters'; import {formatToMs} from '../../utils/timeParsers'; -import {isNumeric} from '../../utils/utils'; +import {convertToNumber} from '../../utils/utils'; import type {ChartDataType, ChartValue} from './types'; @@ -43,12 +43,3 @@ function formatChartValueToPercent(value: ChartValue) { } return Math.round(convertToNumber(value) * 100) + '%'; } - -// Numeric values expected, not numeric value should be displayd as 0 -function convertToNumber(value: unknown): number { - if (isNumeric(value)) { - return Number(value); - } - - return 0; -} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 7e91fe7c2..a0cb1914a 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -97,3 +97,12 @@ export function toExponential(value: number, precision?: number) { } export const UNBREAKABLE_GAP = '\xa0'; + +// Numeric values expected, not numeric value should be displayd as 0 +export function convertToNumber(value: unknown): number { + if (isNumeric(value)) { + return Number(value); + } + + return 0; +} From fb81011d9db6b88f53261ce5ee500922946fefcb Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 11 Apr 2025 17:09:24 +0300 Subject: [PATCH 04/21] refactor: move renderPaginatedTableErrorMessage to utils --- src/containers/Nodes/NodesTable.tsx | 3 ++- src/containers/Nodes/shared.tsx | 12 +----------- src/containers/Storage/PaginatedStorageGroups.tsx | 3 ++- src/containers/Storage/PaginatedStorageNodes.tsx | 3 ++- src/containers/Storage/shared.tsx | 11 ----------- src/utils/renderPaginatedTableErrorMessage.tsx | 11 +++++++++++ 6 files changed, 18 insertions(+), 25 deletions(-) create mode 100644 src/utils/renderPaginatedTableErrorMessage.tsx diff --git a/src/containers/Nodes/NodesTable.tsx b/src/containers/Nodes/NodesTable.tsx index b8d07b62d..c64f0f99c 100644 --- a/src/containers/Nodes/NodesTable.tsx +++ b/src/containers/Nodes/NodesTable.tsx @@ -8,11 +8,12 @@ import type {NodesFilters, NodesPreparedEntity} from '../../store/reducers/nodes import type {ProblemFilterValue} from '../../store/reducers/settings/types'; import type {NodesGroupByField, NodesPeerRole} from '../../types/api/nodes'; import {NodesUptimeFilterValues} from '../../utils/nodes'; +import {renderPaginatedTableErrorMessage} from '../../utils/renderPaginatedTableErrorMessage'; import type {Column} from '../../utils/tableUtils/types'; import {getNodes} from './getNodes'; import i18n from './i18n'; -import {getRowClassName, renderPaginatedTableErrorMessage} from './shared'; +import {getRowClassName} from './shared'; interface NodesTableProps { path?: string; diff --git a/src/containers/Nodes/shared.tsx b/src/containers/Nodes/shared.tsx index 257f7e999..bef440a05 100644 --- a/src/containers/Nodes/shared.tsx +++ b/src/containers/Nodes/shared.tsx @@ -1,6 +1,4 @@ -import {AccessDenied} from '../../components/Errors/403'; -import {ResponseError} from '../../components/Errors/ResponseError'; -import type {GetRowClassName, RenderErrorMessage} from '../../components/PaginatedTable'; +import type {GetRowClassName} from '../../components/PaginatedTable'; import type {NodesPreparedEntity} from '../../store/reducers/nodes/types'; import {cn} from '../../utils/cn'; import {isUnavailableNode} from '../../utils/nodes'; @@ -10,11 +8,3 @@ export const b = cn('ydb-nodes'); export const getRowClassName: GetRowClassName = (row) => { return b('node', {unavailable: isUnavailableNode(row)}); }; - -export const renderPaginatedTableErrorMessage: RenderErrorMessage = (error) => { - if (error && error.status === 403) { - return ; - } - - return ; -}; diff --git a/src/containers/Storage/PaginatedStorageGroups.tsx b/src/containers/Storage/PaginatedStorageGroups.tsx index e9628cc92..dc0f50b8d 100644 --- a/src/containers/Storage/PaginatedStorageGroups.tsx +++ b/src/containers/Storage/PaginatedStorageGroups.tsx @@ -10,6 +10,7 @@ import { } from '../../store/reducers/capabilities/hooks'; import {storageApi} from '../../store/reducers/storage/storage'; import {useAutoRefreshInterval} from '../../utils/hooks'; +import {renderPaginatedTableErrorMessage} from '../../utils/renderPaginatedTableErrorMessage'; import type {PaginatedStorageProps} from './PaginatedStorage'; import {StorageGroupsControls} from './StorageControls/StorageControls'; @@ -18,7 +19,7 @@ import {useStorageGroupsSelectedColumns} from './StorageGroups/columns/hooks'; import {TableGroup} from './TableGroup/TableGroup'; import {useExpandedGroups} from './TableGroup/useExpandedTableGroups'; import i18n from './i18n'; -import {b, renderPaginatedTableErrorMessage} from './shared'; +import {b} from './shared'; import {useStorageQueryParams} from './useStorageQueryParams'; import './Storage.scss'; diff --git a/src/containers/Storage/PaginatedStorageNodes.tsx b/src/containers/Storage/PaginatedStorageNodes.tsx index b3f6345f6..817a27b90 100644 --- a/src/containers/Storage/PaginatedStorageNodes.tsx +++ b/src/containers/Storage/PaginatedStorageNodes.tsx @@ -13,6 +13,7 @@ import type {NodesGroupByField} from '../../types/api/nodes'; import {useAutoRefreshInterval} from '../../utils/hooks'; import {useAdditionalNodesProps} from '../../utils/hooks/useAdditionalNodesProps'; import {NodesUptimeFilterValues} from '../../utils/nodes'; +import {renderPaginatedTableErrorMessage} from '../../utils/renderPaginatedTableErrorMessage'; import type {PaginatedStorageProps} from './PaginatedStorage'; import {StorageNodesControls} from './StorageControls/StorageControls'; @@ -22,7 +23,7 @@ import type {StorageNodesColumnsSettings} from './StorageNodes/columns/types'; import {TableGroup} from './TableGroup/TableGroup'; import {useExpandedGroups} from './TableGroup/useExpandedTableGroups'; import i18n from './i18n'; -import {b, renderPaginatedTableErrorMessage} from './shared'; +import {b} from './shared'; import type {StorageViewContext} from './types'; import {useStorageQueryParams} from './useStorageQueryParams'; import {useStorageColumnsSettings} from './utils'; diff --git a/src/containers/Storage/shared.tsx b/src/containers/Storage/shared.tsx index 6c59dcfbe..be9408069 100644 --- a/src/containers/Storage/shared.tsx +++ b/src/containers/Storage/shared.tsx @@ -1,14 +1,3 @@ -import {AccessDenied} from '../../components/Errors/403'; -import {ResponseError} from '../../components/Errors/ResponseError'; -import type {RenderErrorMessage} from '../../components/PaginatedTable'; import {cn} from '../../utils/cn'; export const b = cn('global-storage'); - -export const renderPaginatedTableErrorMessage: RenderErrorMessage = (error) => { - if (error.status === 403) { - return ; - } - - return ; -}; diff --git a/src/utils/renderPaginatedTableErrorMessage.tsx b/src/utils/renderPaginatedTableErrorMessage.tsx new file mode 100644 index 000000000..ee7635482 --- /dev/null +++ b/src/utils/renderPaginatedTableErrorMessage.tsx @@ -0,0 +1,11 @@ +import {AccessDenied} from '../components/Errors/403'; +import {ResponseError} from '../components/Errors/ResponseError'; +import type {RenderErrorMessage} from '../components/PaginatedTable'; + +export const renderPaginatedTableErrorMessage: RenderErrorMessage = (error) => { + if (error.status === 403) { + return ; + } + + return ; +}; From 3607096519babb7d3b4a15253d7b6be6b31e440c Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 11 Apr 2025 17:11:56 +0300 Subject: [PATCH 05/21] feat(PaginatedTable): add note for column head --- .../PaginatedTable/PaginatedTable.scss | 45 +++++++++++++------ src/components/PaginatedTable/TableHead.tsx | 30 ++++++++++++- src/components/PaginatedTable/types.ts | 1 + 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/components/PaginatedTable/PaginatedTable.scss b/src/components/PaginatedTable/PaginatedTable.scss index 592c715a4..7a5edf80e 100644 --- a/src/components/PaginatedTable/PaginatedTable.scss +++ b/src/components/PaginatedTable/PaginatedTable.scss @@ -79,50 +79,69 @@ display: flex; flex-direction: row; align-items: center; + gap: var(--g-spacing-2); width: 100%; max-width: 100%; padding: var(--paginated-table-cell-vertical-padding) var(--paginated-table-cell-horizontal-padding); + font-weight: bold; + cursor: default; &_align { &_left { justify-content: left; + + text-align: left; } &_center { justify-content: center; + + text-align: center; } &_right { justify-content: right; + + text-align: right; + + #{$block}__head-cell-content-container { + flex-direction: row-reverse; + } } } } - &__head-cell { - gap: 8px; - - font-weight: bold; - cursor: default; - - &_sortable { - cursor: pointer; + &__head-cell_sortable { + cursor: pointer; - &#{$block}__head-cell_align_right { - flex-direction: row-reverse; - } + &#{$block}__head-cell_align_right { + flex-direction: row-reverse; } } + &__head-cell-note { + display: flex; + } + // Separate head cell content class for correct text ellipsis overflow &__head-cell-content { overflow: hidden; - width: min-content; - white-space: nowrap; text-overflow: ellipsis; } + &__head-cell-content-container { + display: inline-flex; + overflow: hidden; + gap: var(--g-spacing-1); + + .g-help-mark__button { + display: inline-flex; + align-items: center; + } + } + &__row-cell { display: table-cell; overflow-x: hidden; diff --git a/src/components/PaginatedTable/TableHead.tsx b/src/components/PaginatedTable/TableHead.tsx index da07fab6a..0c51cf888 100644 --- a/src/components/PaginatedTable/TableHead.tsx +++ b/src/components/PaginatedTable/TableHead.tsx @@ -1,5 +1,8 @@ import React from 'react'; +import type {HelpMarkProps} from '@gravity-ui/uikit'; +import {HelpMark} from '@gravity-ui/uikit'; + import {ResizeHandler} from './ResizeHandler'; import { ASCENDING, @@ -9,7 +12,14 @@ import { DESCENDING, } from './constants'; import {b} from './shared'; -import type {Column, HandleTableColumnsResize, OnSort, SortOrderType, SortParams} from './types'; +import type { + AlignType, + Column, + HandleTableColumnsResize, + OnSort, + SortOrderType, + SortParams, +} from './types'; // Icon similar to original DataTable icons to keep the same tables across diferent pages and tabs const SortIcon = ({order}: {order?: SortOrderType}) => { @@ -43,6 +53,12 @@ const ColumnSortIcon = ({sortOrder, sortable, defaultSortOrder}: ColumnSortIconP } }; +const columnAlignToHelpMarkPlacement: Record = { + left: 'right', + right: 'left', + center: 'right', +}; + interface TableHeadCellProps { column: Column; resizeable?: boolean; @@ -115,7 +131,17 @@ export const TableHeadCell = ({ } }} > -
{content}
+
+
{content}
+ {column.note && ( + + {column.note} + + )} +
void export interface Column { name: string; + note?: string; header?: React.ReactNode; className?: string; sortable?: boolean; From 6be9be4f0e1e8925ac3421c78db3a9bc58217508 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 11 Apr 2025 17:33:34 +0300 Subject: [PATCH 06/21] feat(TopicData): add tab for topic data --- src/components/EmptyState/EmptyState.scss | 3 + .../Storage/EmptyFilter/EmptyFilter.tsx | 4 +- .../Diagnostics/Consumers/Consumers.tsx | 2 +- .../Tenant/Diagnostics/Diagnostics.tsx | 33 +-- .../Tenant/Diagnostics/DiagnosticsPages.ts | 59 +++++- .../Overview/TopicStats/TopicStats.tsx | 2 +- .../Diagnostics/TopicData/FullValue.tsx | 36 ++++ .../Diagnostics/TopicData/TopicData.scss | 15 ++ .../Diagnostics/TopicData/TopicData.tsx | 165 +++++++++++++++ .../TopicDataControls/TopicDataControls.tsx | 172 ++++++++++++++++ .../TopicData/columns/Columns.scss | 18 ++ .../Diagnostics/TopicData/columns/columns.tsx | 193 ++++++++++++++++++ .../Tenant/Diagnostics/TopicData/getData.ts | 100 +++++++++ .../Tenant/Diagnostics/TopicData/i18n/en.json | 21 ++ .../Diagnostics/TopicData/i18n/index.ts | 7 + .../Diagnostics/TopicData/utils/constants.ts | 55 +++++ .../Diagnostics/TopicData/utils/types.ts | 28 +++ src/containers/Tenant/TenantPages.tsx | 2 +- src/containers/Tenant/utils/schema.ts | 2 + src/services/api/viewer.ts | 9 +- src/store/reducers/capabilities/hooks.ts | 4 + src/store/reducers/index.ts | 2 + src/store/reducers/tenant/constants.ts | 1 + src/store/reducers/{ => topic}/topic.ts | 42 +++- src/store/reducers/topic/types.ts | 10 + src/store/state-url-mapping.ts | 39 ++++ src/types/api/capabilities.ts | 3 +- src/types/api/topic.ts | 128 ++++++++++++ src/utils/dataFormatters/dataFormatters.ts | 2 +- 29 files changed, 1122 insertions(+), 35 deletions(-) create mode 100644 src/containers/Tenant/Diagnostics/TopicData/FullValue.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicData.scss create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss create mode 100644 src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopicData/getData.ts create mode 100644 src/containers/Tenant/Diagnostics/TopicData/i18n/en.json create mode 100644 src/containers/Tenant/Diagnostics/TopicData/i18n/index.ts create mode 100644 src/containers/Tenant/Diagnostics/TopicData/utils/constants.ts create mode 100644 src/containers/Tenant/Diagnostics/TopicData/utils/types.ts rename src/store/reducers/{ => topic}/topic.ts (67%) create mode 100644 src/store/reducers/topic/types.ts diff --git a/src/components/EmptyState/EmptyState.scss b/src/components/EmptyState/EmptyState.scss index 2ed7b57c6..51a29c7fd 100644 --- a/src/components/EmptyState/EmptyState.scss +++ b/src/components/EmptyState/EmptyState.scss @@ -28,6 +28,9 @@ margin: 0 auto; } + &_position_left { + margin: unset; + } } &__image { diff --git a/src/containers/Storage/EmptyFilter/EmptyFilter.tsx b/src/containers/Storage/EmptyFilter/EmptyFilter.tsx index ce2d04a07..223d81a2d 100644 --- a/src/containers/Storage/EmptyFilter/EmptyFilter.tsx +++ b/src/containers/Storage/EmptyFilter/EmptyFilter.tsx @@ -10,6 +10,7 @@ interface EmptyFilterProps { message?: string; showAll?: string; onShowAll?: VoidFunction; + image?: React.ReactNode; } export const EmptyFilter = ({ @@ -17,9 +18,10 @@ export const EmptyFilter = ({ message = i18n('default_message'), showAll = i18n('default_button_label'), onShowAll, + image = , }: EmptyFilterProps) => ( } + image={image} position="left" title={title} description={message} diff --git a/src/containers/Tenant/Diagnostics/Consumers/Consumers.tsx b/src/containers/Tenant/Diagnostics/Consumers/Consumers.tsx index 1c0979c8d..e454c9a16 100644 --- a/src/containers/Tenant/Diagnostics/Consumers/Consumers.tsx +++ b/src/containers/Tenant/Diagnostics/Consumers/Consumers.tsx @@ -10,7 +10,7 @@ import { selectPreparedConsumersData, selectPreparedTopicStats, topicApi, -} from '../../../../store/reducers/topic'; +} from '../../../../store/reducers/topic/topic'; import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants'; diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 997b42471..8ebb78a8d 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -3,11 +3,13 @@ import React from 'react'; import {Tabs} from '@gravity-ui/uikit'; import {Helmet} from 'react-helmet-async'; import {Link} from 'react-router-dom'; -import {StringParam, useQueryParams} from 'use-query-params'; import {AutoRefreshControl} from '../../../components/AutoRefreshControl/AutoRefreshControl'; import {DrawerContextProvider} from '../../../components/Drawer/DrawerContext'; -import {useFeatureFlagsAvailable} from '../../../store/reducers/capabilities/hooks'; +import { + useFeatureFlagsAvailable, + useTopicDataAvailable, +} from '../../../store/reducers/capabilities/hooks'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../store/reducers/tenant/constants'; import {setDiagnosticsTab} from '../../../store/reducers/tenant/tenant'; import type {AdditionalNodesProps, AdditionalTenantsProps} from '../../../types/additionalProps'; @@ -20,19 +22,19 @@ import {Operations} from '../../Operations'; import {PaginatedStorage} from '../../Storage/PaginatedStorage'; import {Tablets} from '../../Tablets/Tablets'; import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer'; -import {TenantTabsGroups, getTenantPath} from '../TenantPages'; import {isDatabaseEntityType} from '../utils/schema'; import {Configs} from './Configs/Configs'; import {Consumers} from './Consumers'; import Describe from './Describe/Describe'; import DetailedOverview from './DetailedOverview/DetailedOverview'; -import {getDataBasePages, getPagesByType} from './DiagnosticsPages'; +import {getPagesByType, useDiagnosticsPageLinkGetter} from './DiagnosticsPages'; import {HotKeys} from './HotKeys/HotKeys'; import {NetworkWrapper} from './Network/NetworkWrapper'; import {Partitions} from './Partitions/Partitions'; import {TopQueries} from './TopQueries'; import {TopShards} from './TopShards'; +import {TopicData} from './TopicData/TopicData'; import './Diagnostics.scss'; @@ -54,18 +56,17 @@ function Diagnostics(props: DiagnosticsProps) { (state) => state.tenant, ); - const [queryParams] = useQueryParams({ - database: StringParam, - schema: StringParam, - backend: StringParam, - clusterName: StringParam, - }); + const getDiagnosticsPageLink = useDiagnosticsPageLinkGetter(); const tenantName = isDatabaseEntityType(props.type) ? props.path : props.tenantName; - const isDatabase = isDatabaseEntityType(props.type) || props.path === props.tenantName; const hasFeatureFlags = useFeatureFlagsAvailable(); - const pages = isDatabase ? getDataBasePages({hasFeatureFlags}) : getPagesByType(props.type); + const hasTopicData = useTopicDataAvailable(); + const pages = getPagesByType(props.type, { + hasFeatureFlags, + hasTopicData, + isTopLevel: props.path === props.tenantName, + }); let activeTab = pages.find((el) => el.id === diagnosticsTab); if (!activeTab) { activeTab = pages[0]; @@ -142,6 +143,9 @@ function Diagnostics(props: DiagnosticsProps) { case TENANT_DIAGNOSTICS_TABS_IDS.partitions: { return ; } + case TENANT_DIAGNOSTICS_TABS_IDS.topicData: { + return ; + } case TENANT_DIAGNOSTICS_TABS_IDS.configs: { return ; } @@ -162,10 +166,7 @@ function Diagnostics(props: DiagnosticsProps) { items={pages} activeTab={activeTab?.id} wrapTo={({id}, node) => { - const path = getTenantPath({ - ...queryParams, - [TenantTabsGroups.diagnosticsTab]: id, - }); + const path = getDiagnosticsPageLink(id); return ( diff --git a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts index 9ea138ce8..e58181d77 100644 --- a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts +++ b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts @@ -1,6 +1,13 @@ +import React from 'react'; + +import {StringParam, useQueryParams} from 'use-query-params'; + import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../store/reducers/tenant/constants'; import type {TenantDiagnosticsTab} from '../../../store/reducers/tenant/types'; import {EPathType} from '../../../types/api/schema'; +import type {TenantQuery} from '../TenantPages'; +import {TenantTabsGroups, getTenantPath} from '../TenantPages'; +import {isDatabaseEntityType, isTopicEntityType} from '../utils/schema'; type Page = { id: TenantDiagnosticsTab; @@ -69,6 +76,10 @@ const partitions = { id: TENANT_DIAGNOSTICS_TABS_IDS.partitions, title: 'Partitions', }; +const topicData = { + id: TENANT_DIAGNOSTICS_TABS_IDS.topicData, + title: 'Data', +}; const configs = { id: TENANT_DIAGNOSTICS_TABS_IDS.configs, @@ -103,7 +114,7 @@ const COLUMN_TABLE_PAGES = [overview, schema, topShards, nodes, tablets, describ const DIR_PAGES = [overview, topShards, nodes, describe]; const CDC_STREAM_PAGES = [overview, consumers, partitions, nodes, tablets, describe]; -const TOPIC_PAGES = [overview, consumers, partitions, nodes, tablets, describe]; +const TOPIC_PAGES = [overview, consumers, partitions, topicData, nodes, tablets, describe]; const EXTERNAL_DATA_SOURCE_PAGES = [overview, describe]; const EXTERNAL_TABLE_PAGES = [overview, schema, describe]; @@ -139,10 +150,44 @@ const pathTypeToPages: Record = { [EPathType.EPathTypeResourcePool]: DIR_PAGES, }; -export const getPagesByType = (type?: EPathType) => (type && pathTypeToPages[type]) || DIR_PAGES; - -export const getDataBasePages = ({hasFeatureFlags}: {hasFeatureFlags?: boolean}) => { - return hasFeatureFlags - ? DATABASE_PAGES - : DATABASE_PAGES.filter((item) => item.id !== TENANT_DIAGNOSTICS_TABS_IDS.configs); +export const getPagesByType = ( + type?: EPathType, + options?: {hasFeatureFlags?: boolean; hasTopicData?: boolean; isTopLevel?: boolean}, +) => { + if (!type || !pathTypeToPages[type]) { + return DIR_PAGES; + } + let pages = pathTypeToPages[type]; + if (isTopicEntityType(type) && !options?.hasTopicData) { + return pages?.filter((item) => item.id !== TENANT_DIAGNOSTICS_TABS_IDS.topicData); + } + if (isDatabaseEntityType(type) || options?.isTopLevel) { + pages = DATABASE_PAGES; + if (!options?.hasFeatureFlags) { + return pages.filter((item) => item.id !== TENANT_DIAGNOSTICS_TABS_IDS.configs); + } + } + return pages; +}; + +export const useDiagnosticsPageLinkGetter = () => { + const [queryParams] = useQueryParams({ + database: StringParam, + schema: StringParam, + backend: StringParam, + clusterName: StringParam, + }); + + const getLink = React.useCallback( + (tab: string, params?: TenantQuery) => { + return getTenantPath({ + ...queryParams, + [TenantTabsGroups.diagnosticsTab]: tab, + ...params, + }); + }, + [queryParams], + ); + + return getLink; }; diff --git a/src/containers/Tenant/Diagnostics/Overview/TopicStats/TopicStats.tsx b/src/containers/Tenant/Diagnostics/Overview/TopicStats/TopicStats.tsx index 2eb7d3451..14ecb5cf9 100644 --- a/src/containers/Tenant/Diagnostics/Overview/TopicStats/TopicStats.tsx +++ b/src/containers/Tenant/Diagnostics/Overview/TopicStats/TopicStats.tsx @@ -7,7 +7,7 @@ import {LabelWithPopover} from '../../../../../components/LabelWithPopover'; import {LagPopoverContent} from '../../../../../components/LagPopoverContent'; import {Loader} from '../../../../../components/Loader'; import {SpeedMultiMeter} from '../../../../../components/SpeedMultiMeter'; -import {selectPreparedTopicStats, topicApi} from '../../../../../store/reducers/topic'; +import {selectPreparedTopicStats, topicApi} from '../../../../../store/reducers/topic/topic'; import type {IPreparedTopicStats} from '../../../../../types/store/topic'; import {cn} from '../../../../../utils/cn'; import {formatBps, formatBytes} from '../../../../../utils/dataFormatters/dataFormatters'; diff --git a/src/containers/Tenant/Diagnostics/TopicData/FullValue.tsx b/src/containers/Tenant/Diagnostics/TopicData/FullValue.tsx new file mode 100644 index 000000000..315df7040 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopicData/FullValue.tsx @@ -0,0 +1,36 @@ +import {DefinitionList, Dialog} from '@gravity-ui/uikit'; + +import type {TopicMessageMetadataItem} from '../../../../types/api/topic'; + +import i18n from './i18n'; +import {b} from './utils/constants'; + +interface FullValueProps { + onClose: () => void; + value?: string | TopicMessageMetadataItem[]; +} + +export function FullValue({onClose, value}: FullValueProps) { + const renderContent = () => { + if (typeof value === 'string') { + return value; + } + return ( + + {value?.map((item, index) => ( + + {item.Value} + + ))} + + ); + }; + + return ( + + + + {renderContent()} + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss b/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss new file mode 100644 index 000000000..944b7f9f9 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss @@ -0,0 +1,15 @@ +.ydb-diagnostics-topic-data { + &__partition-select { + min-width: 150px; + } + + &__full-value { + max-width: 70vw; + max-height: 80vh; + + word-break: break-all; + } + &__date-picker { + min-width: 265px; + } +} diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx new file mode 100644 index 000000000..271dfeed8 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -0,0 +1,165 @@ +import React from 'react'; + +import {NoSearchResults} from '@gravity-ui/illustrations'; + +import type {RenderControls} from '../../../../components/PaginatedTable'; +import {ResizeablePaginatedTable} from '../../../../components/PaginatedTable'; +import {partitionsApi} from '../../../../store/reducers/partitions/partitions'; +import {setSelectedOffset, setStartTimestamp} from '../../../../store/reducers/topic/topic'; +import type {TopicMessageMetadataItem} from '../../../../types/api/topic'; +import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; +import {renderPaginatedTableErrorMessage} from '../../../../utils/renderPaginatedTableErrorMessage'; +import {convertToNumber} from '../../../../utils/utils'; +import {EmptyFilter} from '../../../Storage/EmptyFilter/EmptyFilter'; + +import {FullValue} from './FullValue'; +import {TopicDataControls} from './TopicDataControls/TopicDataControls'; +import { + DEFAULT_TOPIC_DATA_COLUMNS, + REQUIRED_TOPIC_DATA_COLUMNS, + getAllColumns, +} from './columns/columns'; +import {generateTopicDataGetter} from './getData'; +import i18n from './i18n'; +import { + TOPIC_DATA_COLUMNS_TITLES, + TOPIC_DATA_COLUMNS_WIDTH_LS_KEY, + TOPIC_DATA_SELECTED_COLUMNS_LS_KEY, +} from './utils/constants'; + +import './TopicData.scss'; + +interface TopicDataProps { + path: string; + database: string; + parentRef: React.RefObject; +} + +export function TopicData({parentRef, path, database}: TopicDataProps) { + const dispatch = useTypedDispatch(); + const [autoRefreshInterval] = useAutoRefreshInterval(); + const [startOffset, setStartOffset] = React.useState(undefined); + const [endOffset, setEndOffset] = React.useState(undefined); + const [fullValue, setFullValue] = React.useState< + string | TopicMessageMetadataItem[] | undefined + >(undefined); + const [controlsKey, setControlsKey] = React.useState(0); + + const {selectedPartition, selectedOffset, startTimestamp, topicDataFilter} = useTypedSelector( + (state) => state.topic, + ); + + const { + data: partitions, + isLoading: partitionsLoading, + error: partitionsError, + } = partitionsApi.useGetPartitionsQuery( + {path, database}, + {pollingInterval: autoRefreshInterval}, + ); + + React.useEffect(() => { + const selectedPartitionData = partitions?.find( + ({partitionId}) => partitionId === selectedPartition, + ); + + if (selectedPartitionData) { + setStartOffset(convertToNumber(selectedPartitionData.startOffset)); + setEndOffset(convertToNumber(selectedPartitionData.endOffset)); + } + }, [selectedPartition, partitions]); + + const numericSelectedOffset = React.useMemo(() => { + return convertToNumber(selectedOffset); + }, [selectedOffset]); + const numericStartTimestamp = React.useMemo(() => { + return convertToNumber(startTimestamp); + }, [startTimestamp]); + + const tableFilters = React.useMemo(() => { + return { + path, + database, + partition: selectedPartition ?? '', + selectedOffset: numericSelectedOffset, + startTimestamp: numericStartTimestamp, + topicDataFilter, + }; + }, [ + path, + database, + selectedPartition, + numericSelectedOffset, + numericStartTimestamp, + topicDataFilter, + ]); + + const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( + getAllColumns(setFullValue), + TOPIC_DATA_SELECTED_COLUMNS_LS_KEY, + TOPIC_DATA_COLUMNS_TITLES, + DEFAULT_TOPIC_DATA_COLUMNS, + REQUIRED_TOPIC_DATA_COLUMNS, + ); + + const renderControls: RenderControls = () => { + return ( + + ); + }; + + const renderEmptyDataMessage = () => { + const hasFilters = selectedOffset || startTimestamp; + + const resetFilter = () => { + dispatch(setSelectedOffset(undefined)); + dispatch(setStartTimestamp(undefined)); + setControlsKey((prev) => prev + 1); + }; + + return ( + } + /> + ); + }; + + return ( + + setFullValue(undefined)} /> + + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx new file mode 100644 index 000000000..a0b66897c --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx @@ -0,0 +1,172 @@ +import React from 'react'; + +import type {Value} from '@gravity-ui/date-components'; +import {RelativeDatePicker} from '@gravity-ui/date-components'; +import {dateTimeParse} from '@gravity-ui/date-utils'; +import type {TableColumnSetupItem} from '@gravity-ui/uikit'; +import {RadioButton, Select, TableColumnSetup} from '@gravity-ui/uikit'; + +import {DebouncedInput} from '../../../../../components/DebouncedInput/DebouncedInput'; +import {EntitiesCount} from '../../../../../components/EntitiesCount'; +import type {PreparedPartitionData} from '../../../../../store/reducers/partitions/types'; +import { + setSelectedOffset, + setSelectedPartition, + setStartTimestamp, + setTopicDataFilter, +} from '../../../../../store/reducers/topic/topic'; +import {TopicDataFilterValues} from '../../../../../store/reducers/topic/types'; +import type {TopicDataFilterValue} from '../../../../../store/reducers/topic/types'; +import {formatNumber} from '../../../../../utils/dataFormatters/dataFormatters'; +import {useTypedDispatch, useTypedSelector} from '../../../../../utils/hooks'; +import {prepareErrorMessage} from '../../../../../utils/prepareErrorMessage'; +import {convertToNumber} from '../../../../../utils/utils'; +import i18n from '../i18n'; +import {b} from '../utils/constants'; + +interface TopicDataControlsProps { + columnsToSelect: TableColumnSetupItem[]; + handleSelectedColumnsUpdate: (updated: TableColumnSetupItem[]) => void; + + partitions?: PreparedPartitionData[]; + partitionsLoading: boolean; + partitionsError: unknown; + + initialOffset?: number; + endOffset?: number; +} + +export function TopicDataControls({ + columnsToSelect, + handleSelectedColumnsUpdate, + + initialOffset = 0, + endOffset = 0, + partitions, + partitionsLoading, + partitionsError, +}: TopicDataControlsProps) { + const dispatch = useTypedDispatch(); + + const {selectedPartition} = useTypedSelector((state) => state.topic); + + const partitionsToSelect = partitions?.map(({partitionId}) => ({ + content: partitionId, + value: partitionId, + })); + + const handleSelectedPartitionChange = React.useCallback( + (value: string[]) => { + dispatch(setSelectedPartition(value[0])); + }, + [dispatch], + ); + + React.useEffect(() => { + if (partitions && partitions.length && selectedPartition === undefined) { + handleSelectedPartitionChange([partitions[0].partitionId]); + } + }, [partitions, selectedPartition, handleSelectedPartitionChange]); + + return ( + + + + + + + + + + ); } @@ -115,9 +133,14 @@ function TopicDataStartControls() { const onFilterChange = React.useCallback( (value: TopicDataFilterValue) => { + if (value === 'TIMESTAMP') { + handleSelectedOffsetChange(undefined); + } else { + handleStartTimestampChange(undefined); + } handleTopicDataFilterChange(value); }, - [handleTopicDataFilterChange], + [handleTopicDataFilterChange, handleSelectedOffsetChange, handleStartTimestampChange], ); const onStartOffsetChange = React.useCallback( (value: string) => { diff --git a/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts b/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts deleted file mode 100644 index 1ede912da..000000000 --- a/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts +++ /dev/null @@ -1,274 +0,0 @@ -import {generateTopicDataGetter} from '../getData'; - -// Mock the window.api.viewer.getTopicData function -const mockGetTopicData = jest.fn(); -Object.defineProperty(window, 'api', { - value: { - viewer: { - getTopicData: mockGetTopicData, - }, - }, - writable: true, -}); - -describe('generateTopicDataGetter', () => { - const setStartOffset = jest.fn(); - const setEndOffset = jest.fn(); - const initialOffset = 5; - - beforeEach(() => { - jest.clearAllMocks(); - mockGetTopicData.mockReset(); - }); - - describe('getTopicData', () => { - test('should return empty data if filters are not provided', async () => { - const getTopicData = generateTopicDataGetter({ - setStartOffset, - setEndOffset, - initialOffset, - }); - - const result = await getTopicData({ - limit: 10, - offset: 0, - columnsIds: [], - }); - - expect(result).toEqual({data: [], total: 0, found: 0}); - expect(mockGetTopicData).not.toHaveBeenCalled(); - }); - - test('should return empty data if partition is nil', async () => { - const getTopicData = generateTopicDataGetter({ - setStartOffset, - setEndOffset, - initialOffset, - }); - - const result = await getTopicData({ - limit: 10, - offset: 0, - filters: { - database: 'db', - path: '/path', - }, - columnsIds: [], - }); - - expect(result).toEqual({data: [], total: 0, found: 0}); - expect(mockGetTopicData).not.toHaveBeenCalled(); - }); - }); - - describe('query parameters building', () => { - test('should build query params with timestamp for TIMESTAMP filter', async () => { - mockGetTopicData.mockResolvedValueOnce({ - StartOffset: '100', - EndOffset: '200', - Messages: [], - }); - - const getTopicData = generateTopicDataGetter({ - setStartOffset, - setEndOffset, - initialOffset, - }); - - await getTopicData({ - limit: 10, - offset: 0, - filters: { - partition: '1', - database: 'db', - path: '/path', - topicDataFilter: 'TIMESTAMP', - startTimestamp: 1234567890, - }, - columnsIds: [], - }); - - expect(mockGetTopicData).toHaveBeenCalledWith({ - partition: '1', - database: 'db', - path: '/path', - limit: 10, - read_timestamp: 1234567890, - }); - }); - - test('should build query params with offset for OFFSET filter', async () => { - mockGetTopicData.mockResolvedValueOnce({ - StartOffset: '100', - EndOffset: '200', - Messages: [], - }); - - const getTopicData = generateTopicDataGetter({ - setStartOffset, - setEndOffset, - initialOffset, - }); - - await getTopicData({ - limit: 10, - offset: 5, - filters: { - partition: '1', - database: 'db', - path: '/path', - topicDataFilter: 'OFFSET', - selectedOffset: 20, - }, - columnsIds: [], - }); - - // Max of selectedOffset (20) and initialOffset (5) is 20 - // normalizedOffset = fromOffset (20) + tableOffset (5) + lostOffsets (0) = 25 - expect(mockGetTopicData).toHaveBeenCalledWith({ - partition: '1', - database: 'db', - path: '/path', - limit: 10, - offset: 25, - }); - }); - }); - - describe('response processing', () => { - test('should process response and update offsets', async () => { - mockGetTopicData.mockResolvedValueOnce({ - StartOffset: '100', - EndOffset: '200', - Messages: [{Offset: '150'}, {Offset: '160'}], - }); - - const getTopicData = generateTopicDataGetter({ - setStartOffset, - setEndOffset, - initialOffset, - }); - - const result = await getTopicData({ - limit: 10, - offset: 0, - filters: { - partition: '1', - database: 'db', - path: '/path', - topicDataFilter: 'OFFSET', - }, - columnsIds: [], - }); - - expect(setStartOffset).toHaveBeenCalledWith(100); - expect(setEndOffset).toHaveBeenCalledWith(200); - expect(result.data).toEqual([{Offset: '150'}, {Offset: '160'}]); - expect(result.total).toBe(195); // 200 - 5 (initialOffset) - expect(result.found).toBe(195); - }); - - test.only('should set fromOffset from first message for TIMESTAMP filter', async () => { - mockGetTopicData.mockResolvedValueOnce({ - StartOffset: '100', - EndOffset: '200', - Messages: [{Offset: '150'}, {Offset: '160'}], - }); - - const getTopicData = generateTopicDataGetter({ - setStartOffset, - setEndOffset, - initialOffset, - }); - - await getTopicData({ - limit: 10, - offset: 0, - filters: { - partition: '1', - database: 'db', - path: '/path', - topicDataFilter: 'TIMESTAMP', - startTimestamp: 1234567890, - }, - columnsIds: [], - }); - - mockGetTopicData.mockResolvedValueOnce({ - StartOffset: '100', - EndOffset: '200', - Messages: [], - }); - - await getTopicData({ - limit: 10, - offset: 5, - filters: { - partition: '1', - database: 'db', - path: '/path', - topicDataFilter: 'TIMESTAMP', - }, - columnsIds: [], - }); - - // First call uses read_timestamp - expect(mockGetTopicData.mock.calls[0][0]).toHaveProperty('read_timestamp', 1234567890); - // Second call uses offset calculated from first message (150) + tableOffset (5) - // The actual value is 154 due to how lostOffsets is calculated in the implementation - expect(mockGetTopicData.mock.calls[1][0]).toHaveProperty('offset', 154); - }); - - test('should update lostOffsets correctly', async () => { - mockGetTopicData.mockResolvedValueOnce({ - StartOffset: '100', - EndOffset: '200', - Messages: [{Offset: '150'}, {Offset: '160'}], - }); - - const getTopicData = generateTopicDataGetter({ - setStartOffset, - setEndOffset, - initialOffset: 0, - }); - - await getTopicData({ - limit: 20, - offset: 0, - filters: { - partition: '1', - database: 'db', - path: '/path', - topicDataFilter: 'OFFSET', - selectedOffset: 0, - }, - columnsIds: [], - }); - - // lostOffsets should be updated: 0 + 0 + 20 - 160 - 1 = -141 - - // Second call - mockGetTopicData.mockResolvedValueOnce({ - StartOffset: '200', - EndOffset: '300', - Messages: [{Offset: '250'}, {Offset: '260'}], - }); - - await getTopicData({ - limit: 20, - offset: 10, - filters: { - partition: '1', - database: 'db', - path: '/path', - topicDataFilter: 'OFFSET', - }, - columnsIds: [], - }); - - // First call uses offset 0 - expect(mockGetTopicData.mock.calls[0][0]).toHaveProperty('offset', 0); - expect(mockGetTopicData.mock.calls[1][0]).toHaveProperty('offset', -131); - }); - }); -}); diff --git a/src/containers/Tenant/Diagnostics/TopicData/getData.ts b/src/containers/Tenant/Diagnostics/TopicData/getData.ts index 1b3775b94..44bfbb701 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/getData.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/getData.ts @@ -6,40 +6,14 @@ import {safeParseNumber} from '../../../../utils/utils'; import type {TopicDataFilters} from './utils/types'; -const getInitialOffset = ({ - topicDataFilter, - selectedOffset = 0, - initialOffset = 0, - fromOffset, -}: Pick & { - initialOffset?: number; - fromOffset?: number; -}) => { - if (!isNil(fromOffset)) { - return fromOffset; - } - if (topicDataFilter === 'TIMESTAMP') { - return undefined; - } - return Math.max(selectedOffset, initialOffset); -}; +const emptyData = {data: [], total: 0, found: 0}; interface GetTopicDataProps { setStartOffset: (offset: number) => void; setEndOffset: (offset: number) => void; - initialOffset?: number; } -const emptyData = {data: [], total: 0, found: 0}; - -export const generateTopicDataGetter = ({ - initialOffset = 0, - setStartOffset, - setEndOffset, -}: GetTopicDataProps) => { - let lostOffsets = 0; - let fromOffset: number | undefined; - +export const generateTopicDataGetter = ({setStartOffset, setEndOffset}: GetTopicDataProps) => { const getTopicData: FetchData = async ({ limit, offset: tableOffset, @@ -49,23 +23,14 @@ export const generateTopicDataGetter = ({ return emptyData; } - const {partition, selectedOffset, startTimestamp, topicDataFilter, ...rest} = filters; + const {partition, isEmpty, ...rest} = filters; - if (isNil(partition)) { + if (isNil(partition) || isEmpty) { return emptyData; } const queryParams: TopicDataRequest = {...rest, partition, limit}; - - fromOffset = getInitialOffset({topicDataFilter, selectedOffset, initialOffset, fromOffset}); - - if (topicDataFilter === 'TIMESTAMP' && isNil(fromOffset)) { - // get data from timestamp only the very first time. Next fetch we will already know offset - queryParams.read_timestamp = startTimestamp; - } else { - const normalizedOffset = (fromOffset ?? 0) + tableOffset + lostOffsets; - queryParams.offset = normalizedOffset; - } + queryParams.offset = tableOffset; const response = await window.api.viewer.getTopicData(queryParams); @@ -73,22 +38,12 @@ export const generateTopicDataGetter = ({ const start = safeParseNumber(StartOffset); const end = safeParseNumber(EndOffset); + //need to update start and end offsets every time data is fetched to show fresh data in parent component setStartOffset(start); setEndOffset(end); - if (isNil(fromOffset)) { - fromOffset = Messages.length ? safeParseNumber(Messages[0].Offset) : end; - } - - const normalizedOffset = fromOffset + tableOffset + lostOffsets; - const lastMessageOffset = Messages.length - ? safeParseNumber(Messages[Messages.length - 1].Offset) - : 0; - - const quantity = end - fromOffset - lostOffsets; - - lostOffsets += normalizedOffset + limit - lastMessageOffset - 1; + const quantity = end - start; return { data: Messages, @@ -96,5 +51,6 @@ export const generateTopicDataGetter = ({ found: quantity, }; }; + return getTopicData; }; diff --git a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json index 6789fe2b6..5ce6deeb1 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json @@ -19,5 +19,7 @@ "description_nothing-found": "Make the filter less strict or start over", "action_show-all": "Reset filter", "label_by-offset": "By offset", - "label_by-timestamp": "By timestamp" + "label_by-timestamp": "By timestamp", + "action_scroll-down": "Scroll to the end", + "action_scroll-up": "Scroll to the start" } diff --git a/src/containers/Tenant/Diagnostics/TopicData/utils/types.ts b/src/containers/Tenant/Diagnostics/TopicData/utils/types.ts index e6f95f45c..436696a54 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/utils/types.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/utils/types.ts @@ -22,11 +22,9 @@ export type TopicDataColumnId = ValueOf; export interface TopicDataFilters { partition?: string; - topicDataFilter?: TopicDataFilterValue; - startTimestamp?: number; - selectedOffset?: number; database: string; path: string; + isEmpty: boolean; } export const TopicDataFilterValues = { diff --git a/src/store/reducers/topic.ts b/src/store/reducers/topic.ts index 48953c096..22f5768db 100644 --- a/src/store/reducers/topic.ts +++ b/src/store/reducers/topic.ts @@ -1,6 +1,7 @@ /* eslint-disable camelcase */ import {createSelector} from '@reduxjs/toolkit'; +import type {TopicDataRequest} from '../../types/api/topic'; import {convertBytesObjectToSpeed} from '../../utils/bytesParsers'; import {parseLag, parseTimestampToIdleTime} from '../../utils/timeParsers'; import type {RootState} from '../defaultStore'; @@ -24,6 +25,17 @@ export const topicApi = api.injectEndpoints({ }, providesTags: ['All'], }), + getTopicData: build.query({ + queryFn: async (params: TopicDataRequest) => { + try { + const data = await window.api.viewer.getTopicData(params); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), }), overrideExisting: 'throw', }); From e328b6444e6a89a2c6a583575341dcc6cc70127b Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 25 Apr 2025 09:49:12 +0300 Subject: [PATCH 12/21] fix: review --- .../DebouncedInput/DebouncedTextInput.tsx | 19 ++++ .../PaginatedTable/PaginatedTable.tsx | 3 - src/components/PaginatedTable/TableChunk.tsx | 4 +- .../PaginatedTable/useScrollBasedChunks.ts | 2 +- src/components/Search/Search.tsx | 2 +- .../Tenant/Diagnostics/Diagnostics.tsx | 9 +- .../Diagnostics/TopicData/TopicData.scss | 3 + .../Diagnostics/TopicData/TopicData.tsx | 107 +++++++++++------- .../TopicDataControls/TopicDataControls.tsx | 55 +++++---- .../Diagnostics/TopicData/columns/columns.tsx | 52 ++++++--- .../Tenant/Diagnostics/TopicData/getData.ts | 39 ++++++- .../Tenant/Diagnostics/TopicData/i18n/en.json | 3 +- .../TopicData/useTopicDataQueryParams.ts | 6 +- .../Diagnostics/TopicData/utils/constants.ts | 2 + src/store/reducers/topic.ts | 2 +- .../hooks/useDebouncedValue.ts} | 24 ++-- .../paginatedTable/paginatedTable.test.ts | 3 +- 17 files changed, 225 insertions(+), 110 deletions(-) create mode 100644 src/components/DebouncedInput/DebouncedTextInput.tsx rename src/{components/DebouncedInput/DebouncedInput.tsx => utils/hooks/useDebouncedValue.ts} (51%) diff --git a/src/components/DebouncedInput/DebouncedTextInput.tsx b/src/components/DebouncedInput/DebouncedTextInput.tsx new file mode 100644 index 000000000..c7fab6d4f --- /dev/null +++ b/src/components/DebouncedInput/DebouncedTextInput.tsx @@ -0,0 +1,19 @@ +import type {TextInputProps} from '@gravity-ui/uikit'; +import {TextInput} from '@gravity-ui/uikit'; + +import {useDebouncedValue} from '../../utils/hooks/useDebouncedValue'; + +interface DebouncedInputProps extends TextInputProps { + debounce?: number; +} + +export const DebouncedInput = ({ + onUpdate, + value = '', + debounce = 200, + ...rest +}: DebouncedInputProps) => { + const [currentValue, handleUpdate] = useDebouncedValue({value, onUpdate, debounce}); + + return ; +}; diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index 32e4ac587..bc9fc6ad4 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -38,7 +38,6 @@ export interface PaginatedTableProps { renderErrorMessage?: RenderErrorMessage; containerClassName?: string; onDataFetched?: (data: PaginatedTableData) => void; - startOffset?: number; } const DEFAULT_PAGINATION_LIMIT = 20; @@ -60,7 +59,6 @@ export const PaginatedTable = ({ renderEmptyDataMessage, containerClassName, onDataFetched, - startOffset = 0, }: PaginatedTableProps) => { const initialTotal = initialEntitiesCount || 0; const initialFound = initialEntitiesCount || 1; @@ -136,7 +134,6 @@ export const PaginatedTable = ({ renderEmptyDataMessage={renderEmptyDataMessage} onDataFetched={handleDataFetched} isActive={isActive} - startOffset={startOffset} /> )); }; diff --git a/src/components/PaginatedTable/TableChunk.tsx b/src/components/PaginatedTable/TableChunk.tsx index 67d3f1f09..df01ee4d4 100644 --- a/src/components/PaginatedTable/TableChunk.tsx +++ b/src/components/PaginatedTable/TableChunk.tsx @@ -31,7 +31,6 @@ interface TableChunkProps { sortParams?: SortParams; isActive: boolean; tableName: string; - startOffset: number; fetchData: FetchData; getRowClassName?: GetRowClassName; @@ -55,7 +54,6 @@ export const TableChunk = typedMemo(function TableChunk({ renderErrorMessage, renderEmptyDataMessage, onDataFetched, - startOffset, isActive, }: TableChunkProps) { const [isTimeoutActive, setIsTimeoutActive] = React.useState(true); @@ -64,7 +62,7 @@ export const TableChunk = typedMemo(function TableChunk({ const columnsIds = columns.map((column) => column.name); const queryParams = { - offset: startOffset + id * chunkSize, + offset: id * chunkSize, limit: chunkSize, fetchData: fetchData as FetchData, filters, diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index 02b3e0712..d78d15730 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -92,7 +92,7 @@ export const useScrollBasedChunks = ({ return React.useMemo(() => { // boolean array that represents active chunks const activeChunks = Array(chunksCount).fill(false); - for (let i = startChunk; i <= Math.min(endChunk, chunksCount); i++) { + for (let i = startChunk; i <= endChunk; i++) { activeChunks[i] = true; } return activeChunks; diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search.tsx index 8b4dc6e80..3892f94cd 100644 --- a/src/components/Search/Search.tsx +++ b/src/components/Search/Search.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {cn} from '../../utils/cn'; -import {DebouncedInput} from '../DebouncedInput/DebouncedInput'; +import {DebouncedInput} from '../DebouncedInput/DebouncedTextInput'; import './Search.scss'; diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 8ebb78a8d..18f5dbfb2 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -144,7 +144,14 @@ function Diagnostics(props: DiagnosticsProps) { return ; } case TENANT_DIAGNOSTICS_TABS_IDS.topicData: { - return ; + return ( + + ); } case TENANT_DIAGNOSTICS_TABS_IDS.configs: { return ; diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss b/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss index 944b7f9f9..42cc126e0 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.scss @@ -12,4 +12,7 @@ &__date-picker { min-width: 265px; } + &__offsets-count { + border-radius: unset; + } } diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx index 39cc824c1..06675443b 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -5,7 +5,10 @@ import {skipToken} from '@reduxjs/toolkit/query'; import {isNil} from 'lodash'; import type {RenderControls} from '../../../../components/PaginatedTable'; -import {ResizeablePaginatedTable} from '../../../../components/PaginatedTable'; +import { + DEFAULT_TABLE_ROW_HEIGHT, + ResizeablePaginatedTable, +} from '../../../../components/PaginatedTable'; import {partitionsApi} from '../../../../store/reducers/partitions/partitions'; import {topicApi} from '../../../../store/reducers/topic'; import type {TopicDataRequest, TopicMessageMetadataItem} from '../../../../types/api/topic'; @@ -28,6 +31,7 @@ import {useTopicDataQueryParams} from './useTopicDataQueryParams'; import { TOPIC_DATA_COLUMNS_TITLES, TOPIC_DATA_COLUMNS_WIDTH_LS_KEY, + TOPIC_DATA_FETCH_LIMIT, TOPIC_DATA_SELECTED_COLUMNS_LS_KEY, } from './utils/constants'; @@ -41,12 +45,19 @@ interface TopicDataProps { export function TopicData({parentRef, path, database}: TopicDataProps) { const [autoRefreshInterval] = useAutoRefreshInterval(); - const [startOffset, setStartOffset] = React.useState(0); - const [endOffset, setEndOffset] = React.useState(0); + const [startOffset, setStartOffset] = React.useState(); + const [endOffset, setEndOffset] = React.useState(); const [fullValue, setFullValue] = React.useState< string | TopicMessageMetadataItem[] | undefined >(undefined); const [controlsKey, setControlsKey] = React.useState(0); + const [emptyData, setEmptyData] = React.useState(false); + + const [baseOffset, setBaseOffset] = React.useState(0); + const [baseEndOffset, setBaseEndOffset] = React.useState(0); + + const startRef = React.useRef(); + startRef.current = startOffset; const { selectedPartition, @@ -58,6 +69,14 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { handleSelectedPartitionChange, } = useTopicDataQueryParams(); + React.useEffect(() => { + return () => { + handleSelectedPartitionChange(undefined); + handleSelectedOffsetChange(undefined); + handleStartTimestampChange(undefined); + }; + }, [handleSelectedPartitionChange, handleSelectedOffsetChange, handleStartTimestampChange]); + const queryParams = React.useMemo(() => { if (isNil(selectedPartition)) { return skipToken; @@ -66,12 +85,12 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { if (startTimestamp) { params.read_timestamp = startTimestamp; } else { - params.offset = selectedOffset ?? 0; + params.offset = safeParseNumber(selectedOffset); } return params; }, [selectedPartition, selectedOffset, startTimestamp, database, path]); - const {currentData, isFetching} = topicApi.useGetTopicDataQuery(queryParams); + const {currentData, error, isFetching} = topicApi.useGetTopicDataQuery(queryParams); const { data: partitions, @@ -86,26 +105,21 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { const selectedPartitionData = partitions?.find( ({partitionId}) => partitionId === selectedPartition, ); - if (selectedPartitionData) { - setStartOffset(safeParseNumber(selectedPartitionData.startOffset)); - setEndOffset(safeParseNumber(selectedPartitionData.endOffset)); + if (!baseOffset) { + setBaseOffset(safeParseNumber(selectedPartitionData.startOffset)); + } + if (!baseEndOffset) { + setBaseEndOffset(safeParseNumber(selectedPartitionData.endOffset)); + } } - }, [selectedPartition, partitions]); + }, [selectedPartition, partitions, baseOffset, baseEndOffset, startOffset, endOffset]); React.useEffect(() => { if (partitions && partitions.length && isNil(selectedPartition)) { handleSelectedPartitionChange(partitions[0].partitionId); - handleSelectedOffsetChange(undefined); - handleStartTimestampChange(undefined); } - }, [ - partitions, - selectedPartition, - handleSelectedPartitionChange, - handleSelectedOffsetChange, - handleStartTimestampChange, - ]); + }, [partitions, selectedPartition, handleSelectedPartitionChange]); const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( getAllColumns(setFullValue), @@ -115,27 +129,42 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { REQUIRED_TOPIC_DATA_COLUMNS, ); - const emptyData = React.useMemo(() => !currentData?.Messages?.length, [currentData]); + React.useEffect(() => { + //values should be recalculated only when data is fetched + if (isFetching || (!currentData && !error)) { + return; + } + if (currentData?.Messages?.length || (!currentData && !error)) { + setEmptyData(false); + } else if (!(currentData && currentData.Messages?.length) || error) { + setEmptyData(true); + } + if (currentData) { + setStartOffset(safeParseNumber(currentData.StartOffset)); + setEndOffset(safeParseNumber(currentData.EndOffset)); + } + }, [isFetching, currentData, error]); - const tableFilters = React.useMemo(() => { - return { + const tableFilters = React.useMemo( + () => ({ path, database, partition: selectedPartition ?? '', isEmpty: emptyData, - }; - }, [path, database, selectedPartition, emptyData]); + }), + [path, database, selectedPartition, emptyData], + ); const scrollToOffset = React.useCallback( (newOffset: number) => { - const scrollTop = (newOffset - (startOffset ?? 0)) * 41; + const scrollTop = (newOffset - (baseOffset ?? 0)) * DEFAULT_TABLE_ROW_HEIGHT; const normalizedScrollTop = Math.max(0, scrollTop); parentRef.current?.scrollTo({ top: normalizedScrollTop, behavior: 'instant', }); }, - [startOffset, parentRef], + [baseOffset, parentRef], ); React.useEffect(() => { @@ -153,20 +182,16 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { }, [currentData, isFetching, scrollToOffset]); const scrollToStartOffset = React.useCallback(() => { - parentRef.current?.scrollTo({ - top: 0, - behavior: 'smooth', - }); - }, [parentRef]); + if (startOffset) { + scrollToOffset(startOffset); + } + }, [startOffset, scrollToOffset]); const scrollToEndOffset = React.useCallback(() => { - if (parentRef.current) { - parentRef.current.scrollTo({ - top: parentRef.current.scrollHeight - parentRef.current.clientHeight, - behavior: 'smooth', - }); + if (endOffset) { + scrollToOffset(endOffset); } - }, [parentRef]); + }, [endOffset, scrollToOffset]); const renderControls: RenderControls = () => { return ( @@ -210,8 +235,8 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { }; const getTopicData = React.useMemo( - () => generateTopicDataGetter({setEndOffset, setStartOffset}), - [], + () => generateTopicDataGetter({setEndOffset, setStartOffset, baseOffset}), + [baseOffset], ); return ( @@ -222,14 +247,14 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { parentRef={parentRef} columns={columnsToShow} fetchData={getTopicData} - initialEntitiesCount={endOffset - startOffset} - limit={50} - startOffset={startOffset} + initialEntitiesCount={baseEndOffset - baseOffset} + limit={TOPIC_DATA_FETCH_LIMIT} renderControls={renderControls} renderErrorMessage={renderPaginatedTableErrorMessage} renderEmptyDataMessage={renderEmptyDataMessage} filters={tableFilters} tableName="topicData" + rowHeight={DEFAULT_TABLE_ROW_HEIGHT} /> ); diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx index c3b066cbf..4ce14c415 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx @@ -14,13 +14,13 @@ import { Select, TableColumnSetup, } from '@gravity-ui/uikit'; +import {isNil} from 'lodash'; -import {DebouncedInput} from '../../../../../components/DebouncedInput/DebouncedInput'; +import {DebouncedInput} from '../../../../../components/DebouncedInput/DebouncedTextInput'; import {EntitiesCount} from '../../../../../components/EntitiesCount'; import type {PreparedPartitionData} from '../../../../../store/reducers/partitions/types'; import {formatNumber} from '../../../../../utils/dataFormatters/dataFormatters'; import {prepareErrorMessage} from '../../../../../utils/prepareErrorMessage'; -import {safeParseNumber} from '../../../../../utils/utils'; import i18n from '../i18n'; import {useTopicDataQueryParams} from '../useTopicDataQueryParams'; import {b} from '../utils/constants'; @@ -45,8 +45,8 @@ export function TopicDataControls({ columnsToSelect, handleSelectedColumnsUpdate, - initialOffset = 0, - endOffset = 0, + initialOffset, + endOffset, partitions, partitionsLoading, partitionsError, @@ -101,22 +101,35 @@ export function TopicDataControls({ onUpdate={handleSelectedColumnsUpdate} sortable={false} /> - - - - - - - - - + {!isNil(initialOffset) && !isNil(endOffset) && ( + + + + + + + + + + )} ); } @@ -144,7 +157,7 @@ function TopicDataStartControls() { ); const onStartOffsetChange = React.useCallback( (value: string) => { - handleSelectedOffsetChange(safeParseNumber(value)); + handleSelectedOffsetChange(value); }, [handleSelectedOffsetChange], ); diff --git a/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx b/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx index ad14e898d..9a080584e 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx @@ -94,23 +94,11 @@ export function getAllColumns(setFullValue: (value: string | TopicMessageMetadat name: TOPIC_DATA_COLUMNS_IDS.MESSAGE, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.MESSAGE], align: DataTable.LEFT, - render: ({row: {Message}}) => { - if (isNil(Message)) { + render: ({row: {Message: message}}) => { + if (isNil(message)) { return EMPTY_DATA_PLACEHOLDER; } - let message = Message; - try { - message = atob(Message); - } catch {} - const longMessage = message.length > 50; - return ( - setFullValue(message) : undefined} - > - {message} - - ); + return ; }, width: 500, }, @@ -194,3 +182,37 @@ function TopicDataTimestamp({timestamp}: TopicDataTimestampProps) { function valueOrPlaceholder(value: string | undefined, placeholder = EMPTY_DATA_PLACEHOLDER) { return isNil(value) ? placeholder : value; } + +interface MessageProps { + setFullValue: (value: string) => void; + message: string; +} + +function Message({setFullValue, message}: MessageProps) { + const longMessage = message.length > 200; + + let encryptedMessage: string | undefined; + + if (!longMessage) { + try { + encryptedMessage = atob(message); + } catch {} + } + + const handleClick = () => { + try { + if (!encryptedMessage) { + encryptedMessage = atob(message); + } + setFullValue(encryptedMessage); + } catch {} + }; + return ( + + {encryptedMessage ?? i18n('action_show-message')} + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TopicData/getData.ts b/src/containers/Tenant/Diagnostics/TopicData/getData.ts index 44bfbb701..a833d234e 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/getData.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/getData.ts @@ -4,6 +4,7 @@ import type {FetchData} from '../../../../components/PaginatedTable'; import type {TopicDataRequest, TopicMessage} from '../../../../types/api/topic'; import {safeParseNumber} from '../../../../utils/utils'; +import {TOPIC_DATA_FETCH_LIMIT} from './utils/constants'; import type {TopicDataFilters} from './utils/types'; const emptyData = {data: [], total: 0, found: 0}; @@ -11,9 +12,14 @@ const emptyData = {data: [], total: 0, found: 0}; interface GetTopicDataProps { setStartOffset: (offset: number) => void; setEndOffset: (offset: number) => void; + baseOffset?: number; } -export const generateTopicDataGetter = ({setStartOffset, setEndOffset}: GetTopicDataProps) => { +export const generateTopicDataGetter = ({ + setStartOffset, + setEndOffset, + baseOffset = 0, +}: GetTopicDataProps) => { const getTopicData: FetchData = async ({ limit, offset: tableOffset, @@ -25,12 +31,14 @@ export const generateTopicDataGetter = ({setStartOffset, setEndOffset}: GetTopic const {partition, isEmpty, ...rest} = filters; - if (isNil(partition) || isEmpty) { + if (isNil(partition) || partition === '' || isEmpty) { return emptyData; } + const normalizedOffset = baseOffset + tableOffset; + const queryParams: TopicDataRequest = {...rest, partition, limit}; - queryParams.offset = tableOffset; + queryParams.offset = normalizedOffset; const response = await window.api.viewer.getTopicData(queryParams); @@ -39,14 +47,35 @@ export const generateTopicDataGetter = ({setStartOffset, setEndOffset}: GetTopic const start = safeParseNumber(StartOffset); const end = safeParseNumber(EndOffset); + const removedMessagesCount = start - normalizedOffset; + + const result = []; + for (let i = 0; i < Math.min(TOPIC_DATA_FETCH_LIMIT, removedMessagesCount); i++) { + result.push({ + Offset: `Offset ${normalizedOffset + i} removed`, + }); + } + for ( + let i = 0; + i < + Math.min( + TOPIC_DATA_FETCH_LIMIT, + TOPIC_DATA_FETCH_LIMIT - removedMessagesCount, + Messages.length, + ); + i++ + ) { + result.push(Messages[i]); + } + //need to update start and end offsets every time data is fetched to show fresh data in parent component setStartOffset(start); setEndOffset(end); - const quantity = end - start; + const quantity = end - baseOffset; return { - data: Messages, + data: result, total: quantity, found: quantity, }; diff --git a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json index 5ce6deeb1..1142326e3 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json @@ -21,5 +21,6 @@ "label_by-offset": "By offset", "label_by-timestamp": "By timestamp", "action_scroll-down": "Scroll to the end", - "action_scroll-up": "Scroll to the start" + "action_scroll-up": "Scroll to the start", + "action_show-message": "Show message content" } diff --git a/src/containers/Tenant/Diagnostics/TopicData/useTopicDataQueryParams.ts b/src/containers/Tenant/Diagnostics/TopicData/useTopicDataQueryParams.ts index 4ed9b4c23..108c9d421 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/useTopicDataQueryParams.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/useTopicDataQueryParams.ts @@ -9,20 +9,20 @@ export function useTopicDataQueryParams() { const [{selectedPartition, selectedOffset, startTimestamp, topicDataFilter}, setQueryParams] = useQueryParams({ selectedPartition: StringParam, - selectedOffset: NumberParam, + selectedOffset: StringParam, startTimestamp: NumberParam, topicDataFilter: TopicDataFilterValueParam, }); const handleSelectedPartitionChange = React.useCallback( - (value: string) => { + (value?: string) => { setQueryParams({selectedPartition: value}, 'replaceIn'); }, [setQueryParams], ); const handleSelectedOffsetChange = React.useCallback( - (value?: number) => { + (value?: string) => { setQueryParams({selectedOffset: value}, 'replaceIn'); }, [setQueryParams], diff --git a/src/containers/Tenant/Diagnostics/TopicData/utils/constants.ts b/src/containers/Tenant/Diagnostics/TopicData/utils/constants.ts index cbb4ac135..4f8873880 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/utils/constants.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/utils/constants.ts @@ -53,3 +53,5 @@ export const codecNumberToName: Record = { 2: 'LZOP', 3: 'ZSTD', }; + +export const TOPIC_DATA_FETCH_LIMIT = 20; diff --git a/src/store/reducers/topic.ts b/src/store/reducers/topic.ts index 22f5768db..109093ad0 100644 --- a/src/store/reducers/topic.ts +++ b/src/store/reducers/topic.ts @@ -34,7 +34,7 @@ export const topicApi = api.injectEndpoints({ return {error}; } }, - providesTags: ['All'], + keepUnusedDataFor: 0, }), }), overrideExisting: 'throw', diff --git a/src/components/DebouncedInput/DebouncedInput.tsx b/src/utils/hooks/useDebouncedValue.ts similarity index 51% rename from src/components/DebouncedInput/DebouncedInput.tsx rename to src/utils/hooks/useDebouncedValue.ts index 60df464b7..2462cc596 100644 --- a/src/components/DebouncedInput/DebouncedInput.tsx +++ b/src/utils/hooks/useDebouncedValue.ts @@ -1,14 +1,15 @@ import React from 'react'; -import type {TextInputProps} from '@gravity-ui/uikit'; -import {TextInput} from '@gravity-ui/uikit'; - -interface SearchProps extends TextInputProps { +export function useDebouncedValue({ + value, + onUpdate, + debounce = 200, +}: { + value: T; + onUpdate?: (value: T) => void; debounce?: number; -} - -export const DebouncedInput = ({onUpdate, value = '', debounce = 200, ...rest}: SearchProps) => { - const [currentValue, setCurrentValue] = React.useState(value); +}) { + const [currentValue, setCurrentValue] = React.useState(value); const timer = React.useRef(); @@ -22,7 +23,7 @@ export const DebouncedInput = ({onUpdate, value = '', debounce = 200, ...rest}: }); }, [value]); - const onSearchValueChange = (newValue: string) => { + const handleUpdate = (newValue: T) => { setCurrentValue(newValue); window.clearTimeout(timer.current); @@ -30,6 +31,5 @@ export const DebouncedInput = ({onUpdate, value = '', debounce = 200, ...rest}: onUpdate?.(newValue); }, debounce); }; - - return ; -}; + return [currentValue, handleUpdate] as const; +} diff --git a/tests/suites/paginatedTable/paginatedTable.test.ts b/tests/suites/paginatedTable/paginatedTable.test.ts index 2422c8a09..655057b9a 100644 --- a/tests/suites/paginatedTable/paginatedTable.test.ts +++ b/tests/suites/paginatedTable/paginatedTable.test.ts @@ -20,8 +20,7 @@ test.describe('PaginatedTable', () => { // Get initial row count (should be first chunk) const initialVisibleRows = await paginatedTable.getRowCount(); - expect(initialVisibleRows).toBeGreaterThan(0); - expect(initialVisibleRows).toBeLessThan(100); // Should not show all rows initially + expect(initialVisibleRows).toEqual(100); // Should not show all rows initially // Get data from first visible row to verify initial chunk const firstRowData = await paginatedTable.getRowData(0); From 84c27acba23276d64a8c7d8efc626a2f617c0c4f Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 25 Apr 2025 10:14:23 +0300 Subject: [PATCH 13/21] fix: add tests --- .../TopicData/__test__/getData.test.ts | 151 ++++++++++++++++++ .../Tenant/Diagnostics/TopicData/getData.ts | 59 +++---- 2 files changed, 183 insertions(+), 27 deletions(-) create mode 100644 src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts diff --git a/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts b/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts new file mode 100644 index 000000000..01107f130 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts @@ -0,0 +1,151 @@ +import type {TopicDataResponse, TopicMessage} from '../../../../../types/api/topic'; +import {prepareResponse} from '../getData'; +import {TOPIC_DATA_FETCH_LIMIT} from '../utils/constants'; + +describe('prepareResponse', () => { + // Test case 1: Normal case with no removed messages + test('should handle case with no removed messages', () => { + const response: TopicDataResponse = { + StartOffset: '100', + EndOffset: '120', + Messages: [{Offset: '100'}, {Offset: '101'}, {Offset: '102'}] as TopicMessage[], + }; + + const result = prepareResponse(response, 100); + + expect(result.start).toBe(100); + expect(result.end).toBe(120); + expect(result.messages.length).toBe(3); + expect(result.messages[0]).toEqual({Offset: '100'}); + expect(result.messages[1]).toEqual({Offset: '101'}); + expect(result.messages[2]).toEqual({Offset: '102'}); + }); + + // Test case 2: Case with some removed messages + test('should handle case with some removed messages', () => { + const response: TopicDataResponse = { + StartOffset: '105', + EndOffset: '120', + Messages: [{Offset: '105'}, {Offset: '106'}, {Offset: '107'}] as TopicMessage[], + }; + + const result = prepareResponse(response, 100); + + expect(result.start).toBe(105); + expect(result.end).toBe(120); + expect(result.messages.length).toBe(8); // 5 removed + 3 actual + + // Check removed messages + expect(result.messages[0]).toEqual({Offset: 'Offset 100 removed'}); + expect(result.messages[1]).toEqual({Offset: 'Offset 101 removed'}); + expect(result.messages[2]).toEqual({Offset: 'Offset 102 removed'}); + expect(result.messages[3]).toEqual({Offset: 'Offset 103 removed'}); + expect(result.messages[4]).toEqual({Offset: 'Offset 104 removed'}); + + // Check actual messages + expect(result.messages[5]).toEqual({Offset: '105'}); + expect(result.messages[6]).toEqual({Offset: '106'}); + expect(result.messages[7]).toEqual({Offset: '107'}); + }); + + // Test case 3: Case with more removed messages than the limit + test('should handle case with more removed messages than the limit', () => { + const response: TopicDataResponse = { + StartOffset: '150', + EndOffset: '170', + Messages: [{Offset: '150'}, {Offset: '151'}, {Offset: '152'}] as TopicMessage[], + }; + + const result = prepareResponse(response, 100); + + expect(result.start).toBe(150); + expect(result.end).toBe(170); + expect(result.messages.length).toBe(TOPIC_DATA_FETCH_LIMIT); // Limited by TOPIC_DATA_FETCH_LIMIT + + // All messages should be "removed" placeholders since there are more than the limit + for (let i = 0; i < TOPIC_DATA_FETCH_LIMIT; i++) { + expect(result.messages[i]).toEqual({Offset: `Offset ${100 + i} removed`}); + } + }); + + // Test case 4: Case with non-numeric offsets + test('should handle case with non-numeric offsets', () => { + const response: TopicDataResponse = { + StartOffset: 'not-a-number', + EndOffset: 'invalid', + Messages: [{Offset: '100'}, {Offset: '101'}] as TopicMessage[], + }; + + const result = prepareResponse(response, 100); + + // safeParseNumber should return 0 for non-numeric values + expect(result.start).toBe(0); + expect(result.end).toBe(0); + + // Since start (0) < offset (100), removedMessagesCount is negative + // No removed messages should be added + expect(result.messages.length).toBe(2); + expect(result.messages[0]).toEqual({Offset: '100'}); + expect(result.messages[1]).toEqual({Offset: '101'}); + }); + + // Test case 5: Case with empty Messages array + test('should handle case with empty Messages array', () => { + const response: TopicDataResponse = { + StartOffset: '100', + EndOffset: '100', + Messages: [], + }; + + const result = prepareResponse(response, 100); + + expect(result.start).toBe(100); + expect(result.end).toBe(100); + expect(result.messages.length).toBe(0); + }); + + // Test case 6: Case with more messages than the limit + test('should handle case with more messages than the limit', () => { + // Create an array of 30 messages (more than TOPIC_DATA_FETCH_LIMIT) + const messages: TopicMessage[] = []; + for (let i = 0; i < TOPIC_DATA_FETCH_LIMIT + 1; i++) { + messages.push({Offset: `${100 + i}`} as TopicMessage); + } + + const response: TopicDataResponse = { + StartOffset: '100', + EndOffset: '130', + Messages: messages, + }; + + const result = prepareResponse(response, 100); + + expect(result.start).toBe(100); + expect(result.end).toBe(130); + + // Should be limited to TOPIC_DATA_FETCH_LIMIT + expect(result.messages.length).toBe(TOPIC_DATA_FETCH_LIMIT); + + // Check the first few messages + expect(result.messages[0]).toEqual({Offset: '100'}); + expect(result.messages[1]).toEqual({Offset: '101'}); + expect(result.messages[2]).toEqual({Offset: '102'}); + }); + + // Test case 7: Case with both removed messages and actual messages within limit + test('should handle case with both removed and actual messages within limit', () => { + const response: TopicDataResponse = { + StartOffset: '110', + EndOffset: '130', + Messages: Array.from({length: 15}, (_, i) => ({Offset: `${110 + i}`}) as TopicMessage), + }; + + const result = prepareResponse(response, 100); + + expect(result.start).toBe(110); + expect(result.end).toBe(130); + + // 10 removed + 10 actual = 20 (TOPIC_DATA_FETCH_LIMIT) + expect(result.messages.length).toBe(TOPIC_DATA_FETCH_LIMIT); + }); +}); diff --git a/src/containers/Tenant/Diagnostics/TopicData/getData.ts b/src/containers/Tenant/Diagnostics/TopicData/getData.ts index a833d234e..bd9a7dc82 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/getData.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/getData.ts @@ -1,7 +1,7 @@ import {isNil} from 'lodash'; import type {FetchData} from '../../../../components/PaginatedTable'; -import type {TopicDataRequest, TopicMessage} from '../../../../types/api/topic'; +import type {TopicDataRequest, TopicDataResponse, TopicMessage} from '../../../../types/api/topic'; import {safeParseNumber} from '../../../../utils/utils'; import {TOPIC_DATA_FETCH_LIMIT} from './utils/constants'; @@ -15,6 +15,35 @@ interface GetTopicDataProps { baseOffset?: number; } +export function prepareResponse(response: TopicDataResponse, offset: number) { + const {StartOffset, EndOffset, Messages = []} = response; + + const start = safeParseNumber(StartOffset); + const end = safeParseNumber(EndOffset); + + const removedMessagesCount = start - offset; + + const result = []; + for (let i = 0; i < Math.min(TOPIC_DATA_FETCH_LIMIT, removedMessagesCount); i++) { + result.push({ + Offset: `Offset ${offset + i} removed`, + }); + } + for ( + let i = 0; + i < + Math.min( + TOPIC_DATA_FETCH_LIMIT, + TOPIC_DATA_FETCH_LIMIT - removedMessagesCount, + Messages.length, + ); + i++ + ) { + result.push(Messages[i]); + } + return {start, end, messages: result}; +} + export const generateTopicDataGetter = ({ setStartOffset, setEndOffset, @@ -42,31 +71,7 @@ export const generateTopicDataGetter = ({ const response = await window.api.viewer.getTopicData(queryParams); - const {StartOffset, EndOffset, Messages = []} = response; - - const start = safeParseNumber(StartOffset); - const end = safeParseNumber(EndOffset); - - const removedMessagesCount = start - normalizedOffset; - - const result = []; - for (let i = 0; i < Math.min(TOPIC_DATA_FETCH_LIMIT, removedMessagesCount); i++) { - result.push({ - Offset: `Offset ${normalizedOffset + i} removed`, - }); - } - for ( - let i = 0; - i < - Math.min( - TOPIC_DATA_FETCH_LIMIT, - TOPIC_DATA_FETCH_LIMIT - removedMessagesCount, - Messages.length, - ); - i++ - ) { - result.push(Messages[i]); - } + const {start, end, messages} = prepareResponse(response, normalizedOffset); //need to update start and end offsets every time data is fetched to show fresh data in parent component setStartOffset(start); @@ -75,7 +80,7 @@ export const generateTopicDataGetter = ({ const quantity = end - baseOffset; return { - data: result, + data: messages, total: quantity, found: quantity, }; From dffe52ef14576e5f46a5db1905a4a7ac8e292197 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 25 Apr 2025 15:42:22 +0300 Subject: [PATCH 14/21] fix --- .../Diagnostics/TopicData/TopicData.tsx | 34 ++++++++++--------- .../TopicData/__test__/getData.test.ts | 12 +++---- .../Tenant/Diagnostics/TopicData/getData.ts | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx index 06675443b..40d583b6a 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -53,8 +53,8 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { const [controlsKey, setControlsKey] = React.useState(0); const [emptyData, setEmptyData] = React.useState(false); - const [baseOffset, setBaseOffset] = React.useState(0); - const [baseEndOffset, setBaseEndOffset] = React.useState(0); + const [baseOffset, setBaseOffset] = React.useState(); + const [baseEndOffset, setBaseEndOffset] = React.useState(); const startRef = React.useRef(); startRef.current = startOffset; @@ -242,20 +242,22 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { return ( setFullValue(undefined)} /> - + {!isNil(baseOffset) && !isNil(baseEndOffset) && ( + + )} ); } diff --git a/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts b/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts index 01107f130..83cfdd4c8 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts @@ -36,11 +36,11 @@ describe('prepareResponse', () => { expect(result.messages.length).toBe(8); // 5 removed + 3 actual // Check removed messages - expect(result.messages[0]).toEqual({Offset: 'Offset 100 removed'}); - expect(result.messages[1]).toEqual({Offset: 'Offset 101 removed'}); - expect(result.messages[2]).toEqual({Offset: 'Offset 102 removed'}); - expect(result.messages[3]).toEqual({Offset: 'Offset 103 removed'}); - expect(result.messages[4]).toEqual({Offset: 'Offset 104 removed'}); + expect(result.messages[0]).toEqual({Offset: ' 100'}); + expect(result.messages[1]).toEqual({Offset: ' 101'}); + expect(result.messages[2]).toEqual({Offset: ' 102'}); + expect(result.messages[3]).toEqual({Offset: ' 103'}); + expect(result.messages[4]).toEqual({Offset: ' 104'}); // Check actual messages expect(result.messages[5]).toEqual({Offset: '105'}); @@ -64,7 +64,7 @@ describe('prepareResponse', () => { // All messages should be "removed" placeholders since there are more than the limit for (let i = 0; i < TOPIC_DATA_FETCH_LIMIT; i++) { - expect(result.messages[i]).toEqual({Offset: `Offset ${100 + i} removed`}); + expect(result.messages[i]).toEqual({Offset: ` ${100 + i}`}); } }); diff --git a/src/containers/Tenant/Diagnostics/TopicData/getData.ts b/src/containers/Tenant/Diagnostics/TopicData/getData.ts index bd9a7dc82..7982a73ba 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/getData.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/getData.ts @@ -26,7 +26,7 @@ export function prepareResponse(response: TopicDataResponse, offset: number) { const result = []; for (let i = 0; i < Math.min(TOPIC_DATA_FETCH_LIMIT, removedMessagesCount); i++) { result.push({ - Offset: `Offset ${offset + i} removed`, + Offset: ` ${offset + i}`, }); } for ( From f904c587f7912e42986a5f7caacf6e7493bb9cba Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 25 Apr 2025 15:52:33 +0300 Subject: [PATCH 15/21] fix --- .../Diagnostics/TopicData/TopicData.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx index 40d583b6a..013afab91 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -155,6 +155,14 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { [path, database, selectedPartition, emptyData], ); + const resetFilters = React.useCallback(() => { + handleSelectedOffsetChange(undefined); + handleStartTimestampChange(undefined); + if (topicDataFilter === 'TIMESTAMP') { + setControlsKey((prev) => prev + 1); + } + }, [handleSelectedOffsetChange, handleStartTimestampChange, topicDataFilter]); + const scrollToOffset = React.useCallback( (newOffset: number) => { const scrollTop = (newOffset - (baseOffset ?? 0)) * DEFAULT_TABLE_ROW_HEIGHT; @@ -183,15 +191,17 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { const scrollToStartOffset = React.useCallback(() => { if (startOffset) { + resetFilters(); scrollToOffset(startOffset); } - }, [startOffset, scrollToOffset]); + }, [startOffset, scrollToOffset, resetFilters]); const scrollToEndOffset = React.useCallback(() => { if (endOffset) { + resetFilters(); scrollToOffset(endOffset); } - }, [endOffset, scrollToOffset]); + }, [endOffset, scrollToOffset, resetFilters]); const renderControls: RenderControls = () => { return ( @@ -214,20 +224,11 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { const renderEmptyDataMessage = () => { const hasFilters = selectedOffset || startTimestamp; - const resetFilter = () => { - if (topicDataFilter === 'OFFSET') { - handleSelectedOffsetChange(undefined); - } else if (topicDataFilter === 'TIMESTAMP') { - handleStartTimestampChange(undefined); - setControlsKey((prev) => prev + 1); - } - }; - return ( } /> From d358294f96c9a0347a072cd8810f8a51c49c4e0a Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 25 Apr 2025 16:25:55 +0300 Subject: [PATCH 16/21] fix: test --- tests/suites/paginatedTable/paginatedTable.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/suites/paginatedTable/paginatedTable.test.ts b/tests/suites/paginatedTable/paginatedTable.test.ts index 655057b9a..7bbdd8d70 100644 --- a/tests/suites/paginatedTable/paginatedTable.test.ts +++ b/tests/suites/paginatedTable/paginatedTable.test.ts @@ -20,7 +20,7 @@ test.describe('PaginatedTable', () => { // Get initial row count (should be first chunk) const initialVisibleRows = await paginatedTable.getRowCount(); - expect(initialVisibleRows).toEqual(100); // Should not show all rows initially + expect(initialVisibleRows).toEqual(40); // Should not show all rows initially // Get data from first visible row to verify initial chunk const firstRowData = await paginatedTable.getRowData(0); From 2af2961a774aa5038946fd60f54bee9eff9f5bb3 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Sat, 26 Apr 2025 12:09:16 +0300 Subject: [PATCH 17/21] fix: should not cache data --- src/components/PaginatedTable/PaginatedTable.tsx | 3 +++ src/components/PaginatedTable/TableChunk.tsx | 4 ++++ src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx | 1 + 3 files changed, 8 insertions(+) diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index bc9fc6ad4..01dc6a998 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -38,6 +38,7 @@ export interface PaginatedTableProps { renderErrorMessage?: RenderErrorMessage; containerClassName?: string; onDataFetched?: (data: PaginatedTableData) => void; + keepCache?: boolean; } const DEFAULT_PAGINATION_LIMIT = 20; @@ -59,6 +60,7 @@ export const PaginatedTable = ({ renderEmptyDataMessage, containerClassName, onDataFetched, + keepCache = true, }: PaginatedTableProps) => { const initialTotal = initialEntitiesCount || 0; const initialFound = initialEntitiesCount || 1; @@ -134,6 +136,7 @@ export const PaginatedTable = ({ renderEmptyDataMessage={renderEmptyDataMessage} onDataFetched={handleDataFetched} isActive={isActive} + keepCache={keepCache} /> )); }; diff --git a/src/components/PaginatedTable/TableChunk.tsx b/src/components/PaginatedTable/TableChunk.tsx index df01ee4d4..0a4085a7e 100644 --- a/src/components/PaginatedTable/TableChunk.tsx +++ b/src/components/PaginatedTable/TableChunk.tsx @@ -37,6 +37,8 @@ interface TableChunkProps { renderErrorMessage?: RenderErrorMessage; renderEmptyDataMessage?: RenderEmptyDataMessage; onDataFetched: (data?: PaginatedTableData) => void; + + keepCache?: boolean; } // Memoisation prevents chunks rerenders that could cause perfomance issues on big tables @@ -55,6 +57,7 @@ export const TableChunk = typedMemo(function TableChunk({ renderEmptyDataMessage, onDataFetched, isActive, + keepCache, }: TableChunkProps) { const [isTimeoutActive, setIsTimeoutActive] = React.useState(true); const [autoRefreshInterval] = useAutoRefreshInterval(); @@ -74,6 +77,7 @@ export const TableChunk = typedMemo(function TableChunk({ tableDataApi.useFetchTableChunkQuery(queryParams, { skip: isTimeoutActive || !isActive, pollingInterval: autoRefreshInterval, + refetchOnMountOrArgChange: !keepCache, }); const {currentData, error} = tableDataApi.endpoints.fetchTableChunk.useQueryState(queryParams); diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx index 013afab91..fd82a5c1b 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -257,6 +257,7 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { filters={tableFilters} tableName="topicData" rowHeight={DEFAULT_TABLE_ROW_HEIGHT} + keepCache={false} /> )} From f297f08d64f5907f41d9763528db084c3cc1ae3e Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Sat, 26 Apr 2025 14:13:14 +0300 Subject: [PATCH 18/21] feat: should scroll to selected offset --- .../Diagnostics/TopicData/TopicData.tsx | 26 +++------ .../TopicDataControls/TopicDataControls.tsx | 54 +++++++++++++++---- .../Tenant/Diagnostics/TopicData/i18n/en.json | 1 + 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx index fd82a5c1b..374906b12 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -164,15 +164,18 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { }, [handleSelectedOffsetChange, handleStartTimestampChange, topicDataFilter]); const scrollToOffset = React.useCallback( - (newOffset: number) => { + (newOffset: number, reset?: boolean) => { const scrollTop = (newOffset - (baseOffset ?? 0)) * DEFAULT_TABLE_ROW_HEIGHT; const normalizedScrollTop = Math.max(0, scrollTop); parentRef.current?.scrollTo({ top: normalizedScrollTop, behavior: 'instant', }); + if (reset) { + resetFilters(); + } }, - [baseOffset, parentRef], + [baseOffset, parentRef, resetFilters], ); React.useEffect(() => { @@ -189,20 +192,6 @@ export function TopicData({parentRef, path, database}: TopicDataProps) { } }, [currentData, isFetching, scrollToOffset]); - const scrollToStartOffset = React.useCallback(() => { - if (startOffset) { - resetFilters(); - scrollToOffset(startOffset); - } - }, [startOffset, scrollToOffset, resetFilters]); - - const scrollToEndOffset = React.useCallback(() => { - if (endOffset) { - resetFilters(); - scrollToOffset(endOffset); - } - }, [endOffset, scrollToOffset, resetFilters]); - 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 4ce14c415..1f690bb1d 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicDataControls/TopicDataControls.tsx @@ -3,7 +3,7 @@ import React from 'react'; import type {Value} from '@gravity-ui/date-components'; import {RelativeDatePicker} from '@gravity-ui/date-components'; import {dateTimeParse} from '@gravity-ui/date-utils'; -import {ArrowDownToLine, ArrowUpToLine} from '@gravity-ui/icons'; +import {ArrowDownToLine, ArrowUpToLine, CircleChevronDownFill} from '@gravity-ui/icons'; import type {TableColumnSetupItem} from '@gravity-ui/uikit'; import { ActionTooltip, @@ -21,6 +21,7 @@ import {EntitiesCount} from '../../../../../components/EntitiesCount'; import type {PreparedPartitionData} from '../../../../../store/reducers/partitions/types'; import {formatNumber} from '../../../../../utils/dataFormatters/dataFormatters'; import {prepareErrorMessage} from '../../../../../utils/prepareErrorMessage'; +import {safeParseNumber} from '../../../../../utils/utils'; import i18n from '../i18n'; import {useTopicDataQueryParams} from '../useTopicDataQueryParams'; import {b} from '../utils/constants'; @@ -35,23 +36,21 @@ interface TopicDataControlsProps { partitionsLoading: boolean; partitionsError: unknown; - initialOffset?: number; + startOffset?: number; endOffset?: number; - scrollToStartOffset: VoidFunction; - scrollToEndOffset: VoidFunction; + scrollToOffset: (start: number, reset?: boolean) => void; } export function TopicDataControls({ columnsToSelect, handleSelectedColumnsUpdate, - initialOffset, + startOffset, endOffset, partitions, partitionsLoading, partitionsError, - scrollToStartOffset, - scrollToEndOffset, + scrollToOffset, }: TopicDataControlsProps) { const { selectedPartition, @@ -78,6 +77,18 @@ export function TopicDataControls({ ], ); + const scrollToStartOffset = React.useCallback(() => { + if (startOffset) { + scrollToOffset(startOffset, true); + } + }, [startOffset, scrollToOffset]); + + const scrollToEndOffset = React.useCallback(() => { + if (endOffset) { + scrollToOffset(endOffset, true); + } + }, [endOffset, scrollToOffset]); + return ( + + {!isNil(startOffset) && !isNil(endOffset) && ( + + {formatNumber(startOffset)}—{formatNumber(endOffset - 1)} + + )} - {!isNil(startOffset) && !isNil(endOffset) && ( - - - - - - - - - - )} ); } diff --git a/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts b/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts index 83cfdd4c8..19291dfc9 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/__test__/getData.test.ts @@ -36,11 +36,11 @@ describe('prepareResponse', () => { expect(result.messages.length).toBe(8); // 5 removed + 3 actual // Check removed messages - expect(result.messages[0]).toEqual({Offset: ' 100'}); - expect(result.messages[1]).toEqual({Offset: ' 101'}); - expect(result.messages[2]).toEqual({Offset: ' 102'}); - expect(result.messages[3]).toEqual({Offset: ' 103'}); - expect(result.messages[4]).toEqual({Offset: ' 104'}); + expect(result.messages[0]).toEqual({Offset: '100', removed: true}); + expect(result.messages[1]).toEqual({Offset: '101', removed: true}); + expect(result.messages[2]).toEqual({Offset: '102', removed: true}); + expect(result.messages[3]).toEqual({Offset: '103', removed: true}); + expect(result.messages[4]).toEqual({Offset: '104', removed: true}); // Check actual messages expect(result.messages[5]).toEqual({Offset: '105'}); @@ -64,7 +64,7 @@ describe('prepareResponse', () => { // All messages should be "removed" placeholders since there are more than the limit for (let i = 0; i < TOPIC_DATA_FETCH_LIMIT; i++) { - expect(result.messages[i]).toEqual({Offset: ` ${100 + i}`}); + expect(result.messages[i]).toEqual({Offset: `${100 + i}`, removed: true}); } }); diff --git a/src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss b/src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss index 9ddf18f5e..5be8951b6 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss +++ b/src/containers/Tenant/Diagnostics/TopicData/columns/Columns.scss @@ -11,4 +11,12 @@ color: var(--g-color-text-info); } + &__offset_removed { + text-decoration: line-through; + } + + &__message_invalid, + &__truncated { + font-style: italic; + } } diff --git a/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx b/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx index 9a080584e..f2230f669 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/columns/columns.tsx @@ -1,12 +1,14 @@ import React from 'react'; import DataTable from '@gravity-ui/react-data-table'; +import {Text} from '@gravity-ui/uikit'; import {isNil} from 'lodash'; import {EntityStatus} from '../../../../../components/EntityStatus/EntityStatus'; import {MultilineTableHeader} from '../../../../../components/MultilineTableHeader/MultilineTableHeader'; import type {Column} from '../../../../../components/PaginatedTable'; -import type {TopicMessage, TopicMessageMetadataItem} from '../../../../../types/api/topic'; +import {TOPIC_MESSAGE_SIZE_LIMIT} from '../../../../../store/reducers/topic'; +import type {TopicMessageEnhanced} from '../../../../../types/api/topic'; import {cn} from '../../../../../utils/cn'; import {EMPTY_DATA_PLACEHOLDER} from '../../../../../utils/constants'; import {formatBytes, formatTimestamp} from '../../../../../utils/dataFormatters/dataFormatters'; @@ -21,13 +23,24 @@ import './Columns.scss'; const b = cn('ydb-diagnostics-topic-data-columns'); -export function getAllColumns(setFullValue: (value: string | TopicMessageMetadataItem[]) => void) { - const columns: Column[] = [ +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}) => valueOrPlaceholder(row.Offset), + render: ({row}) => { + const {Offset, removed} = row; + return ( + + {valueOrPlaceholder(Offset)} + + ); + }, width: 100, }, { @@ -78,15 +91,7 @@ export function getAllColumns(setFullValue: (value: string | TopicMessageMetadat const prepared = MessageMetadata.map( ({Key = '', Value = ''}) => `${Key}: ${Value}`, ); - const isTruncated = prepared.length > 0; - return ( - setFullValue(MessageMetadata) : undefined} - > - {prepared.join(', ')} - - ); + return prepared.join(', '); }, width: 200, }, @@ -94,11 +99,35 @@ export function getAllColumns(setFullValue: (value: string | TopicMessageMetadat name: TOPIC_DATA_COLUMNS_IDS.MESSAGE, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.MESSAGE], align: DataTable.LEFT, - render: ({row: {Message: message}}) => { - if (isNil(message)) { + render: ({row: {Message, OriginalSize}}) => { + if (isNil(Message)) { return EMPTY_DATA_PLACEHOLDER; } - return ; + 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, }, @@ -179,40 +208,9 @@ function TopicDataTimestamp({timestamp}: TopicDataTimestampProps) { ); } -function valueOrPlaceholder(value: string | undefined, placeholder = EMPTY_DATA_PLACEHOLDER) { +function valueOrPlaceholder( + value: string | number | undefined, + placeholder = EMPTY_DATA_PLACEHOLDER, +) { return isNil(value) ? placeholder : value; } - -interface MessageProps { - setFullValue: (value: string) => void; - message: string; -} - -function Message({setFullValue, message}: MessageProps) { - const longMessage = message.length > 200; - - let encryptedMessage: string | undefined; - - if (!longMessage) { - try { - encryptedMessage = atob(message); - } catch {} - } - - const handleClick = () => { - try { - if (!encryptedMessage) { - encryptedMessage = atob(message); - } - setFullValue(encryptedMessage); - } catch {} - }; - return ( - - {encryptedMessage ?? i18n('action_show-message')} - - ); -} diff --git a/src/containers/Tenant/Diagnostics/TopicData/getData.ts b/src/containers/Tenant/Diagnostics/TopicData/getData.ts index 30089594a..e8805870e 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/getData.ts +++ b/src/containers/Tenant/Diagnostics/TopicData/getData.ts @@ -1,7 +1,12 @@ import {isNil} from 'lodash'; import type {FetchData} from '../../../../components/PaginatedTable'; -import type {TopicDataRequest, TopicDataResponse, TopicMessage} from '../../../../types/api/topic'; +import type { + TopicDataRequest, + TopicDataResponse, + TopicMessage, + TopicMessageEnhanced, +} from '../../../../types/api/topic'; import {safeParseNumber} from '../../../../utils/utils'; import {TOPIC_DATA_FETCH_LIMIT} from './utils/constants'; @@ -23,10 +28,11 @@ export function prepareResponse(response: TopicDataResponse, offset: number) { const removedMessagesCount = start - offset; - const result = []; + const result: TopicMessageEnhanced[] = []; for (let i = 0; i < Math.min(TOPIC_DATA_FETCH_LIMIT, removedMessagesCount); i++) { result.push({ - Offset: ` ${offset + i}`, + Offset: String(offset + i), + removed: true, }); } for ( diff --git a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json index cb1b69895..ecbc71682 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopicData/i18n/en.json @@ -23,5 +23,6 @@ "action_scroll-down": "Scroll to the end", "action_scroll-selected": "Scroll to selected offset", "action_scroll-up": "Scroll to the start", - "action_show-message": "Show message content" + "description_failed-decode": "Failed to decode message", + "description_truncated": "[truncated]" } diff --git a/src/services/api/viewer.ts b/src/services/api/viewer.ts index 4039c79cc..83b93b5d8 100644 --- a/src/services/api/viewer.ts +++ b/src/services/api/viewer.ts @@ -263,14 +263,10 @@ export class ViewerAPI extends BaseYdbAPI { } getTopicData(params: TopicDataRequest, {concurrentId, signal}: AxiosOptions = {}) { - return this.get( - this.getPath('/viewer/json/topic_data'), - {message_size_limit: '5000', ...params}, - { - concurrentId, - requestConfig: {signal}, - }, - ); + return this.get(this.getPath('/viewer/json/topic_data'), params, { + concurrentId, + requestConfig: {signal}, + }); } getConsumer( diff --git a/src/store/reducers/topic.ts b/src/store/reducers/topic.ts index 109093ad0..44e8862d1 100644 --- a/src/store/reducers/topic.ts +++ b/src/store/reducers/topic.ts @@ -8,6 +8,8 @@ import type {RootState} from '../defaultStore'; import {api} from './api'; +export const TOPIC_MESSAGE_SIZE_LIMIT = 1000; + export const topicApi = api.injectEndpoints({ endpoints: (build) => ({ getTopic: build.query({ @@ -28,7 +30,10 @@ export const topicApi = api.injectEndpoints({ getTopicData: build.query({ queryFn: async (params: TopicDataRequest) => { try { - const data = await window.api.viewer.getTopicData(params); + const data = await window.api.viewer.getTopicData({ + message_size_limit: TOPIC_MESSAGE_SIZE_LIMIT, + ...params, + }); return {data}; } catch (error) { return {error}; diff --git a/src/types/api/topic.ts b/src/types/api/topic.ts index a0c70c8f5..f36b764dd 100644 --- a/src/types/api/topic.ts +++ b/src/types/api/topic.ts @@ -216,7 +216,7 @@ export interface TopicMessage { * * Message offset in the partition */ - Offset?: string; + Offset?: string | number; /** * uint64 @@ -283,6 +283,10 @@ export interface TopicMessage { MessageMetadata?: TopicMessageMetadataItem[]; } +export interface TopicMessageEnhanced extends TopicMessage { + removed?: boolean; +} + export interface TopicMessageMetadataItem { /** * Metadata key