import React from 'react'; import {CircleExclamationFill, TriangleExclamation} from '@gravity-ui/icons'; import DataTable from '@gravity-ui/react-data-table'; import type {TextProps} from '@gravity-ui/uikit'; import {ActionTooltip, Icon, Popover, Text} from '@gravity-ui/uikit'; import {isNil} from 'lodash'; import {Link} from 'react-router-dom'; import {EntityName} from '../../../../../components/EntityName/EntityName'; import {MultilineTableHeader} from '../../../../../components/MultilineTableHeader/MultilineTableHeader'; import type {Column} from '../../../../../components/PaginatedTable'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {TOPIC_MESSAGE_SIZE_LIMIT} from '../../../../../store/reducers/topic/topic'; import type {TopicMessageEnhanced} from '../../../../../types/api/topic'; import {cn} from '../../../../../utils/cn'; import {EMPTY_DATA_PLACEHOLDER, YDB_POPOVER_CLASS_NAME} from '../../../../../utils/constants'; import {formatBytes, formatTimestamp} from '../../../../../utils/dataFormatters/dataFormatters'; import {isModifiedClickEvent} from '../../../../../utils/events'; import {formatToMs} from '../../../../../utils/timeParsers'; import {safeParseNumber} from '../../../../../utils/utils'; import {useDiagnosticsPageLinkGetter} from '../../DiagnosticsPages'; import i18n from '../i18n'; import {useTopicDataQueryParams} from '../useTopicDataQueryParams'; import {TOPIC_DATA_COLUMNS_TITLES, codecNumberToName} from '../utils/constants'; import type {TopicDataColumnId} from '../utils/types'; import {TOPIC_DATA_COLUMNS_IDS} from '../utils/types'; import './Columns.scss'; const b = cn('ydb-diagnostics-topic-data-columns'); export const offsetColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.OFFSET, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.OFFSET], align: DataTable.LEFT, render: ({row}) => { const {Offset: offset, removed, notLoaded} = row; return ; }, width: 100, }; export const timestampCreateColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.TIMESTAMP_CREATE, header: ( ), align: DataTable.LEFT, render: ({row: {CreateTimestamp}}) => CreateTimestamp ? ( } name={formatTimestamp(CreateTimestamp)} hasClipboardButton /> ) : ( EMPTY_DATA_PLACEHOLDER ), width: 220, }; export const timestampWriteColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.TIMESTAMP_WRITE, header: ( ), align: DataTable.LEFT, render: ({row: {WriteTimestamp}}) => WriteTimestamp ? ( } name={formatTimestamp(WriteTimestamp)} hasClipboardButton /> ) : ( EMPTY_DATA_PLACEHOLDER ), width: 220, }; export const tsDiffColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.TS_DIFF, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.TS_DIFF], align: DataTable.RIGHT, render: ({row: {TimestampDiff}}) => , width: 110, note: i18n('context_ts-diff'), }; export const metadataColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.METADATA, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.METADATA], align: DataTable.LEFT, render: ({row: {MessageMetadata}}) => { if (!MessageMetadata) { return EMPTY_DATA_PLACEHOLDER; } const prepared = MessageMetadata.map(({Key = '', Value = ''}) => `${Key}: ${Value}`); return prepared.join(', '); }, width: 200, }; export const messageColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.MESSAGE, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.MESSAGE], align: DataTable.LEFT, render: ({row: {Message, OriginalSize, SchematizeError, isSchematized}}) => ( ), width: 500, }; export const sizeColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.SIZE, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.SIZE], align: DataTable.RIGHT, render: ({row: {StorageSize}}) => , width: 100, }; export const originalSizeColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.ORIGINAL_SIZE, header: ( ), align: DataTable.RIGHT, render: ({row}) => formatBytes(row.OriginalSize), width: 100, }; export const codecColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.CODEC, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.CODEC], align: DataTable.RIGHT, render: ({row: {Codec}}) => { if (isNil(Codec)) { return EMPTY_DATA_PLACEHOLDER; } return codecNumberToName[Codec] ?? Codec; }, width: 70, }; export const producerIdColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.PRODUCERID, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.PRODUCERID], align: DataTable.LEFT, render: ({row}) => row.ProducerId ? ( ) : ( EMPTY_DATA_PLACEHOLDER ), width: 100, }; export const seqNoColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.SEQNO, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.SEQNO], align: DataTable.RIGHT, render: ({row}) => row.SeqNo ? : EMPTY_DATA_PLACEHOLDER, width: 100, }; export const ipColumn: Column = { name: TOPIC_DATA_COLUMNS_IDS.IP, header: TOPIC_DATA_COLUMNS_TITLES[TOPIC_DATA_COLUMNS_IDS.IP], align: DataTable.LEFT, render: ({row}) => row.Ip ? : EMPTY_DATA_PLACEHOLDER, width: 220, }; export function getAllColumns() { const columns: Column[] = [ offsetColumn, timestampCreateColumn, timestampWriteColumn, tsDiffColumn, metadataColumn, messageColumn, sizeColumn, originalSizeColumn, codecColumn, producerIdColumn, seqNoColumn, ipColumn, ]; return columns; } export const DEFAULT_TOPIC_DATA_COLUMNS: TopicDataColumnId[] = [ 'offset', 'timestampCreate', 'tsDiff', 'message', ]; export const REQUIRED_TOPIC_DATA_COLUMNS: TopicDataColumnId[] = ['offset']; interface TopicDataTimestampProps { timestamp?: string; } export function TopicDataTimestamp({timestamp}: TopicDataTimestampProps) { if (!timestamp) { return EMPTY_DATA_PLACEHOLDER; } const formatted = formatTimestamp(timestamp); const splitted = formatted.split('.'); const ms = splitted.pop(); return ( {splitted.join('.')} .{ms} ); } interface PartitionIdProps { offset?: string | number; removed?: boolean; notLoaded?: boolean; } function Offset({offset, removed, notLoaded}: PartitionIdProps) { const getDiagnosticsPageLink = useDiagnosticsPageLinkGetter(); const {handleActiveOffsetChange} = useTopicDataQueryParams(); if (isNil(offset)) { return EMPTY_DATA_PLACEHOLDER; } if (removed) { return ( {offset} ); } const offsetLink = getDiagnosticsPageLink(TENANT_DIAGNOSTICS_TABS_IDS.topicData, { activeOffset: String(offset), }); const handleClick: React.MouseEventHandler = (e) => { // Let the browser handle modified clicks (new tab/window) natively if (isModifiedClickEvent(e)) { e.stopPropagation(); return; } // if allow to navigate link, the table will be rerendered e.stopPropagation(); e.preventDefault(); handleActiveOffsetChange(String(offset)); }; return ( {offset} {notLoaded && ( {i18n('description_not-loaded-message')} } className={b('help-popover')} > )} ); } interface TopicDataTsDiffProps { value?: string; baseColor?: TextProps['color']; variant?: TextProps['variant']; } export function TopicDataTsDiff({ value, baseColor = 'primary', variant = 'body-2', }: TopicDataTsDiffProps) { if (isNil(value)) { return EMPTY_DATA_PLACEHOLDER; } const numericValue = safeParseNumber(value); return ( = 100_000 ? 'danger' : baseColor}> {formatToMs(numericValue)} ); } interface TopicDataMessageProps { message?: string | unknown; size?: number; messageSchematizeError?: string; /** * When true the message value is already schematized (including string * primitives) and must be rendered as-is without base64 decoding. */ isSchematized?: boolean; } interface SchematizeErrorIconProps { error?: string; } export function SchematizeErrorIcon({error}: SchematizeErrorIconProps) { if (!error) { return null; } return ( ); } export function TopicDataMessage({ message, size, messageSchematizeError, isSchematized, }: TopicDataMessageProps) { const errorIcon = ; if (isNil(message)) { return messageSchematizeError ? ( {errorIcon} {EMPTY_DATA_PLACEHOLDER} ) : ( EMPTY_DATA_PLACEHOLDER ); } // A schematized value is already a parsed JSON value (object/array or a // primitive such as a string) and must be rendered as-is. Only legacy, // non-schematized string values are base64-encoded and need decoding. A raw // string value alone is ambiguous, so we rely on the `isSchematized` signal // derived from the response schema context rather than guessing from the type. let messageToRender: string; let invalid = false; if (typeof message === 'string') { if (isSchematized) { messageToRender = message; } else { messageToRender = message; try { messageToRender = atob(message); } catch (e) { console.warn(e); messageToRender = i18n('description_failed-decode'); invalid = true; } } } else { try { messageToRender = JSON.stringify(message); } catch (e) { console.warn(e); messageToRender = String(message); } } const truncated = safeParseNumber(size) > TOPIC_MESSAGE_SIZE_LIMIT; let title = messageToRender; if (truncated) { title += ` ${i18n('description_truncated')}`; } return ( {errorIcon} {messageToRender} {truncated && ( {' '} {i18n('description_truncated')} )} ); } interface TopicDataSizeProps { size?: number; } export function TopicDataSize({size}: TopicDataSizeProps) { return formatBytes(size); }