From 91bf0d2f3fccd67e1351a2580e93edee090df1f4 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 12 Feb 2025 15:30:49 +0300 Subject: [PATCH 1/3] feat: add new JsonViewer --- package-lock.json | 58 +- package.json | 2 +- src/components/JSONTree/JSONTree.scss | 20 - src/components/JSONTree/JSONTree.tsx | 61 -- src/components/JSONTree/i18n/en.json | 4 - src/components/JsonViewer/JsonViewer.scss | 117 ++++ src/components/JsonViewer/JsonViewer.tsx | 559 ++++++++++++++++++ .../JsonViewer/components/Filter.tsx | 90 +++ .../JsonViewer/components/FullValueDialog.tsx | 34 ++ .../JsonViewer/components/HighlightedText.tsx | 74 +++ src/components/JsonViewer/constants.ts | 2 + src/components/JsonViewer/i18n/en.json | 13 + .../{JSONTree => JsonViewer}/i18n/index.ts | 2 +- .../JsonViewer/unipika/StructuredYsonTypes.ts | 62 ++ .../JsonViewer/unipika/flattenUnipika.ts | 385 ++++++++++++ src/components/JsonViewer/unipika/unipika.ts | 23 + src/components/JsonViewer/utils.ts | 7 + .../Tenant/Diagnostics/Describe/Describe.scss | 6 - .../Tenant/Diagnostics/Describe/Describe.tsx | 31 +- .../Healthcheck/IssuesViewer/IssueTree.scss | 33 +- .../Healthcheck/IssuesViewer/IssueTree.tsx | 10 +- .../Tenant/Query/QueryEditor/QueryEditor.scss | 4 + .../QueryJSONViewer/QueryJSONViewer.scss | 7 +- .../QueryJSONViewer/QueryJSONViewer.tsx | 10 +- src/index.tsx | 1 + src/styles/mixins.scss | 91 --- src/styles/themes.scss | 3 + src/styles/unipika.scss | 12 + src/types/react-json-inspector.d.ts | 21 - src/types/unipika.d.ts | 1 + .../tenant/queryEditor/models/QueryEditor.ts | 2 +- 31 files changed, 1428 insertions(+), 317 deletions(-) delete mode 100644 src/components/JSONTree/JSONTree.scss delete mode 100644 src/components/JSONTree/JSONTree.tsx delete mode 100644 src/components/JSONTree/i18n/en.json create mode 100644 src/components/JsonViewer/JsonViewer.scss create mode 100644 src/components/JsonViewer/JsonViewer.tsx create mode 100644 src/components/JsonViewer/components/Filter.tsx create mode 100644 src/components/JsonViewer/components/FullValueDialog.tsx create mode 100644 src/components/JsonViewer/components/HighlightedText.tsx create mode 100644 src/components/JsonViewer/constants.ts create mode 100644 src/components/JsonViewer/i18n/en.json rename src/components/{JSONTree => JsonViewer}/i18n/index.ts (78%) create mode 100644 src/components/JsonViewer/unipika/StructuredYsonTypes.ts create mode 100644 src/components/JsonViewer/unipika/flattenUnipika.ts create mode 100644 src/components/JsonViewer/unipika/unipika.ts create mode 100644 src/components/JsonViewer/utils.ts create mode 100644 src/styles/unipika.scss delete mode 100644 src/types/react-json-inspector.d.ts create mode 100644 src/types/unipika.d.ts diff --git a/package-lock.json b/package-lock.json index d02cc5a1f8..39d47eca00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@gravity-ui/react-data-table": "^2.1.1", "@gravity-ui/table": "^1.7.0", "@gravity-ui/uikit": "^6.40.0", + "@gravity-ui/unipika": "^5.2.1", "@gravity-ui/websql-autocomplete": "^13.7.0", "@hookform/resolvers": "^3.10.0", "@reduxjs/toolkit": "^2.5.0", @@ -41,7 +42,6 @@ "react-error-boundary": "^4.1.2", "react-helmet-async": "^2.0.5", "react-hook-form": "^7.54.2", - "react-json-inspector": "^7.1.1", "react-monaco-editor": "^0.56.2", "react-redux": "^9.2.0", "react-router-dom": "^5.3.4", @@ -3275,6 +3275,12 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@gravity-ui/unipika": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@gravity-ui/unipika/-/unipika-5.2.1.tgz", + "integrity": "sha512-WUmwSgzU/3NP713vYI8jnAyW0Vq/x7TLAffi2bm9ZPTobYlzELzZIBCshsMz57x7Xdi8QWd+87Eq8erwqBnDNA==", + "license": "MIT" + }, "node_modules/@gravity-ui/websql-autocomplete": { "version": "13.7.0", "resolved": "https://registry.npmjs.org/@gravity-ui/websql-autocomplete/-/websql-autocomplete-13.7.0.tgz", @@ -8851,15 +8857,6 @@ "dev": true, "peer": true }, - "node_modules/create-react-class": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", - "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", - "dependencies": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -9800,24 +9797,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-now": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-1.0.1.tgz", - "integrity": "sha512-yiizelQCqYLUEVT4zqYihOW6Ird7Qyc6fD3Pv5xGxk4+Jz0rsB1dMN2KyNV6jgOHYh5K+sPGCSOknQN4Upa3pg==" - }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, - "node_modules/debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.0.0.tgz", - "integrity": "sha512-4FCfBL8uZFIh3BShn4AlxH4O9F5v+CVriJfiwW8Me/MhO7NqBE5JO5WO48NasbsY9Lww/KYflB79MejA3eKhxw==", - "dependencies": { - "date-now": "1.0.1" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -18238,11 +18222,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/md5-o-matic": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/md5-o-matic/-/md5-o-matic-0.1.1.tgz", - "integrity": "sha512-QBJSFpsedXUl/Lgs4ySdB2XCzUEcJ3ujpbagdZCkRaYIaC0kFnID8jhc84KEiVv6dNFtIrmW7bqow0lDxgJi6A==" - }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -22024,29 +22003,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "node_modules/react-json-inspector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/react-json-inspector/-/react-json-inspector-7.1.1.tgz", - "integrity": "sha512-3CGhjaxObmFv0yV0wy9/x5km5xdniLlgTenyb8HZbzV3AjIQcGrrLkhIhUaAIOnhv4viFTWcYyu2hBDyja1LBw==", - "dependencies": { - "create-react-class": "^15.6.0", - "debounce": "1.0.0", - "md5-o-matic": "^0.1.1", - "object-assign": "2.0.0", - "prop-types": "^15.5.10" - }, - "peerDependencies": { - "react": "^15.0.0" - } - }, - "node_modules/react-json-inspector/node_modules/object-assign": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.0.0.tgz", - "integrity": "sha512-TTVfbeUpQoCNyoOddbCTlMYnK8LsIpLD72jtE6SjwYL2JRr7lskqbMghqdTFp9wHWrZAlDWYUJ1unzPnWWPWQA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-list": { "version": "0.8.18", "resolved": "https://registry.npmjs.org/react-list/-/react-list-0.8.18.tgz", diff --git a/package.json b/package.json index 7c52d8c671..d4bc78d8da 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@gravity-ui/react-data-table": "^2.1.1", "@gravity-ui/table": "^1.7.0", "@gravity-ui/uikit": "^6.40.0", + "@gravity-ui/unipika": "^5.2.1", "@gravity-ui/websql-autocomplete": "^13.7.0", "@hookform/resolvers": "^3.10.0", "@reduxjs/toolkit": "^2.5.0", @@ -43,7 +44,6 @@ "react-error-boundary": "^4.1.2", "react-helmet-async": "^2.0.5", "react-hook-form": "^7.54.2", - "react-json-inspector": "^7.1.1", "react-monaco-editor": "^0.56.2", "react-redux": "^9.2.0", "react-router-dom": "^5.3.4", diff --git a/src/components/JSONTree/JSONTree.scss b/src/components/JSONTree/JSONTree.scss deleted file mode 100644 index 67f4595d4e..0000000000 --- a/src/components/JSONTree/JSONTree.scss +++ /dev/null @@ -1,20 +0,0 @@ -@use '../../styles/mixins.scss'; - -.ydb-json-tree { - position: relative; - - width: 100%; - height: 100%; - &__tree { - @include mixins.json-tree-styles(); - } - &__case { - position: absolute; - top: 0; - left: 308px; - } - - .json-inspector__search { - height: 26px; - } -} diff --git a/src/components/JSONTree/JSONTree.tsx b/src/components/JSONTree/JSONTree.tsx deleted file mode 100644 index dc91f62029..0000000000 --- a/src/components/JSONTree/JSONTree.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import {ActionTooltip, Button, Icon} from '@gravity-ui/uikit'; -import JSONTreeBase from 'react-json-inspector'; - -import {cn} from '../../utils/cn'; -import {CASE_SENSITIVE_JSON_SEARCH} from '../../utils/constants'; -import {useSetting} from '../../utils/hooks'; - -import i18n from './i18n'; - -import FontCaseIcon from '@gravity-ui/icons/svgs/font-case.svg'; - -import './JSONTree.scss'; -import 'react-json-inspector/json-inspector.css'; - -const b = cn('ydb-json-tree'); - -const DEBAUNCE_TIME = 300; - -interface JSONTreeProps extends React.ComponentProps { - search?: false; - treeClassName?: string; -} - -export function JSONTree({treeClassName, search, ...rest}: JSONTreeProps) { - const [caseSensitiveSearch, setCaseSensitiveSearch] = useSetting( - CASE_SENSITIVE_JSON_SEARCH, - false, - ); - return ( -
- - {search !== false && ( - - - - )} -
- ); -} diff --git a/src/components/JSONTree/i18n/en.json b/src/components/JSONTree/i18n/en.json deleted file mode 100644 index bff2f2ae88..0000000000 --- a/src/components/JSONTree/i18n/en.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "context_case-sensitive-search": "Case sensitive search enadled", - "context_case-sensitive-search-disabled": "Case sensitive search disabled" -} diff --git a/src/components/JsonViewer/JsonViewer.scss b/src/components/JsonViewer/JsonViewer.scss new file mode 100644 index 0000000000..f39d51ed6a --- /dev/null +++ b/src/components/JsonViewer/JsonViewer.scss @@ -0,0 +1,117 @@ +@use '../../styles/mixins.scss'; + +.ydb-json-viewer { + --data-table-row-height: 20px; + --toolbar-background-color: var(--g-color-base-background); + + &__toolbar { + position: sticky; + z-index: 2; + top: 0; + left: 0; + + padding-bottom: var(--g-spacing-2); + + 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: -5ex; + } + + &__match-counter { + align-content: center; + + text-wrap: nowrap; + + color: var(--g-color-text-secondary); + } + + &__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); + } + } + } + + &__filter { + max-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(); + } + + &__extra-tools { + margin-left: 1ex; + } + + .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 new file mode 100644 index 0000000000..739b1dfa5b --- /dev/null +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -0,0 +1,559 @@ +import React from 'react'; + +import type * as DT100 from '@gravity-ui/react-data-table'; +import DataTable from '@gravity-ui/react-data-table'; +import {ActionTooltip, Button, Flex, Icon} from '@gravity-ui/uikit'; +import fill_ from 'lodash/fill'; + +import {CASE_SENSITIVE_JSON_SEARCH} from '../../utils/constants'; +import {useSetting} from '../../utils/hooks'; + +import {Filter} from './components/Filter'; +import {FullValueDialog} from './components/FullValueDialog'; +import {MultiHighlightedText} from './components/HighlightedText'; +import {block} from './constants'; +import i18n from './i18n'; +import type {UnipikaSettings, UnipikaValue} from './unipika/StructuredYsonTypes'; +import {flattenUnipika} from './unipika/flattenUnipika'; +import type { + BlockType, + CollapsedState, + FlattenUnipikaResult, + SearchInfo, + UnipikaFlattenTreeItem, +} from './unipika/flattenUnipika'; +import {defaultUnipikaSettings, unipika} from './unipika/unipika'; +import {getHightLightedClassName} from './utils'; + +import ArrowDownToLineIcon from '@gravity-ui/icons/svgs/arrow-down-to-line.svg'; +import ArrowUpFromLineIcon from '@gravity-ui/icons/svgs/arrow-up-from-line.svg'; +import ArrowUpRightFromSquareIcon from '@gravity-ui/icons/svgs/arrow-up-right-from-square.svg'; + +import './JsonViewer.scss'; + +interface JsonViewerProps { + value: UnipikaValue; + unipikaSettings?: UnipikaSettings; + extraTools?: React.ReactNode; + tableSettings?: DT100.Settings; + search?: boolean; + collapsedInitially?: boolean; +} + +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: 100, +}; + +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), + }, + ); +} + +export function JsonViewer({ + tableSettings, + value, + unipikaSettings, + search = true, + extraTools, + collapsedInitially, +}: JsonViewerProps) { + const [caseSensitiveSearch] = useSetting(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>, + cb?: () => void, + ) => { + const { + collapsedState: newCollapsedState, + matchIndex: newMatchIndex, + filter: newFilter, + } = changedState; + + if (newCollapsedState !== undefined) { + setCollapsedState(newCollapsedState); + } + if (newMatchIndex !== undefined) { + setMatchIndex(newMatchIndex); + } + if (newFilter !== undefined) { + setFilter(newFilter); + } + setState(calculateState(value, newCollapsedState ?? collapsedState, newFilter ?? filter)); + + cb?.(); + }; + + const renderTable = () => { + const columns: Array> = [ + { + name: 'content', + render: renderCell, + header: null, + }, + ]; + + const { + flattenResult: {data}, + } = state; + + return ( +
+ +
+ ); + }; + + const rowClassName = ({key}: UnipikaFlattenTreeItem) => { + const k = key?.$decoded_value ?? ''; + return block('row', {key: asModifier(k)}); + }; + + 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 renderToolbar = () => { + return ( + + + + + + + + + + {search && ( + + )} + {extraTools} + + ); + }; + + 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}); + + return ( + value && ( + + ) + ); + }; + + return ( +
+ {renderToolbar()} + {renderTable()} + {renderFullValueModal()} +
+ ); +} + +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 * 4), ' ').join(''); + res = OFFSETS_BY_LEVEL[level] = ; + } + return res; +} + +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; +} + +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; +} + +function Value(props: ValueProps) { + return ( + + {renderValueWithFilter(props, block('value', {type: props.text?.$type}))} + + ); +} + +function asModifier(path = '') { + return path.replace(/[^-\w\d]/g, '_'); +} + +function renderValueWithFilter(props: ValueProps, className: string) { + if ('string' === props.text?.$type) { + return renderStringWithFilter(props, className, 100); + } + return renderWithFilter(props, block('value')); +} + +function renderStringWithFilter(props: ValueProps, className: string, maxWidth = Infinity) { + const {text, settings = defaultUnipikaSettings, matched = [], filter, showFullText} = props; + const tmp = unipika.format(text, {...settings, asHTML: false}); + const visible = tmp.substr(1, Math.min(tmp.length - 2, maxWidth)); + 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 SlaveText({text}: {text: string}) { + return {text}; +} + +function OpenClose(props: { + type: BlockType; + close?: boolean; + settings: JsonViewerProps['unipikaSettings']; +}) { + const {type, close} = props; + switch (type) { + case 'array': + return ; + case 'object': + return ; + } +} + +interface ToggleCollapseProps { + collapsed?: boolean; + path?: UnipikaFlattenTreeItem['path']; + onToggle: () => void; +} + +function ToggleCollapseButton(props: ToggleCollapseProps) { + const {collapsed, onToggle, path} = props; + return ( + + + + ); +} + +function formatValue( + value: UnipikaFlattenTreeItem['key'] | UnipikaFlattenTreeItem['value'], + settings: UnipikaSettings = defaultUnipikaSettings, +) { + const __html = unipika.formatValue(value, {...defaultUnipikaSettings, ...settings}, 0); + return ; +} diff --git a/src/components/JsonViewer/components/Filter.tsx b/src/components/JsonViewer/components/Filter.tsx new file mode 100644 index 0000000000..ff2a064725 --- /dev/null +++ b/src/components/JsonViewer/components/Filter.tsx @@ -0,0 +1,90 @@ +import React from 'react'; + +import {ActionTooltip, Button, Flex, Icon, TextInput} from '@gravity-ui/uikit'; + +import {CASE_SENSITIVE_JSON_SEARCH} from '../../../utils/constants'; +import {useSetting} from '../../../utils/hooks'; +import {block} from '../constants'; +import i18n from '../i18n'; + +import ChevronDownIcon from '@gravity-ui/icons/svgs/chevron-down.svg'; +import ChevronUpIcon from '@gravity-ui/icons/svgs/chevron-up.svg'; +import FontCaseIcon from '@gravity-ui/icons/svgs/font-case.svg'; + +interface FilterProps { + matchIndex: number; + matchedRows: number[]; + value: string; + onUpdate: (value: string) => void; + onKeyDown?: (event: React.KeyboardEvent) => void; + onNextMatch?: (_event: unknown, diff?: number) => void; + onPrevMatch?: (_event: unknown, diff?: number) => void; +} + +export const Filter = React.forwardRef(function Filter( + {matchIndex, matchedRows, value, onUpdate, onKeyDown, onNextMatch, onPrevMatch}, + ref, +) { + const [caseSensitiveSearch, setCaseSensitiveSearch] = useSetting( + CASE_SENSITIVE_JSON_SEARCH, + false, + ); + const count = matchedRows.length; + const matchPosition = count ? 1 + (matchIndex % count) : 0; + return ( + + + + + } + /> + + + + + + {matchPosition} / {count || 0} + + + ); +}); diff --git a/src/components/JsonViewer/components/FullValueDialog.tsx b/src/components/JsonViewer/components/FullValueDialog.tsx new file mode 100644 index 0000000000..90b4647d91 --- /dev/null +++ b/src/components/JsonViewer/components/FullValueDialog.tsx @@ -0,0 +1,34 @@ +import {Dialog, Flex} from '@gravity-ui/uikit'; + +import {block} from '../constants'; +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) { + return ( + + + + + +
+ +
+
+
+
+ ); +} diff --git a/src/components/JsonViewer/components/HighlightedText.tsx b/src/components/JsonViewer/components/HighlightedText.tsx new file mode 100644 index 0000000000..8f97086534 --- /dev/null +++ b/src/components/JsonViewer/components/HighlightedText.tsx @@ -0,0 +1,74 @@ +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! > 0 && start! >= 0 && start! < text.length) { + const begin = text.substr(0, start); + const highlighted = text.substr(start!, length); + const end = text.substr(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/constants.ts b/src/components/JsonViewer/constants.ts new file mode 100644 index 0000000000..bad71d59c5 --- /dev/null +++ b/src/components/JsonViewer/constants.ts @@ -0,0 +1,2 @@ +import {cn} from '../../utils/cn'; +export const block = cn('ydb-json-viewer'); diff --git a/src/components/JsonViewer/i18n/en.json b/src/components/JsonViewer/i18n/en.json new file mode 100644 index 0000000000..ea272262f0 --- /dev/null +++ b/src/components/JsonViewer/i18n/en.json @@ -0,0 +1,13 @@ +{ + "action_collapse-all": "Collapse all", + "action_expand-all": "Expand all", + "description_search": "Search...", + "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 " + ] +} diff --git a/src/components/JSONTree/i18n/index.ts b/src/components/JsonViewer/i18n/index.ts similarity index 78% rename from src/components/JSONTree/i18n/index.ts rename to src/components/JsonViewer/i18n/index.ts index c475ebffc7..b478faae70 100644 --- a/src/components/JSONTree/i18n/index.ts +++ b/src/components/JsonViewer/i18n/index.ts @@ -2,6 +2,6 @@ import {registerKeysets} from '../../../utils/i18n'; import en from './en.json'; -const COMPONENT = 'ydb-json-tree'; +const COMPONENT = 'ydb-json-viewer'; export default registerKeysets(COMPONENT, {en}); diff --git a/src/components/JsonViewer/unipika/StructuredYsonTypes.ts b/src/components/JsonViewer/unipika/StructuredYsonTypes.ts new file mode 100644 index 0000000000..612ee20f9a --- /dev/null +++ b/src/components/JsonViewer/unipika/StructuredYsonTypes.ts @@ -0,0 +1,62 @@ +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 new file mode 100644 index 0000000000..a3ff270226 --- /dev/null +++ b/src/components/JsonViewer/unipika/flattenUnipika.ts @@ -0,0 +1,385 @@ +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.substr(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 new file mode 100644 index 0000000000..f4fe555889 --- /dev/null +++ b/src/components/JsonViewer/unipika/unipika.ts @@ -0,0 +1,23 @@ +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, +}; + +export function unipikaConvert(value: unknown) { + return unipika.converters.yson(value, defaultUnipikaSettings); +} + +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 new file mode 100644 index 0000000000..f2c1a413de --- /dev/null +++ b/src/components/JsonViewer/utils.ts @@ -0,0 +1,7 @@ +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/Tenant/Diagnostics/Describe/Describe.scss b/src/containers/Tenant/Diagnostics/Describe/Describe.scss index b56265cdc5..96b293a062 100644 --- a/src/containers/Tenant/Diagnostics/Describe/Describe.scss +++ b/src/containers/Tenant/Diagnostics/Describe/Describe.scss @@ -9,14 +9,8 @@ position: relative; display: flex; - overflow: auto; flex: 0 0 auto; padding: 0 20px 20px 0; } - - &__copy { - position: absolute; - left: 340px; - } } diff --git a/src/containers/Tenant/Diagnostics/Describe/Describe.tsx b/src/containers/Tenant/Diagnostics/Describe/Describe.tsx index 048fde4a5a..fa6fcc03b0 100644 --- a/src/containers/Tenant/Diagnostics/Describe/Describe.tsx +++ b/src/containers/Tenant/Diagnostics/Describe/Describe.tsx @@ -2,7 +2,8 @@ import {ClipboardButton} from '@gravity-ui/uikit'; import {shallowEqual} from 'react-redux'; import {ResponseError} from '../../../../components/Errors/ResponseError'; -import {JSONTree} from '../../../../components/JSONTree/JSONTree'; +import {JsonViewer} from '../../../../components/JsonViewer/JsonViewer'; +import {useUnipikaConvert} from '../../../../components/JsonViewer/unipika/unipika'; import {Loader} from '../../../../components/Loader'; import { selectSchemaMergedChildrenPaths, @@ -17,8 +18,6 @@ import './Describe.scss'; const b = cn('ydb-describe'); -const expandMap = new Map(); - interface IDescribeProps { path: string; database: string; @@ -58,6 +57,8 @@ const Describe = ({path, database, type}: IDescribeProps) => { } } + const convertedValue = useUnipikaConvert(preparedDescribeData); + if (loading || (isEntityWithMergedImpl && !mergedChildrenPaths)) { return ; } @@ -71,20 +72,16 @@ const Describe = ({path, database, type}: IDescribeProps) => { {error ? : null} {preparedDescribeData ? (
- { - const newValue = !(expandMap.get(path) || false); - expandMap.set(path, newValue); - }} - isExpanded={(keypath) => { - return expandMap.get(keypath) || false; - }} - /> - + } + search + collapsedInitially />
) : null} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.scss b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.scss index 2abe05bd65..ec576b391b 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.scss +++ b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.scss @@ -20,38 +20,9 @@ border-radius: 4px; background: var(--g-color-base-generic); - } - - &__inspector { - :not(.json-inspector__leaf_expanded).json-inspector__leaf_composite:before { - content: ''; - } - .json-inspector__leaf_expanded.json-inspector__leaf_composite:before { - content: ''; - } - - & .json-inspector { - &__line:hover:after { - background: transparent; - } - &__show-original:hover:after, - &__show-original:hover:before { - color: transparent; - } - - &__value_helper { - display: none; - } - - &__value { - overflow: hidden; - - word-break: break-all; - & > span { - user-select: all; - } - } + .ydb-json-viewer { + --toolbar-background-color: var(--g-color-base-simple-hover-solid); } } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.tsx index b97a35e28c..94ab34b9c6 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/Healthcheck/IssuesViewer/IssueTree.tsx @@ -3,7 +3,8 @@ import React from 'react'; import _omit from 'lodash/omit'; import {TreeView} from 'ydb-ui-components'; -import {JSONTree} from '../../../../../../components/JSONTree/JSONTree'; +import {JsonViewer} from '../../../../../../components/JsonViewer/JsonViewer'; +import {unipikaConvert} from '../../../../../../components/JsonViewer/unipika/unipika'; import type {IssuesTree} from '../../../../../../store/reducers/healthcheckInfo/types'; import {hcStatusToColorFlag} from '../../../../../../store/reducers/healthcheckInfo/utils'; import {cn} from '../../../../../../utils/cn'; @@ -28,12 +29,7 @@ const IssueTree = ({issueTree}: IssuesViewerProps) => { return (
- true} - treeClassName={b('inspector')} - /> +
); }, []); diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.scss b/src/containers/Tenant/Query/QueryEditor/QueryEditor.scss index 59c9a23793..40430d5285 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.scss +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.scss @@ -11,6 +11,10 @@ @include mixins.query-data-table; + & .data-table__box .data-table__table-wrapper { + padding-bottom: 0; + } + &__monaco { position: relative; diff --git a/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.scss b/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.scss index c3bf594eda..73b99f439e 100644 --- a/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.scss +++ b/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.scss @@ -1,9 +1,12 @@ .ydb-query-json-viewer { - &__inspector { + width: 100%; + height: 100%; + padding: 15px 0; + &__tree { overflow-y: auto; width: 100%; height: 100%; - padding: 15px 10px; + padding: 0 10px; } } diff --git a/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.tsx b/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.tsx index 12bf8f29e3..7c2f200a3f 100644 --- a/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.tsx +++ b/src/containers/Tenant/Query/QueryResult/components/QueryJSONViewer/QueryJSONViewer.tsx @@ -1,4 +1,5 @@ -import {JSONTree} from '../../../../../../components/JSONTree/JSONTree'; +import {JsonViewer} from '../../../../../../components/JsonViewer/JsonViewer'; +import {useUnipikaConvert} from '../../../../../../components/JsonViewer/unipika/unipika'; import {cn} from '../../../../../../utils/cn'; import './QueryJSONViewer.scss'; @@ -10,9 +11,12 @@ interface QueryJSONViewerProps { } export function QueryJSONViewer({data}: QueryJSONViewerProps) { + const convertedData = useUnipikaConvert(data); return ( -
- true} /> +
+
+ +
); } diff --git a/src/index.tsx b/src/index.tsx index ba02628f1a..a0f0dcc911 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,6 +9,7 @@ import reportWebVitals from './reportWebVitals'; import {history, store} from './store/defaultStore'; import './styles/themes.scss'; +import './styles/unipika.scss'; import './index.css'; async function render() { diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss index b7883909c8..ea0bb7d131 100644 --- a/src/styles/mixins.scss +++ b/src/styles/mixins.scss @@ -203,97 +203,6 @@ } } -@mixin json-tree-styles { - width: 100%; - - word-wrap: break-word; - - // stylelint-disable - font-family: var(--g-font-family-monospace) !important; - font-size: var(--g-text-code-1-font-size) !important; - line-height: var(--g-text-code-1-line-height) !important; - // stylelint-enable - - .json-inspector__leaf_composite:before { - position: absolute; - left: 20px; - - font-size: 9px; - - color: var(--g-color-text-secondary); - } - .json-inspector__leaf_composite.json-inspector__leaf_root:before { - left: 0; - } - :not(.json-inspector__leaf_expanded).json-inspector__leaf_composite:before { - content: '[+]'; - } - .json-inspector__leaf_expanded.json-inspector__leaf_composite:before { - content: '[-]'; - } - - & .json-inspector { - &__key { - color: var(--g-color-text-misc); - } - &__leaf { - position: relative; - - padding-left: 20px; - } - &__leaf_root { - padding-left: 0; - } - &__line { - padding-left: 20px; - } - &__toolbar { - width: 300px; - margin-bottom: 10px; - - border: 1px solid var(--g-color-line-generic); - border-radius: 4px; - } - &__search { - box-sizing: border-box; - width: 300px; - height: 28px; - margin: 0; - padding: 0; - - font-family: var(--g-text-body-font-family); - font-size: 13px; - vertical-align: top; - - color: var(--g-color-text-primary); - border: 0 solid transparent; - border-width: 0 8px; - border-right-width: 22px; - outline: 0; - background: none; - } - &__value { - &_helper { - color: var(--g-color-text-secondary); - } - } - &__line:hover:after { - background: var(--g-color-base-simple-hover); - } - &__show-original:before { - color: var(--g-color-text-secondary); - } - &__show-original:hover:after, - &__show-original:hover:before { - color: var(--g-color-text-primary); - } - } - - & .json-inspector__leaf.json-inspector__leaf_root.json-inspector__leaf_composite { - max-width: calc(100% - 50px); - } -} - @mixin tabs-wrapper-styles() { --g-tabs-border-width: 0; diff --git a/src/styles/themes.scss b/src/styles/themes.scss index 52b0320dcf..8b749bcc01 100644 --- a/src/styles/themes.scss +++ b/src/styles/themes.scss @@ -2,6 +2,7 @@ @use './themes/light-hc'; @use './themes/dark'; @use './themes/dark-hc'; +@use '~@gravity-ui/unipika/styles/unipika.scss'; // Override @gravity-ui/uikit color palette with our own colors @@ -54,6 +55,7 @@ --g-color-text-link: var(--g-color-private-blue-550-solid); --g-color-text-link-hover: var(--g-color-private-blue-700-solid); @include dark.colors-private-dark; + @include unipika.unipika-dark; } &_theme_dark-hc { @@ -69,5 +71,6 @@ --g-color-text-link: var(--g-color-private-blue-650-solid); --g-color-text-link-hover: var(--g-color-private-blue-800-solid); @include dark-hc.colors-private-dark-hc; + @include unipika.unipika-dark; } } diff --git a/src/styles/unipika.scss b/src/styles/unipika.scss new file mode 100644 index 0000000000..4ad37df8b4 --- /dev/null +++ b/src/styles/unipika.scss @@ -0,0 +1,12 @@ +.g-root { + .unipika { + font-family: var(--g-font-family-monospace); + + &-wrapper & { + margin: 0; + padding: 0; + + border: 0; + } + } +} diff --git a/src/types/react-json-inspector.d.ts b/src/types/react-json-inspector.d.ts deleted file mode 100644 index 62b678c079..0000000000 --- a/src/types/react-json-inspector.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -declare module 'react-json-inspector' { - // This typing is sufficient for current use cases, but some types are incompelete - class JSONTree extends React.Component<{ - data?: object; - search?: boolean; - searchOptions?: { - debounceTime?: number; - }; - onClick?: ({path: string, key: string, value: object}) => void; - validateQuery?: (query: string) => boolean; - isExpanded?: (keypath: string) => boolean; - filterOptions?: { - cacheResults?: bool; - ignoreCase?: bool; - }; - query?: string; - verboseShowOriginal?: boolean; - className?: string; - }> {} - export default JSONTree; -} diff --git a/src/types/unipika.d.ts b/src/types/unipika.d.ts new file mode 100644 index 0000000000..6268d65a31 --- /dev/null +++ b/src/types/unipika.d.ts @@ -0,0 +1 @@ +declare module '@gravity-ui/unipika'; diff --git a/tests/suites/tenant/queryEditor/models/QueryEditor.ts b/tests/suites/tenant/queryEditor/models/QueryEditor.ts index ce3aab38dd..98f38a7abc 100644 --- a/tests/suites/tenant/queryEditor/models/QueryEditor.ts +++ b/tests/suites/tenant/queryEditor/models/QueryEditor.ts @@ -133,7 +133,7 @@ export class QueryEditor { case ExplainResultType.Schema: return resultArea.locator('.ydb-query-explain-graph__canvas-container'); case ExplainResultType.JSON: - return resultArea.locator('.ydb-query-json-viewer__inspector'); + return resultArea.locator('.ydb-query-json-viewer__tree'); case ExplainResultType.AST: return resultArea.locator('.ydb-query-ast'); } From 85e2b8005f94373245ad274bc0eda56531d0419f Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 14 Feb 2025 15:07:28 +0300 Subject: [PATCH 2/3] fix: review --- src/components/JsonViewer/JsonViewer.scss | 4 +- src/components/JsonViewer/JsonViewer.tsx | 230 +----------------- src/components/JsonViewer/components/Cell.tsx | 212 ++++++++++++++++ .../JsonViewer/components/Filter.tsx | 2 +- .../components/ToggleCollapseButton.tsx | 23 ++ src/components/JsonViewer/unipika/unipika.ts | 1 + 6 files changed, 243 insertions(+), 229 deletions(-) create mode 100644 src/components/JsonViewer/components/Cell.tsx create mode 100644 src/components/JsonViewer/components/ToggleCollapseButton.tsx diff --git a/src/components/JsonViewer/JsonViewer.scss b/src/components/JsonViewer/JsonViewer.scss index f39d51ed6a..1286148b1d 100644 --- a/src/components/JsonViewer/JsonViewer.scss +++ b/src/components/JsonViewer/JsonViewer.scss @@ -36,7 +36,7 @@ position: absolute; margin-top: -2px; - margin-left: -5ex; + margin-left: -3ex; } &__match-counter { @@ -72,7 +72,7 @@ } &__filter { - max-width: 300px; + width: 300px; } &__filtered { diff --git a/src/components/JsonViewer/JsonViewer.tsx b/src/components/JsonViewer/JsonViewer.tsx index 739b1dfa5b..192a9630a3 100644 --- a/src/components/JsonViewer/JsonViewer.tsx +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -3,31 +3,27 @@ import React from 'react'; import type * as DT100 from '@gravity-ui/react-data-table'; import DataTable from '@gravity-ui/react-data-table'; import {ActionTooltip, Button, Flex, Icon} from '@gravity-ui/uikit'; -import fill_ from 'lodash/fill'; import {CASE_SENSITIVE_JSON_SEARCH} from '../../utils/constants'; import {useSetting} from '../../utils/hooks'; +import {Cell} from './components/Cell'; import {Filter} from './components/Filter'; import {FullValueDialog} from './components/FullValueDialog'; -import {MultiHighlightedText} from './components/HighlightedText'; import {block} from './constants'; import i18n from './i18n'; import type {UnipikaSettings, UnipikaValue} from './unipika/StructuredYsonTypes'; import {flattenUnipika} from './unipika/flattenUnipika'; import type { - BlockType, CollapsedState, FlattenUnipikaResult, SearchInfo, UnipikaFlattenTreeItem, } from './unipika/flattenUnipika'; -import {defaultUnipikaSettings, unipika} from './unipika/unipika'; -import {getHightLightedClassName} from './utils'; +import {unipika} from './unipika/unipika'; import ArrowDownToLineIcon from '@gravity-ui/icons/svgs/arrow-down-to-line.svg'; import ArrowUpFromLineIcon from '@gravity-ui/icons/svgs/arrow-up-from-line.svg'; -import ArrowUpRightFromSquareIcon from '@gravity-ui/icons/svgs/arrow-up-right-from-square.svg'; import './JsonViewer.scss'; @@ -268,12 +264,12 @@ export function JsonViewer({ - - @@ -336,224 +332,6 @@ export function JsonViewer({ ); } -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 * 4), ' ').join(''); - res = OFFSETS_BY_LEVEL[level] = ; - } - return res; -} - -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; -} - -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; -} - -function Value(props: ValueProps) { - return ( - - {renderValueWithFilter(props, block('value', {type: props.text?.$type}))} - - ); -} - function asModifier(path = '') { return path.replace(/[^-\w\d]/g, '_'); } - -function renderValueWithFilter(props: ValueProps, className: string) { - if ('string' === props.text?.$type) { - return renderStringWithFilter(props, className, 100); - } - return renderWithFilter(props, block('value')); -} - -function renderStringWithFilter(props: ValueProps, className: string, maxWidth = Infinity) { - const {text, settings = defaultUnipikaSettings, matched = [], filter, showFullText} = props; - const tmp = unipika.format(text, {...settings, asHTML: false}); - const visible = tmp.substr(1, Math.min(tmp.length - 2, maxWidth)); - 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 SlaveText({text}: {text: string}) { - return {text}; -} - -function OpenClose(props: { - type: BlockType; - close?: boolean; - settings: JsonViewerProps['unipikaSettings']; -}) { - const {type, close} = props; - switch (type) { - case 'array': - return ; - case 'object': - return ; - } -} - -interface ToggleCollapseProps { - collapsed?: boolean; - path?: UnipikaFlattenTreeItem['path']; - onToggle: () => void; -} - -function ToggleCollapseButton(props: ToggleCollapseProps) { - const {collapsed, onToggle, path} = props; - return ( - - - - ); -} - -function formatValue( - value: UnipikaFlattenTreeItem['key'] | UnipikaFlattenTreeItem['value'], - settings: UnipikaSettings = defaultUnipikaSettings, -) { - const __html = unipika.formatValue(value, {...defaultUnipikaSettings, ...settings}, 0); - return ; -} diff --git a/src/components/JsonViewer/components/Cell.tsx b/src/components/JsonViewer/components/Cell.tsx new file mode 100644 index 0000000000..5ee2e99bb4 --- /dev/null +++ b/src/components/JsonViewer/components/Cell.tsx @@ -0,0 +1,212 @@ +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; +} + +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; +} + +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, 100); + } + return renderWithFilter(props, block('value')); +} + +function renderStringWithFilter(props: ValueProps, className: string, maxWidth = Infinity) { + const {text, settings = defaultUnipikaSettings, matched = [], filter, showFullText} = props; + const tmp = unipika.format(text, {...settings, asHTML: false}); + const visible = tmp.substr(1, Math.min(tmp.length - 2, maxWidth)); + 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 SlaveText({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 ff2a064725..51ba1613c5 100644 --- a/src/components/JsonViewer/components/Filter.tsx +++ b/src/components/JsonViewer/components/Filter.tsx @@ -53,7 +53,7 @@ export const Filter = React.forwardRef(function F } > + + ); +} diff --git a/src/components/JsonViewer/unipika/unipika.ts b/src/components/JsonViewer/unipika/unipika.ts index f4fe555889..7066ba83d4 100644 --- a/src/components/JsonViewer/unipika/unipika.ts +++ b/src/components/JsonViewer/unipika/unipika.ts @@ -11,6 +11,7 @@ export const defaultUnipikaSettings = { escapeWhitespace: true, showDecoded: true, binaryAsHex: false, + indent: 2, }; export function unipikaConvert(value: unknown) { From 23ea45f65cb65fa055349447b0424feaf27ccb30 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Mon, 17 Feb 2025 13:50:21 +0300 Subject: [PATCH 3/3] fix: review --- src/components/JsonViewer/JsonViewer.tsx | 11 +---------- src/components/JsonViewer/components/Cell.tsx | 15 ++++++++------- src/components/JsonViewer/components/Filter.tsx | 6 +++--- .../JsonViewer/components/FullValueDialog.tsx | 3 ++- .../JsonViewer/components/HighlightedText.tsx | 8 ++++---- src/components/JsonViewer/i18n/en.json | 4 ++++ .../JsonViewer/unipika/flattenUnipika.ts | 5 ++++- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/components/JsonViewer/JsonViewer.tsx b/src/components/JsonViewer/JsonViewer.tsx index 192a9630a3..554acb4579 100644 --- a/src/components/JsonViewer/JsonViewer.tsx +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -198,17 +198,12 @@ export function JsonViewer({ data={data} theme={'yson'} settings={normalizedTableSettings} - rowClassName={rowClassName} + rowClassName={() => block('row')} />
); }; - const rowClassName = ({key}: UnipikaFlattenTreeItem) => { - const k = key?.$decoded_value ?? ''; - return block('row', {key: asModifier(k)}); - }; - const onExpandAll = () => { updateState({collapsedState: {}}, () => { onNextMatch(null, 0); @@ -331,7 +326,3 @@ export function JsonViewer({ ); } - -function asModifier(path = '') { - return path.replace(/[^-\w\d]/g, '_'); -} diff --git a/src/components/JsonViewer/components/Cell.tsx b/src/components/JsonViewer/components/Cell.tsx index 5ee2e99bb4..9a40ca7118 100644 --- a/src/components/JsonViewer/components/Cell.tsx +++ b/src/components/JsonViewer/components/Cell.tsx @@ -73,7 +73,7 @@ export function Cell(props: CellProps) { )} {collapsed && depth === undefined && ...} {close && } - {hasDelimiter && } + {hasDelimiter && } ); } @@ -90,7 +90,7 @@ function Key(props: KeyProps) { return text ? ( {text} - + ) : null; } @@ -117,7 +117,8 @@ function renderValueWithFilter(props: ValueProps, className: string) { function renderStringWithFilter(props: ValueProps, className: string, maxWidth = Infinity) { const {text, settings = defaultUnipikaSettings, matched = [], filter, showFullText} = props; const tmp = unipika.format(text, {...settings, asHTML: false}); - const visible = tmp.substr(1, Math.min(tmp.length - 2, maxWidth)); + const length = tmp.length; + const visible = tmp.substring(1, Math.min(length - 1, maxWidth + 1)); const truncated = visible.length < tmp.length - 2; let hasHiddenMatch = false; if (truncated) { @@ -178,17 +179,17 @@ function renderWithFilter(props: KeyProps, className: string) { return res ? res : null; } -function SlaveText({text}: {text: string}) { - return {text}; +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 ; + return ; case 'object': - return ; + return ; } } diff --git a/src/components/JsonViewer/components/Filter.tsx b/src/components/JsonViewer/components/Filter.tsx index 51ba1613c5..87b732b3b0 100644 --- a/src/components/JsonViewer/components/Filter.tsx +++ b/src/components/JsonViewer/components/Filter.tsx @@ -66,7 +66,7 @@ export const Filter = React.forwardRef(function F - + {matchPosition} / {count || 0} diff --git a/src/components/JsonViewer/components/FullValueDialog.tsx b/src/components/JsonViewer/components/FullValueDialog.tsx index 90b4647d91..a71ac911f7 100644 --- a/src/components/JsonViewer/components/FullValueDialog.tsx +++ b/src/components/JsonViewer/components/FullValueDialog.tsx @@ -1,6 +1,7 @@ import {Dialog, Flex} from '@gravity-ui/uikit'; import {block} from '../constants'; +import i18n from '../i18n'; import {getHightLightedClassName} from '../utils'; import {MultiHighlightedText} from './HighlightedText'; @@ -15,7 +16,7 @@ interface FullValueDialogProps { export function FullValueDialog({onClose, text, starts, length}: FullValueDialogProps) { return ( - + diff --git a/src/components/JsonViewer/components/HighlightedText.tsx b/src/components/JsonViewer/components/HighlightedText.tsx index 8f97086534..e788145cde 100644 --- a/src/components/JsonViewer/components/HighlightedText.tsx +++ b/src/components/JsonViewer/components/HighlightedText.tsx @@ -14,10 +14,10 @@ interface Props { export default function HighlightedText({className, text, start, length, hasComa}: Props) { const comma = hasComa ? : null; - if (length! > 0 && start! >= 0 && start! < text.length) { - const begin = text.substr(0, start); - const highlighted = text.substr(start!, length); - const end = text.substr(start! + length!); + 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 ( diff --git a/src/components/JsonViewer/i18n/en.json b/src/components/JsonViewer/i18n/en.json index ea272262f0..a5fd5c16ac 100644 --- a/src/components/JsonViewer/i18n/en.json +++ b/src/components/JsonViewer/i18n/en.json @@ -1,7 +1,11 @@ { "action_collapse-all": "Collapse all", "action_expand-all": "Expand all", + "action_next": "Next", + "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": [ diff --git a/src/components/JsonViewer/unipika/flattenUnipika.ts b/src/components/JsonViewer/unipika/flattenUnipika.ts index a3ff270226..abf6efefd4 100644 --- a/src/components/JsonViewer/unipika/flattenUnipika.ts +++ b/src/components/JsonViewer/unipika/flattenUnipika.ts @@ -181,7 +181,10 @@ function pushPath(path: string, ctx: FlatContext) { function popPath(ctx: FlatContext) { const last = ctx.path.pop(); if (last !== undefined) { - ctx.collapsedPath = ctx.collapsedPath.substr(0, ctx.collapsedPath.length - last.length - 1); + ctx.collapsedPath = ctx.collapsedPath.substring( + 0, + ctx.collapsedPath.length - last.length - 1, + ); } }