diff --git a/package-lock.json b/package-lock.json index 8db6afa19d..de6fb5e7a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@gravity-ui/navigation": "^3.3.9", "@gravity-ui/paranoid": "^3.0.0", "@gravity-ui/react-data-table": "^2.2.1", + "@gravity-ui/react-unipika": "^0.5.0", "@gravity-ui/table": "^1.10.1", "@gravity-ui/uikit": "^7.23.0", "@gravity-ui/unipika": "^5.2.1", @@ -3201,6 +3202,19 @@ "react": ">=16.14.0" } }, + "node_modules/@gravity-ui/react-unipika": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/react-unipika/-/react-unipika-0.5.0.tgz", + "integrity": "sha512-EPYd8oNzLeWodup2l1BbU5cbSbqq0rLhMenSvby3rEpNGcMgSZr2P9NexkipCAJ32jN+VriSdPhTXPfg4yHWag==", + "license": "MIT", + "peerDependencies": { + "@bem-react/classname": "^1.6.0", + "@gravity-ui/i18n": "^1.0.0", + "@gravity-ui/icons": "^2.0.0", + "@gravity-ui/table": "^1.10.1", + "@gravity-ui/uikit": "^7.1.1" + } + }, "node_modules/@gravity-ui/stylelint-config": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@gravity-ui/stylelint-config/-/stylelint-config-5.0.0.tgz", diff --git a/package.json b/package.json index 054f1c12e1..6b70fcab05 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@gravity-ui/navigation": "^3.3.9", "@gravity-ui/paranoid": "^3.0.0", "@gravity-ui/react-data-table": "^2.2.1", + "@gravity-ui/react-unipika": "^0.5.0", "@gravity-ui/table": "^1.10.1", "@gravity-ui/uikit": "^7.23.0", "@gravity-ui/unipika": "^5.2.1", diff --git a/src/components/JsonViewer/JsonViewer.scss b/src/components/JsonViewer/JsonViewer.scss index 4590feec84..b6d6be0d71 100644 --- a/src/components/JsonViewer/JsonViewer.scss +++ b/src/components/JsonViewer/JsonViewer.scss @@ -1,9 +1,7 @@ @use '../../styles/mixins.scss'; .ydb-json-viewer { - --data-table-row-height: 20px; --toolbar-background-color: var(--g-color-base-background); - width: 100%; &__toolbar { @@ -17,56 +15,32 @@ background-color: var(--toolbar-background-color); } - &__content { - font-family: var(--g-font-family-monospace); - } - - &__row { - height: 1em; - } - - &__cell { - position: relative; - - white-space: nowrap !important; - * { - white-space: nowrap !important; - } - } - - &__collapsed { - position: absolute; - - margin-top: -2px; - margin-left: -3ex; - } - &__match-counter { align-content: center; text-wrap: nowrap; } - - &__key { - color: var(--g-color-text-misc); - } - - &__value { - &_type { - &_string { - color: var(--color-unipika-string); - } - &_boolean { - color: var(--color-unipika-bool); - } - &_null { - color: var(--color-unipika-null); - } - &_int64 { - color: var(--color-unipika-int); - } - &_double { - color: var(--color-unipika-float); + .g-ru-cell { + &__key { + color: var(--g-color-text-misc); + } + &__value { + &_type { + &_string { + color: var(--color-unipika-string); + } + &_boolean { + color: var(--color-unipika-bool); + } + &_null { + color: var(--color-unipika-null); + } + &_int64 { + color: var(--color-unipika-int); + } + &_double { + color: var(--color-unipika-float); + } } } } @@ -77,39 +51,7 @@ width: 300px; } - &__filtered { - &_highlighted { - background-color: var(--g-color-base-generic-medium); - } - &_clickable { - cursor: pointer; - - color: var(--g-color-text-info); - } - } - &__match-btn { margin-left: -1px; } - - &__full-value { - overflow: hidden auto; - - max-width: 90vw; - max-height: 90vh; - margin: var(--g-spacing-3) 0; - - word-break: break-all; - @include mixins.body-2-typography(); - } - - .data-table__head { - display: none; - } - - .data-table__td { - overflow: visible; - - padding: 0; - } } diff --git a/src/components/JsonViewer/JsonViewer.tsx b/src/components/JsonViewer/JsonViewer.tsx index 4a1518744b..531a46a345 100644 --- a/src/components/JsonViewer/JsonViewer.tsx +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -1,8 +1,8 @@ import React from 'react'; import {ChevronsCollapseVertical, ChevronsExpandVertical} from '@gravity-ui/icons'; -import type * as DT100 from '@gravity-ui/react-data-table'; -import DataTable from '@gravity-ui/react-data-table'; +import type {ReactUnipikaProps} from '@gravity-ui/react-unipika/container-scroll'; +import {ReactUnipika} from '@gravity-ui/react-unipika/container-scroll'; import {ActionTooltip, Button, Flex, Icon} from '@gravity-ui/uikit'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; @@ -10,375 +10,134 @@ import {useSetting} from '../../utils/hooks'; import type {ClipboardButtonProps} from '../ClipboardButton/ClipboardButton'; import {ClipboardButton} from '../ClipboardButton/ClipboardButton'; -import {Cell} from './components/Cell'; import {Filter} from './components/Filter'; -import {FullValueDialog} from './components/FullValueDialog'; import {block} from './constants'; import i18n from './i18n'; -import type {UnipikaSettings, UnipikaValue} from './unipika/StructuredYsonTypes'; -import {flattenUnipika} from './unipika/flattenUnipika'; -import type { - CollapsedState, - FlattenUnipikaResult, - SearchInfo, - UnipikaFlattenTreeItem, -} from './unipika/flattenUnipika'; -import {unipika} from './unipika/unipika'; import './JsonViewer.scss'; -interface JsonViewerCommonProps { - unipikaSettings?: UnipikaSettings; - extraTools?: React.ReactNode; - tableSettings?: DT100.Settings; +const defaultUnipikaSettings = { + asHTML: true, + format: 'json', + compact: false, + escapeWhitespace: true, + showDecoded: true, + binaryAsHex: false, + indent: 2, + decodeUTF8: false, +}; + +type RenderToolbarProps = NonNullable< + Parameters>[0] +>; + +interface JsonViewerProps { search?: boolean; collapsedInitially?: boolean; maxValueWidth?: number; toolbarClassName?: string; withClipboardButton?: Omit; + value: ReactUnipikaProps['value']; + scrollContainerRef: React.RefObject; } -interface JsonViewerProps extends JsonViewerCommonProps { - value: UnipikaValue | {_error: string}; -} - -interface JsonViewerComponentProps extends JsonViewerCommonProps { - value: UnipikaValue; -} - -interface State { - flattenResult: FlattenUnipikaResult; - value: JsonViewerProps['value']; - collapsedState: CollapsedState; - filter: string; - matchIndex: number; - matchedRows: Array; - fullValue?: { - value: UnipikaFlattenTreeItem['value']; - searchInfo?: SearchInfo; - }; -} - -const SETTINGS: DT100.Settings = { - displayIndices: false, - dynamicRender: true, - sortable: false, - dynamicRenderMinSize: 50, -}; - -function getCollapsedState(value: UnipikaValue) { - const {data} = flattenUnipika(value); - const collapsedState = data.reduce((acc, {path}) => { - if (path) { - acc[path] = true; - } - return acc; - }, {}); - return collapsedState; -} - -function calculateState( - value: UnipikaValue, - collapsedState: CollapsedState, - filter: string, - caseSensitive?: boolean, -) { - const flattenResult = flattenUnipika(value, { - collapsedState: collapsedState, - filter, - caseSensitive, - }); - - return Object.assign( - {}, - { - flattenResult, - matchedRows: Object.keys(flattenResult.searchIndex).map(Number), - }, - ); -} - -function isUnipikaValue(value: UnipikaValue | {_error: string}): value is UnipikaValue { - return !('_error' in value); -} - -export function JsonViewer(props: JsonViewerProps) { - const {value} = props; - if (!isUnipikaValue(value)) { - return value._error; - } - return ; -} - -function JsonViewerComponent({ - tableSettings, +export function JsonViewer({ value, - unipikaSettings, search = true, - extraTools, - collapsedInitially, - maxValueWidth = 100, toolbarClassName, withClipboardButton, -}: JsonViewerComponentProps) { + collapsedInitially, + scrollContainerRef, +}: JsonViewerProps) { const [caseSensitiveSearch, setCaseSensitiveSearch] = useSetting( SETTING_KEYS.CASE_SENSITIVE_JSON_SEARCH, false, ); - const [collapsedState, setCollapsedState] = React.useState(() => { - if (collapsedInitially) { - return getCollapsedState(value); - } - return {}; - }); - const [filter, setFilter] = React.useState(''); - const [state, setState] = React.useState<{ - flattenResult: FlattenUnipikaResult; - matchedRows: Array; - }>(() => calculateState(value, collapsedState, filter, caseSensitiveSearch)); - - const [matchIndex, setMatchIndex] = React.useState(-1); - const [fullValue, setFullValue] = React.useState<{ - value: UnipikaFlattenTreeItem['value']; - searchInfo?: SearchInfo; - }>(); - - const dataTable = React.useRef(null); - const searchRef = React.useRef(null); - - const normalizedTableSettings = React.useMemo(() => { - return { - ...SETTINGS, - dynamicInnerRef: dataTable, - ...tableSettings, - }; - }, [tableSettings]); - - const renderCell = ({row, index}: {row: UnipikaFlattenTreeItem; index: number}) => { - const { - flattenResult: {searchIndex}, - } = state; - return ( - - ); - }; - - const onTogglePathCollapse = (path: string) => { - const newCollapsedState = {...collapsedState}; - if (newCollapsedState[path]) { - delete newCollapsedState[path]; - } else { - newCollapsedState[path] = true; - } - updateState({collapsedState: newCollapsedState}); - }; - - const updateState = ( - changedState: Partial> & { - caseSensitive?: boolean; - }, - cb?: () => void, - ) => { - const { - collapsedState: newCollapsedState, - matchIndex: newMatchIndex, - filter: newFilter, - caseSensitive: newCaseSensitive, - } = changedState; - - if (newCollapsedState !== undefined) { - setCollapsedState(newCollapsedState); - } - if (newMatchIndex !== undefined) { - setMatchIndex(newMatchIndex); - } - if (newFilter !== undefined) { - setFilter(newFilter); - } - const caseSensitive = newCaseSensitive ?? caseSensitiveSearch; - setState( - calculateState( - value, - newCollapsedState ?? collapsedState, - newFilter ?? filter, - caseSensitive, - ), - ); - - cb?.(); - }; - - const renderTable = () => { - const columns: Array> = [ - { - name: 'content', - render: renderCell, - header: null, - }, - ]; - - const { - flattenResult: {data}, - } = state; - - return ( -
- block('row')} - /> -
- ); - }; - - const onExpandAll = () => { - updateState({collapsedState: {}}, () => { - onNextMatch(null, 0); - }); - }; - - const onCollapseAll = () => { - const collapsedState = getCollapsedState(value); - updateState({collapsedState}); - }; - - const onFilterChange = (filter: string) => { - updateState({filter, matchIndex: 0}, () => { - onNextMatch(null, 0); - }); - }; - - const onNextMatch = (_event: unknown, diff = 1) => { - const {matchedRows} = state; - if (!matchedRows.length) { - return; - } - - let index = (matchIndex + diff) % matchedRows.length; - if (index < 0) { - index = matchedRows.length + index; - } - - if (index !== matchIndex) { - setMatchIndex(index); - } - dataTable.current?.scrollTo(matchedRows[index] - 6); - searchRef.current?.focus(); - }; - - const onPrevMatch = () => { - onNextMatch(null, -1); - }; - - const onEnterKeyDown = (e: React.KeyboardEvent) => { - if (e.key !== 'Enter') { - return; - } - if (e.shiftKey || e.ctrlKey) { - onPrevMatch(); - } else { - onNextMatch(null); - } - }; - - const handleUpdateCaseSensitive = () => { - const newCaseSensitive = !caseSensitiveSearch; - setCaseSensitiveSearch(newCaseSensitive); - updateState({caseSensitive: newCaseSensitive}); - }; - - const renderToolbar = () => { - return ( - - {search && ( - - )} - - - - - - - - {withClipboardButton && ( - + const renderToolbar: ReactUnipikaProps['renderToolbar'] = React.useCallback( + ({ + filter, + matchIndex, + matchedRows, + allMatchPaths, + onExpandAll, + onCollapseAll, + onFilterChange, + onNextMatch, + onPrevMatch, + onEnterKeyDown, + }: RenderToolbarProps) => { + const handleUpdateCaseSensitive = () => { + setCaseSensitiveSearch(!caseSensitiveSearch); + }; + return ( + + {search && ( + )} - {extraTools} + + + + + + + + {withClipboardButton && ( + + )} + - - ); - }; - - const onShowFullText = (index: number) => { - const { - flattenResult: {searchIndex, data}, - } = state; - - setFullValue({ - value: data[index].value, - searchInfo: searchIndex[index], - }); - }; - - const onHideFullValue = () => { - setFullValue(undefined); - }; - - const renderFullValueModal = () => { - const {value, searchInfo} = fullValue ?? {}; - - const tmp = unipika.format(value, {...unipikaSettings, asHTML: false}); + ); + }, + [ + caseSensitiveSearch, + search, + toolbarClassName, + withClipboardButton, + setCaseSensitiveSearch, + ], + ); - return ( - value && ( - - ) - ); - }; + const renderError = React.useCallback((error: unknown) => { + if (error instanceof Error) { + return
{error.message}
; + } + return i18n('description_unknown-error'); + }, []); return (
- {renderToolbar()} - {renderTable()} - {renderFullValueModal()} + {/* @ts-ignore will be fixed shortly in library*/} +
); } diff --git a/src/components/JsonViewer/components/Cell.tsx b/src/components/JsonViewer/components/Cell.tsx deleted file mode 100644 index 1fd9568095..0000000000 --- a/src/components/JsonViewer/components/Cell.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React from 'react'; - -import {Icon} from '@gravity-ui/uikit'; -import fill_ from 'lodash/fill'; - -import {MultiHighlightedText} from '../components/HighlightedText'; -import {ToggleCollapseButton} from '../components/ToggleCollapseButton'; -import {block} from '../constants'; -import i18n from '../i18n'; -import type {UnipikaSettings} from '../unipika/StructuredYsonTypes'; -import type {BlockType, SearchInfo, UnipikaFlattenTreeItem} from '../unipika/flattenUnipika'; -import {defaultUnipikaSettings, unipika} from '../unipika/unipika'; -import {getHightLightedClassName} from '../utils'; - -import ArrowUpRightFromSquareIcon from '@gravity-ui/icons/svgs/arrow-up-right-from-square.svg'; - -interface CellProps { - matched: SearchInfo; - row: UnipikaFlattenTreeItem; - settings?: UnipikaSettings; - collapsedState?: {readonly [key: string]: boolean}; - onToggleCollapse: (path: string) => void; - filter?: string; - index: number; - showFullText: (index: number) => void; - maxValueWidth?: number; -} - -export function Cell(props: CellProps) { - const { - row: {level, open, close, key, value, hasDelimiter, path, collapsed, depth}, - settings, - onToggleCollapse, - matched, - filter, - showFullText, - index, - } = props; - - const handleToggleCollapse = React.useCallback(() => { - if (!path) { - return; - } - onToggleCollapse(path); - }, [path, onToggleCollapse]); - - const handleShowFullText = React.useCallback(() => { - showFullText(index); - }, [showFullText, index]); - - return ( -
- {getLevelOffsetSpaces(level)} - {path && ( - - )} - - {open && } - {depth !== undefined && ( - {i18n('context_items-count', {count: depth})} - )} - {value !== undefined && ( - - )} - {collapsed && depth === undefined && ...} - {close && } - {hasDelimiter && } -
- ); -} - -interface KeyProps { - text: UnipikaFlattenTreeItem['key'] | UnipikaFlattenTreeItem['value']; - settings?: UnipikaSettings; - filter?: string; - matched?: Array; -} - -function Key(props: KeyProps) { - const text: React.ReactNode = renderKeyWithFilter(props); - return text ? ( - - {text} - - - ) : null; -} - -interface ValueProps extends KeyProps { - showFullText?: () => void; - maxValueWidth?: number; -} - -function Value(props: ValueProps) { - return ( - - {renderValueWithFilter(props, block('value', {type: props.text?.$type}))} - - ); -} - -function renderValueWithFilter(props: ValueProps, className: string) { - if ('string' === props.text?.$type) { - return renderStringWithFilter(props, className); - } - return renderWithFilter(props, block('value')); -} - -function renderStringWithFilter(props: ValueProps, className: string) { - const { - text, - settings = defaultUnipikaSettings, - matched = [], - filter, - showFullText, - maxValueWidth = Infinity, - } = props; - - const tmp = unipika.format(text, {...settings, maxStringSize: 10, asHTML: false}); - const length = tmp.length; - const visible = tmp.substring(1, Math.min(length - 1, maxValueWidth + 1)); - const truncated = visible.length < tmp.length - 2; - let hasHiddenMatch = false; - if (truncated) { - for (let i = matched.length - 1; i >= 0; --i) { - if (visible.length < matched[i] + (filter?.length || 0)) { - hasHiddenMatch = true; - break; - } - } - } - return ( - - - {truncated && ( - - {'\u2026'} - - - )} - - ); -} - -function renderKeyWithFilter(props: KeyProps) { - if (!props?.text) { - return null; - } - return renderStringWithFilter(props, block('key')); -} - -function renderWithFilter(props: KeyProps, className: string) { - const {text, filter, settings, matched} = props; - let res: React.ReactNode = null; - if (matched && filter) { - const tmp = unipika.format(text, {...settings, asHTML: false}); - res = ( - - ); - } else { - res = text ? formatValue(text, settings) : undefined; - } - return res ? res : null; -} - -function AdditionalText({text}: {text: string}) { - return {text}; -} - -function OpenClose(props: {type: BlockType; close?: boolean; settings?: UnipikaSettings}) { - const {type, close} = props; - switch (type) { - case 'array': - return ; - case 'object': - return ; - } -} - -function formatValue( - value: UnipikaFlattenTreeItem['key'] | UnipikaFlattenTreeItem['value'], - settings: UnipikaSettings = defaultUnipikaSettings, -) { - const __html = unipika.formatValue(value, {...defaultUnipikaSettings, ...settings}, 0); - return ; -} - -const OFFSETS_BY_LEVEL: {[key: number]: React.ReactNode} = {}; - -function getLevelOffsetSpaces(level: number) { - let res = OFFSETS_BY_LEVEL[level]; - if (!res) { - const __html = fill_(Array(level * 3), ' ').join(''); - res = OFFSETS_BY_LEVEL[level] = ; - } - return res; -} diff --git a/src/components/JsonViewer/components/Filter.tsx b/src/components/JsonViewer/components/Filter.tsx index da6da3aa3f..78fd3fef98 100644 --- a/src/components/JsonViewer/components/Filter.tsx +++ b/src/components/JsonViewer/components/Filter.tsx @@ -19,6 +19,7 @@ interface FilterProps { onPrevMatch?: (_event: unknown, diff?: number) => void; caseSensitive?: boolean; onUpdateCaseSensitive: VoidFunction; + allMatchPaths?: string[]; } export const Filter = React.forwardRef(function Filter( @@ -32,10 +33,12 @@ export const Filter = React.forwardRef(function F onPrevMatch, caseSensitive, onUpdateCaseSensitive, + allMatchPaths, }, ref, ) { - const count = matchedRows.length; + const totalMatches = allMatchPaths?.length || 0; + const count = totalMatches || matchedRows.length; const matchPosition = count ? 1 + (matchIndex % count) : 0; return ( diff --git a/src/components/JsonViewer/components/FullValueDialog.tsx b/src/components/JsonViewer/components/FullValueDialog.tsx deleted file mode 100644 index 77645043a9..0000000000 --- a/src/components/JsonViewer/components/FullValueDialog.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import {Dialog, Flex} from '@gravity-ui/uikit'; - -import {block} from '../constants'; -import i18n from '../i18n'; -import {getHightLightedClassName} from '../utils'; - -import {MultiHighlightedText} from './HighlightedText'; - -interface FullValueDialogProps { - onClose: () => void; - length: number; - text: string; - starts: number[]; -} - -export function FullValueDialog({onClose, text, starts, length}: FullValueDialogProps) { - //if dialog opens from Drawer, outside click should not close Drawer - const handleClickOutside = (e: MouseEvent) => { - e.stopPropagation(); - }; - return ( - - - - - -
- -
-
-
-
- ); -} diff --git a/src/components/JsonViewer/components/HighlightedText.tsx b/src/components/JsonViewer/components/HighlightedText.tsx deleted file mode 100644 index e788145cde..0000000000 --- a/src/components/JsonViewer/components/HighlightedText.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; - -import type {NoStrictEntityMods} from '@bem-react/classname'; - -interface Props { - className: (mods?: NoStrictEntityMods) => string; - text: string; - start?: number; - length?: number; - hasComa?: boolean; - markBegin?: boolean; -} - -export default function HighlightedText({className, text, start, length, hasComa}: Props) { - const comma = hasComa ? : null; - - if (length && typeof start === 'number' && start >= 0 && start < text.length) { - const begin = text.substring(0, start); - const highlighted = text.substring(start, start + length); - const end = text.substring(start + length); - - return ( - - {begin && {begin}} - {highlighted} - {end && {end}} - {comma} - - ); - } - - return ( - - {text} - {comma} - - ); -} - -interface MultiProps extends Omit { - starts: Array; -} - -export function MultiHighlightedText({className, text, starts, length, hasComa}: MultiProps) { - if (!length || !starts.length) { - const comma = hasComa ? : null; - return ( - - {text} - {comma} - - ); - } - - const substrs = []; - for (let i = 0, pos = 0; i < starts.length && pos < text.length; ++i) { - const isLast = i === starts.length - 1; - const to = starts[i] + (isLast ? text.length : length); - const substr = text.substring(pos, to); - if (substr) { - substrs.push( - , - ); - } - pos = to; - } - return {substrs}; -} diff --git a/src/components/JsonViewer/components/ToggleCollapseButton.tsx b/src/components/JsonViewer/components/ToggleCollapseButton.tsx deleted file mode 100644 index 36ae24b780..0000000000 --- a/src/components/JsonViewer/components/ToggleCollapseButton.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import {Button} from '@gravity-ui/uikit'; - -import {block} from '../constants'; -import type {UnipikaFlattenTreeItem} from '../unipika/flattenUnipika'; - -interface ToggleCollapseProps { - collapsed?: boolean; - path?: UnipikaFlattenTreeItem['path']; - onToggle: () => void; -} - -export function ToggleCollapseButton(props: ToggleCollapseProps) { - const {collapsed, onToggle, path} = props; - return ( - - - - ); -} diff --git a/src/components/JsonViewer/i18n/en.json b/src/components/JsonViewer/i18n/en.json index a5fd5c16ac..0cafc89d00 100644 --- a/src/components/JsonViewer/i18n/en.json +++ b/src/components/JsonViewer/i18n/en.json @@ -5,13 +5,7 @@ "action_back": "Back", "description_search": "Search...", "description_matched-rows": "Matched rows", - "description_full-value": "Full value", "context_case-sensitive-search": "Case sensitive search enadled", "context_case-sensitive-search-disabled": "Case sensitive search disabled", - "context_items-count": [ - " {{count}} item ", - " {{count}} items ", - " {{count}} items ", - " {{count}} items " - ] + "description_unknown-error": "Unknown error while parsing data" } diff --git a/src/components/JsonViewer/unipika/StructuredYsonTypes.ts b/src/components/JsonViewer/unipika/StructuredYsonTypes.ts deleted file mode 100644 index 612ee20f9a..0000000000 --- a/src/components/JsonViewer/unipika/StructuredYsonTypes.ts +++ /dev/null @@ -1,62 +0,0 @@ -export type UnipikaSettings = { - nonBreakingIndent?: boolean; - escapeWhitespace?: boolean; - escapeYQLStrings?: boolean; - binaryAsHex?: boolean; - showDecoded?: boolean; - decodeUTF8?: boolean; - format?: string; - indent?: number; - compact?: boolean; - asHTML?: boolean; - break?: boolean; - maxListSize?: number; - maxStringSize?: number; - omitStructNull?: boolean; - treatValAsData?: boolean; - - validateSrcUrl?: (taggedTypeUrl: string) => boolean; - normalizeUrl?: (url?: string) => string; -}; - -interface BaseUnipikaValue { - $attributes?: UnipikaMap['$value']; -} - -export type UnipikaValue = UnipikaMap | UnipikaList | UnipikaString | UnipikaPrimitive; - -export interface UnipikaMap extends BaseUnipikaValue { - $type: 'map'; - $value: Array<[UnipikaMapKey, UnipikaValue]>; -} - -interface UnipikaType extends BaseUnipikaValue { - $type: Type; - $value: Value; -} - -export interface UnipikaMapKey extends BaseUnipikaValue { - $key: true; - $type: 'string'; - $value: string; - $decoded_value?: string; -} - -export interface UnipikaList extends BaseUnipikaValue { - $type: 'list'; - $value: Array; -} - -export type UnipikaString = UnipikaType<'string', string> & { - $decoded_value?: string; -}; - -/** - * Actually there might be another primitive types but at this level - * it is enought to know that there are specific interfaces for 'map', 'list' and 'string', - * and similar structure for all rest types. - */ -export type UnipikaPrimitive = UnipikaType< - 'null' | 'boolean' | 'number' | 'double' | 'int64', - string | number | boolean | null ->; diff --git a/src/components/JsonViewer/unipika/flattenUnipika.ts b/src/components/JsonViewer/unipika/flattenUnipika.ts deleted file mode 100644 index abf6efefd4..0000000000 --- a/src/components/JsonViewer/unipika/flattenUnipika.ts +++ /dev/null @@ -1,388 +0,0 @@ -import type { - UnipikaList, - UnipikaMap, - UnipikaMapKey, - UnipikaPrimitive, - UnipikaSettings, - UnipikaString, - UnipikaValue, -} from './StructuredYsonTypes'; -import {unipika} from './unipika'; - -export type BlockType = 'object' | 'array'; - -export interface UnipikaFlattenTreeItem { - level: number; - open?: BlockType; - close?: BlockType; - depth?: number; - - path?: string; // if present the block is collapsible/expandable - - key?: UnipikaMapKey; - - value?: UnipikaString | UnipikaPrimitive; - - hasDelimiter?: boolean; - - collapsed?: boolean; -} - -export type UnipikaFlattenTree = Array; - -interface FlattenUnipikaOptions { - collapsedState?: CollapsedState; - matchedState?: {}; - settings?: UnipikaSettings; - filter?: string; - caseSensitive?: boolean; -} - -export interface FlattenUnipikaResult { - data: UnipikaFlattenTree; - searchIndex: {[index: number]: SearchInfo}; -} - -export function flattenUnipika( - value: UnipikaValue, - options?: FlattenUnipikaOptions, -): FlattenUnipikaResult { - const collapsedState = options?.collapsedState || {}; - const ctx = { - dst: [], - levels: [], - path: [], - collapsedState, - matchedPath: '', - collapsedPath: '', - }; - flattenUnipikaImpl(value, 0, ctx); - const searchIndex = makeSearchIndex(ctx.dst, options?.filter, { - settings: options?.settings, - caseSensitive: options?.caseSensitive, - }); - return {data: ctx.dst, searchIndex}; -} - -interface LevelInfo { - type: BlockType; - length: number; - currentIndex: number; -} - -interface FlatContext { - readonly collapsedState: CollapsedState; - - dst: UnipikaFlattenTree; - levels: Array; - path: Array; - collapsedPath: string; -} - -export type CollapsedState = {[path: string]: boolean}; - -function isObjectLike(type: BlockType) { - return type === 'object'; -} - -function flattenUnipikaImpl(value: UnipikaValue, level: number, ctx: FlatContext): void { - return flattenUnipikaJsonImpl(value, level, ctx); -} - -function flattenUnipikaJsonImpl(value: UnipikaValue, level = 0, ctx: FlatContext): void { - const beforeAttrs = ctx.dst.length; - const {type} = ctx.levels[ctx.levels.length - 1] || {}; - const itemPathIndex = isObjectLike(type) ? beforeAttrs - 1 : ctx.dst.length; - - const isCollapsed = isPathCollapsed(ctx); - const isContainerType = isValueContainenrType(value); - - let containerSize = 0; - - if (isCollapsed) { - handleCollapsedValue(value, level, ctx); - } else { - const valueLevel = level; - - containerSize = handleValueBlock(isContainerType, value, valueLevel, ctx); - } - - if (isContainerType && containerSize) { - ctx.dst[itemPathIndex].depth = containerSize; - handlePath(ctx, itemPathIndex); // handle 'array item'/'object field' path - } -} - -function handleValueBlock( - isContainerType: boolean, - value: UnipikaValue, - valueLevel: number, - ctx: FlatContext, -) { - let containerSize = 0; - - const isValueCollapsed = isContainerType && isPathCollapsed(ctx); - if (isValueCollapsed) { - handleCollapsedValue(value, valueLevel, ctx); - } else { - switch (value.$type) { - case 'map': - handleUnipikaMap(value, valueLevel, ctx); - containerSize = value.$value.length; - break; - case 'list': - handleUnipikaList(value, valueLevel, ctx); - containerSize = value.$value.length; - break; - case 'string': - handleElement(fromUnipikaString(value, valueLevel), ctx); - break; - default: - handleElement(fromUnipikaPrimitive(value, valueLevel), ctx); - break; - } - } - - return containerSize; -} - -function handleCollapsedValue(value: UnipikaValue, level: number, ctx: FlatContext) { - switch (value.$type) { - case 'map': { - handleCollapsedBlock('object', level, ctx, value.$value.length); - break; - } - case 'list': { - handleCollapsedBlock('array', level, ctx, value.$value.length); - break; - } - } -} - -function handleCollapsedBlock(type: BlockType, level: number, ctx: FlatContext, depth?: number) { - openBlock(type, level, ctx, 0); - const item = ctx.dst[ctx.dst.length - 1]; - item.depth = depth; - item.collapsed = true; - handlePath(ctx, ctx.dst.length - 1); - closeBlock(type, level, ctx); -} - -function handlePath(ctx: FlatContext, index: number) { - if (ctx.collapsedPath.length) { - ctx.dst[index].path = ctx.collapsedPath; - } -} - -function pushPath(path: string, ctx: FlatContext) { - ctx.path.push(path); - ctx.collapsedPath = ctx.collapsedPath.length ? ctx.collapsedPath + '/' + path : path; -} -function popPath(ctx: FlatContext) { - const last = ctx.path.pop(); - if (last !== undefined) { - ctx.collapsedPath = ctx.collapsedPath.substring( - 0, - ctx.collapsedPath.length - last.length - 1, - ); - } -} - -function isValueContainenrType(value: UnipikaValue) { - return value.$type === 'map' || value.$type === 'list'; -} - -function isPathCollapsed(ctx: FlatContext) { - return Boolean(ctx.collapsedState[ctx.collapsedPath]); -} - -function openBlock(type: BlockType, level: number, ctx: FlatContext, length: number): LevelInfo { - const {dst} = ctx; - const last = getLastAsKey(dst); - // for attributes level should be upper than level of key or parent array - if (last?.key && last.level === level) { - last.open = type; - } else { - dst.push({level, open: type}); - } - const levelInfo = {type, length, currentIndex: 0}; - ctx.levels.push(levelInfo); - return levelInfo; -} - -function closeBlock(type: BlockType, level: number, ctx: FlatContext) { - const info = ctx.levels.pop(); - if (info!.type !== type) { - throw new Error( - 'The unipika tree cannot be converted to array, there is some mess with levels ' + - `\n${JSON.stringify({type, level, info, ctx}, null, 2)}`, - ); - } - - const last = ctx.dst[ctx.dst.length - 1]; - const isCloseSameAsOpen = last.level === level && last.open === type; - - const item: UnipikaFlattenTreeItem = isCloseSameAsOpen - ? last - : { - level, - close: type, - }; - - if (isDelimiterRequired(ctx)) { - item.hasDelimiter = true; - } - - if (isCloseSameAsOpen) { - item.close = type; - } else { - ctx.dst.push(item); - } -} - -function isDelimiterRequired(ctx: FlatContext) { - const {length, currentIndex} = ctx.levels[ctx.levels.length - 1] || {}; - return length !== undefined && currentIndex < length - 1; -} - -function handleElement(value: UnipikaFlattenTreeItem, ctx: FlatContext) { - const lastAsKey = getLastAsKey(ctx.dst); - if (lastAsKey && !lastAsKey.open) { - Object.assign(lastAsKey, value, {level: lastAsKey.level}); - } else { - ctx.dst.push(value); - } - - const last = ctx.dst[ctx.dst.length - 1]; - if (isDelimiterRequired(ctx)) { - last.hasDelimiter = true; - } -} - -function getLastAsKey(dst: UnipikaFlattenTree) { - const item = dst[dst.length - 1]; - return item?.key && !item?.close ? item : null; -} - -function handleUnipikaMap(map: UnipikaMap, level: number, ctx: FlatContext) { - const info = openBlock('object', level, ctx, map.$value.length); - handleUnipikaMapImpl(map.$value, level + 1, ctx, info); - closeBlock('object', level, ctx); -} - -function handleUnipikaMapImpl( - items: UnipikaMap['$value'], - level: number, - ctx: FlatContext, - info: LevelInfo, -) { - for (let i = 0; i < items.length; ++i) { - const [key, value] = items[i]; - const keyItem: UnipikaFlattenTreeItem = {key, level: level}; - ctx.dst.push(keyItem); - pushPath(key.$value, ctx); - flattenUnipikaImpl(value, level, ctx); - ++info.currentIndex; - popPath(ctx); - } -} - -function handleUnipikaList(value: UnipikaList, level: number, ctx: FlatContext) { - const {$value: items} = value; - const info = openBlock('array', level, ctx, items.length); - for (let i = 0; i < items.length; ++i) { - pushPath(String(i), ctx); - flattenUnipikaImpl(items[i], level + 1, ctx); - ++info.currentIndex; - popPath(ctx); - } - closeBlock('array', level, ctx); -} - -function fromUnipikaString(value: UnipikaString, level: number): UnipikaFlattenTreeItem { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {$attributes, ...rest} = value; - return {level: level, value: rest}; -} - -function fromUnipikaPrimitive(value: UnipikaPrimitive, level: number): UnipikaFlattenTreeItem { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {$attributes, ...rest} = value; - return {level: level, value: rest}; -} - -interface SearchParams { - settings?: UnipikaSettings; - caseSensitive?: boolean; -} - -export interface SearchInfo { - keyMatch?: Array; - valueMatch?: Array; -} - -type SearchIndex = {[index: number]: SearchInfo}; - -export function makeSearchIndex( - tree: UnipikaFlattenTree, - filter?: string, - options?: SearchParams, -): SearchIndex { - if (!filter) { - return {}; - } - const settings = Object.assign( - {}, - options?.settings, - {asHTML: false}, - ); - const res: SearchIndex = {}; - for (let i = 0; i < tree.length; ++i) { - const {key, value} = tree[i]; - const keyMatch = rowSearchInfo(key, filter, settings, options?.caseSensitive); - const valueMatch = rowSearchInfo(value, filter, settings, options?.caseSensitive); - if (keyMatch || valueMatch) { - res[i] = Object.assign({}, keyMatch && {keyMatch}, valueMatch && {valueMatch}); - } - } - return res; -} - -type SearchValue = undefined | UnipikaMapKey | UnipikaString | UnipikaPrimitive; - -function rowSearchInfo( - v: SearchValue, - filter: string, - settings: UnipikaSettings, - caseSensitive?: boolean, -): Array | undefined { - if (!v) { - return undefined; - } - const res = []; - let tmp = unipika.formatValue(v, settings); - if (!tmp) { - return undefined; - } - tmp = String(tmp); //unipika.formatValue might return an instance of Number - if (v.$type === 'string') { - tmp = tmp.substring(1, tmp.length - 1); // skip quotes - } - let from = 0; - let normolizedFilter = filter; - if (!caseSensitive) { - tmp = tmp.toLowerCase(); - normolizedFilter = filter.toLowerCase(); - } - while (from >= 0 && from < tmp.length) { - if (!caseSensitive) { - } - const index = tmp.indexOf(normolizedFilter, from); - if (-1 === index) { - break; - } - from = index + normolizedFilter.length; - res.push(index); - } - return res.length ? res : undefined; -} diff --git a/src/components/JsonViewer/unipika/unipika.ts b/src/components/JsonViewer/unipika/unipika.ts deleted file mode 100644 index 10e5ba4b02..0000000000 --- a/src/components/JsonViewer/unipika/unipika.ts +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; - -import unipikaFabric from '@gravity-ui/unipika'; - -export const unipika = unipikaFabric({}); - -export const defaultUnipikaSettings = { - asHTML: true, - format: 'json', - compact: false, - escapeWhitespace: true, - showDecoded: true, - binaryAsHex: false, - indent: 2, - decodeUTF8: false, -}; - -export function unipikaConvert(value: unknown) { - if (!value) { - return value; - } - let result; - try { - result = unipika.converters.yson(value, defaultUnipikaSettings); - } catch (e) { - console.error(e); - result = {_error: 'JSON is invalid'}; - } - return result; -} - -export function useUnipikaConvert(value: unknown) { - const memoized = React.useMemo(() => unipikaConvert(value), [value]); - return memoized; -} diff --git a/src/components/JsonViewer/utils.ts b/src/components/JsonViewer/utils.ts deleted file mode 100644 index f2c1a413de..0000000000 --- a/src/components/JsonViewer/utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type {ClassNameList, NoStrictEntityMods} from '@bem-react/classname'; - -import {block} from './constants'; - -export function getHightLightedClassName(mix?: ClassNameList) { - return (mods?: NoStrictEntityMods) => block('filtered', mods, mix); -} diff --git a/src/containers/Configs/Configs.scss b/src/containers/Configs/Configs.scss index 74d9ac842e..046609ac88 100644 --- a/src/containers/Configs/Configs.scss +++ b/src/containers/Configs/Configs.scss @@ -3,7 +3,6 @@ .ydb-configs { --ydb-table-with-controls-layout-controls-height: var(--ydb-configs-controls-height); --configs-padding-bottom: var(--g-spacing-4); - --ydb-json-viewer-toolbar-top: var(--data-table-sticky-header-offset); --ydb-syntax-highlighter-height: calc( var(--ydb-configs-container-height) - var( --ydb-table-with-controls-layout-controls-height diff --git a/src/containers/Configs/components/Config/Config.scss b/src/containers/Configs/components/Config/Config.scss new file mode 100644 index 0000000000..14b736a0cf --- /dev/null +++ b/src/containers/Configs/components/Config/Config.scss @@ -0,0 +1,18 @@ +.ydb-config { + height: var(--ydb-syntax-highlighter-height); + padding: var(--g-spacing-2) 0 0 0; + + background-color: var(--code-background-color); + + &__scroll-container { + overflow: auto; + + max-height: 100%; + padding: 0 var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-3); + scrollbar-width: thin; + scrollbar-color: var(--g-color-line-generic) transparent; + } + &__json-viewer-toolbar { + background-color: var(--code-background-color); + } +} diff --git a/src/containers/Configs/components/Config/Config.tsx b/src/containers/Configs/components/Config/Config.tsx index ae8c0d04e6..ab6cd1792a 100644 --- a/src/containers/Configs/components/Config/Config.tsx +++ b/src/containers/Configs/components/Config/Config.tsx @@ -2,16 +2,21 @@ import React from 'react'; import {ResponseError} from '../../../../components/Errors/ResponseError'; import {JsonViewer} from '../../../../components/JsonViewer/JsonViewer'; -import {useUnipikaConvert} from '../../../../components/JsonViewer/unipika/unipika'; import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'; import {configsApi} from '../../../../store/reducers/configs'; +import {cn} from '../../../../utils/cn'; import {useAutoRefreshInterval} from '../../../../utils/hooks/useAutoRefreshInterval'; import i18n from '../../i18n'; +import './Config.scss'; + +const b = cn('ydb-config'); + interface ConfigProps { database?: string; } export function Config({database}: ConfigProps) { + const scrollContainerRef = React.useRef(null); const [autoRefreshInterval] = useAutoRefreshInterval(); const {currentData, isLoading, error} = configsApi.useGetConfigQuery( {database}, @@ -20,18 +25,22 @@ export function Config({database}: ConfigProps) { const {current} = currentData || {}; - const convertedValue = useUnipikaConvert(current); - const copyText = React.useMemo(() => JSON.stringify(current, null, 4), [current]); return ( {current ? ( - +
+
+ +
+
) : null} {error ? : null}
diff --git a/src/containers/Tenant/Diagnostics/Describe/Describe.tsx b/src/containers/Tenant/Diagnostics/Describe/Describe.tsx index 906d0291dc..f10a049d73 100644 --- a/src/containers/Tenant/Diagnostics/Describe/Describe.tsx +++ b/src/containers/Tenant/Diagnostics/Describe/Describe.tsx @@ -1,6 +1,5 @@ import {ResponseError} from '../../../../components/Errors/ResponseError'; import {JsonViewer} from '../../../../components/JsonViewer/JsonViewer'; -import {useUnipikaConvert} from '../../../../components/JsonViewer/unipika/unipika'; import {Loader} from '../../../../components/Loader'; import {overviewApi} from '../../../../store/reducers/overview/overview'; import {cn} from '../../../../utils/cn'; @@ -14,9 +13,10 @@ interface IDescribeProps { path: string; database: string; databaseFullPath: string; + scrollContainerRef: React.RefObject; } -const Describe = ({path, database, databaseFullPath}: IDescribeProps) => { +const Describe = ({path, database, databaseFullPath, scrollContainerRef}: IDescribeProps) => { const [autoRefreshInterval] = useAutoRefreshInterval(); const {currentData, isFetching, error} = overviewApi.useGetOverviewQuery( @@ -26,8 +26,6 @@ const Describe = ({path, database, databaseFullPath}: IDescribeProps) => { const loading = isFetching && currentData === undefined; - const convertedValue = useUnipikaConvert(currentData); - if (loading) { return ; } @@ -42,13 +40,14 @@ const Describe = ({path, database, databaseFullPath}: IDescribeProps) => { {currentData ? (
) : null} diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index c739761a92..066e88aba9 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -163,7 +163,12 @@ function Diagnostics({additionalTenantProps}: DiagnosticsProps) { } case TENANT_DIAGNOSTICS_TABS_IDS.describe: { return ( - + ); } case TENANT_DIAGNOSTICS_TABS_IDS.hotKeys: { @@ -207,7 +212,7 @@ function Diagnostics({additionalTenantProps}: DiagnosticsProps) { ); } case TENANT_DIAGNOSTICS_TABS_IDS.configs: { - return ; + return ; } case TENANT_DIAGNOSTICS_TABS_IDS.operations: { return ; diff --git a/src/containers/Tenant/Diagnostics/TopicData/FullValue.tsx b/src/containers/Tenant/Diagnostics/TopicData/FullValue.tsx deleted file mode 100644 index 315df70400..0000000000 --- a/src/containers/Tenant/Diagnostics/TopicData/FullValue.tsx +++ /dev/null @@ -1,36 +0,0 @@ -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.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx index 86fb8dde5b..8480e49cb1 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicData.tsx @@ -48,7 +48,7 @@ interface TopicDataProps { path: string; database: string; databaseFullPath: string; - scrollContainerRef: React.RefObject; + scrollContainerRef: React.RefObject; } const PAGINATED_TABLE_LIMIT = 50_000; @@ -309,10 +309,14 @@ export function TopicData({scrollContainerRef, path, database, databaseFullPath} const renderDrawerContent = React.useCallback(() => { return ( - + ); - }, [database, path]); + }, [database, path, scrollContainerRef]); if (error) { return ; diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.scss b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.scss index cbee9a44a9..da203fda0e 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.scss @@ -50,4 +50,9 @@ white-space: pre-wrap; word-break: break-all; } + &__message { + flex-grow: 1; + + margin-bottom: var(--g-spacing-1); + } } diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.tsx index 63ac02bcaf..b36f1f22ae 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/TopicMessageDetails.tsx @@ -24,9 +24,14 @@ import './TopicMessageDetails.scss'; interface TopicMessageDetailsProps { database: string; path: string; + scrollContainerRef: React.RefObject; } -export function TopicMessageDetails({database, path}: TopicMessageDetailsProps) { +export function TopicMessageDetails({ + database, + path, + scrollContainerRef, +}: TopicMessageDetailsProps) { const {selectedPartition, activeOffset} = useTopicDataQueryParams(); const queryParams = React.useMemo(() => { @@ -78,6 +83,7 @@ export function TopicMessageDetails({database, path}: TopicMessageDetailsProps) message={message} offset={messageDetails.Offset} size={messageDetails.OriginalSize} + scrollContainerRef={scrollContainerRef} /> ); }; @@ -117,7 +123,7 @@ export function TopicMessageDetails({database, path}: TopicMessageDetailsProps) } return ( - + {renderMessageGeneralInfo()} {renderMessageMeta()} {renderMessage()} diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicDataSection.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicDataSection.tsx index 4a69181782..b164123a2a 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicDataSection.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicDataSection.tsx @@ -7,6 +7,7 @@ interface TopicDataSectionProps { title: React.ReactNode; className?: string; renderToolbar?: () => React.ReactNode; + scrollContainerRef?: React.RefObject; } export function TopicDataSection({ @@ -14,6 +15,7 @@ export function TopicDataSection({ title, className, renderToolbar, + scrollContainerRef, }: TopicDataSectionProps) { return ( @@ -26,7 +28,9 @@ export function TopicDataSection({ {renderToolbar?.()}
-
{children}
+
+ {children} +
); diff --git a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx index 06ebd57e9e..de2be7edd4 100644 --- a/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx +++ b/src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx @@ -4,7 +4,6 @@ import {ArrowDownToLine} from '@gravity-ui/icons'; import {ActionTooltip, Button, ClipboardButton, Flex, Icon, Text} from '@gravity-ui/uikit'; import {JsonViewer} from '../../../../../../components/JsonViewer/JsonViewer'; -import {unipikaConvert} from '../../../../../../components/JsonViewer/unipika/unipika'; import ShortyString from '../../../../../../components/ShortyString/ShortyString'; import {createAndDownloadFile} from '../../../../../../utils/downloadFile'; import {useTypedSelector} from '../../../../../../utils/hooks'; @@ -20,11 +19,12 @@ interface TopicMessageProps { message: string; offset?: string | number; size?: number; + scrollContainerRef: React.RefObject; } -export function TopicMessage({offset, size, message}: TopicMessageProps) { +export function TopicMessage({offset, size, message, scrollContainerRef}: TopicMessageProps) { const isFullscreen = useTypedSelector((state) => state.fullscreen); - const {preparedMessage, decodedMessage, convertedMessage} = React.useMemo(() => { + const {preparedMessage, decodedMessage, isJson} = React.useMemo(() => { let preparedMessage = message; let decodedMessage = message; try { @@ -45,26 +45,24 @@ export function TopicMessage({offset, size, message}: TopicMessageProps) { console.warn(e); } - let convertedMessage; + let isJson = false; if (typeof preparedMessage === 'object' && safeParseNumber(size) <= UNIPIKA_MAX_SIZE) { - convertedMessage = unipikaConvert(preparedMessage); + isJson = true; } else if (preparedMessage && typeof preparedMessage === 'object') { preparedMessage = JSON.stringify(preparedMessage, null, 2); } - return {preparedMessage, decodedMessage, convertedMessage}; + return {preparedMessage, decodedMessage, isJson}; }, [message, size]); - const isJson = Boolean(convertedMessage); - const messageContent = isJson ? ( ) : (
@@ -102,6 +100,7 @@ export function TopicMessage({offset, size, message}: TopicMessageProps) { title={} renderToolbar={renderToolbar} className={b('message')} + scrollContainerRef={scrollContainerRef} > {messageContent} diff --git a/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.tsx b/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.tsx index 7c2f200a3f..a55dde18b8 100644 --- a/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.tsx +++ b/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.tsx @@ -1,6 +1,8 @@ +import React from 'react'; + import {JsonViewer} from '../../../../../../components/JsonViewer/JsonViewer'; -import {useUnipikaConvert} from '../../../../../../components/JsonViewer/unipika/unipika'; import {cn} from '../../../../../../utils/cn'; +import {useTypedSelector} from '../../../../../../utils/hooks'; import './QueryJSONViewer.scss'; @@ -11,11 +13,16 @@ interface QueryJSONViewerProps { } export function QueryJSONViewer({data}: QueryJSONViewerProps) { - const convertedData = useUnipikaConvert(data); + const scrollRef = React.useRef(null); + const isFullscreen = useTypedSelector((state) => state.fullscreen); return (
-
- +
+
);