diff --git a/packages/ui/src/ui/components/StatisticTable/Statistics.scss b/packages/ui/src/ui/components/StatisticTable/StatisticTable.scss similarity index 88% rename from packages/ui/src/ui/components/StatisticTable/Statistics.scss rename to packages/ui/src/ui/components/StatisticTable/StatisticTable.scss index 8f1b59913..06c8e1226 100644 --- a/packages/ui/src/ui/components/StatisticTable/Statistics.scss +++ b/packages/ui/src/ui/components/StatisticTable/StatisticTable.scss @@ -1,4 +1,4 @@ -.job-statistics { +.yt-statistics-table { .elements-toolbar { margin-top: -10px; margin-bottom: 10px; @@ -52,4 +52,10 @@ & &__filter { margin-right: 8px; } + + &__description { + width: 100%; + text-overflow: unset; + white-space: unset; + } } diff --git a/packages/ui/src/ui/components/StatisticTable/StatisticTable.tsx b/packages/ui/src/ui/components/StatisticTable/StatisticTable.tsx index 4ccf83141..2bbbf075b 100644 --- a/packages/ui/src/ui/components/StatisticTable/StatisticTable.tsx +++ b/packages/ui/src/ui/components/StatisticTable/StatisticTable.tsx @@ -3,23 +3,32 @@ import {useSelector} from 'react-redux'; import cn from 'bem-cn-lite'; import Icon from '../Icon/Icon'; -import hammer from '../../common/hammer'; +import format from '../../common/hammer/format'; import ErrorBoundary from '../ErrorBoundary/ErrorBoundary'; import ElementsTableRow from '../ElementsTable/ElementsTable'; +import {ExpandButton} from '../ExpandButton'; import {getFontFamilies} from '../../store/selectors/global/fonts'; +import {Tooltip} from '../../components/Tooltip/Tooltip'; +import MetaTable from '../../components/MetaTable/MetaTable'; +import {Secondary} from '../../components/Text/Text'; import Toolbar from './Toolbar'; import {getMinWidth} from './get-min-width'; import {filterStatisticTree, prepareStatisticTs} from './prepare-statistic.ts'; import {Statistic, StatisticTree, TreeState} from './types'; -import {ExpandButton} from '../ExpandButton'; -import './Statistics.scss'; +import {formatByUnit} from './utils'; +import './StatisticTable.scss'; -const block = cn('job-statistics'); +const block = cn('yt-statistics-table'); export const LEVEL_OFFSET = 40; +export type StatisticInfo = { + description?: string; + unit?: string; +}; + interface TreeItem { name: string; level: number; @@ -34,39 +43,43 @@ interface TreeItem { interface AvgProps { item: TreeItem; + unit?: string; } -function Avg({item}: AvgProps) { +function Avg({item, unit}: AvgProps) { const statistic: Statistic = item.attributes.value as Statistic; if (statistic && statistic.count && statistic.sum) { const result: number = statistic.sum / statistic.count; if (result < 1) { - return hammer.format['Number'](result, {significantDigits: 6}); + return formatByUnit(result, unit, {significantDigits: 6}); } else { - return hammer.format.Number(result); + return formatByUnit(result, unit); } } - return hammer.format.NO_VALUE; + return format.NO_VALUE; } interface StatisticProps { item: TreeItem; aggregation: 'avg' | 'min' | 'max' | 'sum' | 'count' | 'last'; + unit?: string; } -function StatisticTableStaticCell({item, aggregation}: StatisticProps) { +function StatisticTableStaticCell({item, aggregation, unit}: StatisticProps) { if (item.isLeafNode && Boolean(item.attributes.value)) { if (aggregation === 'avg') { - return ; + return ; + } else if (aggregation === 'count') { + return format['Number'](item.attributes?.value?.[aggregation]); } else { - return hammer.format['Number'](item.attributes?.value?.[aggregation]); + return formatByUnit(item.attributes?.value?.[aggregation], unit); } } - return hammer.format.NO_VALUE; + return format.NO_VALUE; } interface ItemState { @@ -81,6 +94,42 @@ interface MetricProps { toggleItemState: Function; renderValue: (item: TreeItem) => React.ReactChild; minWidth?: number; + info?: StatisticInfo; +} + +export function StatisticName({title, info}: {title: React.ReactNode; info?: StatisticInfo}) { + const emptyInfo = !info?.description && !info?.unit; + + return ( + + ) + } + > + {title}{' '} + {!emptyInfo && ( + + + + )} + + ); } export function ExpandedCell({ @@ -89,6 +138,7 @@ export function ExpandedCell({ toggleItemState, minWidth = undefined, renderValue, + info, }: MetricProps) { const offsetStyle = React.useMemo(() => { return {minWidth, paddingLeft: (item?.level || 0) * LEVEL_OFFSET}; @@ -105,7 +155,7 @@ export function ExpandedCell({ - {renderValue(item)} + ); } else { @@ -200,17 +250,21 @@ const useJobStatisticTable = ({ }; export function StatisticTable({ + className, helpUrl, virtual, visibleColumns, fixedHeader, statistic, + getStatisticInfo, }: { + className?: string; helpUrl?: string; virtual?: boolean; fixedHeader?: boolean; statistic: StatisticTree; visibleColumns: Array<'avg' | 'min' | 'max' | 'sum' | 'count' | 'last'>; + getStatisticInfo?: (name: string) => StatisticInfo | undefined; }) { const fontFamilies = useSelector(getFontFamilies); const {items, minWidth, treeState, setTreeState, onFilterChange} = useJobStatisticTable({ @@ -222,6 +276,7 @@ export function StatisticTable({ () => ({ name(item, _, toggleItemState, itemState) { + const info = getStatisticInfo?.(item.name) ?? {}; return ( item?.attributes?.name} + info={info} /> ); }, __default__(item, columnName: ColumnName) { if (item.isLeafNode) { - return ; + const {unit} = getStatisticInfo?.(item.name) ?? {}; + return ( + + ); } return null; }, }) as StatisticTableTemplate, - [minWidth], + [minWidth, getStatisticInfo], ); const tableProps = React.useMemo(() => { return prepareTableProps({ @@ -250,7 +313,7 @@ export function StatisticTable({ return ( -
+
string)> = { + ['ms']: (v, settings) => + format.TimeDuration(Math.round(v!), {format: 'milliseconds', ...settings}), + ['bytes']: (v, settings) => formatBytes(v, settings), + ['bytes * sec']: (v, settings) => formatBytes(v, settings, ' * sec'), + ['ms * bytes']: (v, settings) => formatBytes(v, settings, ' * ms'), + ['Mb*sec']: (v, settings) => formatBytes(v! * 1024 * 1024, settings, ' * sec'), +}; + +function formatBytes(v?: number, settings?: object, suffix = '') { + return isNaN(v!) ? format.NO_VALUE : format.Bytes(Math.round(v!), settings) + suffix; +} + +export function formatByUnit(v?: number, unit?: string, settings?: object) { + const formatFn = UNIT_TO_FORMATTER[unit!] ?? format.Number; + return formatFn?.(v, settings); +} diff --git a/packages/ui/src/ui/pages/job/tabs/Statistics/Statistics.scss b/packages/ui/src/ui/pages/job/tabs/Statistics/Statistics.scss new file mode 100644 index 000000000..913288d1d --- /dev/null +++ b/packages/ui/src/ui/pages/job/tabs/Statistics/Statistics.scss @@ -0,0 +1,3 @@ +.yt-job-statistics { + margin-bottom: 50px; +} diff --git a/packages/ui/src/ui/pages/job/tabs/Statistics/Statistics.tsx b/packages/ui/src/ui/pages/job/tabs/Statistics/Statistics.tsx index ec0a34df9..674ed0bfb 100644 --- a/packages/ui/src/ui/pages/job/tabs/Statistics/Statistics.tsx +++ b/packages/ui/src/ui/pages/job/tabs/Statistics/Statistics.tsx @@ -3,6 +3,7 @@ import {useSelector} from 'react-redux'; import cn from 'bem-cn-lite'; import {getRawStatistic} from '../../../../store/selectors/job/statistics'; +import {getOperationStatisticsDescription} from '../../../../store/selectors/global/supported-features'; import {StatisticTable, StatisticTree} from '../../../../components/StatisticTable'; import {isDocsAllowed} from '../../../../config'; import UIFactory from '../../../../UIFactory'; @@ -13,12 +14,14 @@ const block = cn('yt-job-statistics'); export default function Statistics() { const statistic = useSelector(getRawStatistic); + const {getStatisticInfo} = useSelector(getOperationStatisticsDescription); return ( ); } diff --git a/packages/ui/src/ui/pages/operations/OperationDetail/tabs/statistics/OperationStatisticName.scss b/packages/ui/src/ui/pages/operations/OperationDetail/tabs/statistics/OperationStatisticName.scss deleted file mode 100644 index 0e6fa1fe4..000000000 --- a/packages/ui/src/ui/pages/operations/OperationDetail/tabs/statistics/OperationStatisticName.scss +++ /dev/null @@ -1,7 +0,0 @@ -.operation-statistc-name { - &__description { - width: 100%; - text-overflow: unset; - white-space: unset; - } -} diff --git a/packages/ui/src/ui/pages/operations/OperationDetail/tabs/statistics/OperationStatisticName.tsx b/packages/ui/src/ui/pages/operations/OperationDetail/tabs/statistics/OperationStatisticName.tsx index 314ad9d77..6582c2a28 100644 --- a/packages/ui/src/ui/pages/operations/OperationDetail/tabs/statistics/OperationStatisticName.tsx +++ b/packages/ui/src/ui/pages/operations/OperationDetail/tabs/statistics/OperationStatisticName.tsx @@ -1,85 +1,20 @@ import React from 'react'; import {useSelector} from 'react-redux'; -import find_ from 'lodash/find'; -import cn from 'bem-cn-lite'; - import {getOperationStatisticsDescription} from '../../../../../store/selectors/global/supported-features'; -import MetaTable from '../../../../../components/MetaTable/MetaTable'; import {Tooltip} from '../../../../../components/Tooltip/Tooltip'; -import Icon from '../../../../../components/Icon/Icon'; -import {Secondary} from '../../../../../components/Text/Text'; import format from '../../../../../common/hammer/format'; - -import './OperationStatisticName.scss'; - -const block = cn('operation-statistc-name'); +import {StatisticName, formatByUnit} from '../../../../../components/StatisticTable'; function useStatisticInfo(name: string) { - const {byName, byRegexp} = useSelector(getOperationStatisticsDescription); - - const info = React.useMemo(() => { - const key = name.startsWith('/') ? name.substring('/'.length) : name; - const res = key.endsWith('/$$') ? byName[key.substring(0, key.length - 3)] : byName[key]; - if (res) { - return res; - } - - return find_(byRegexp, ({regexp}) => { - return regexp.test(key); - }); - }, [name, byName, byRegexp]); - - return info; + const {getStatisticInfo} = useSelector(getOperationStatisticsDescription); + return getStatisticInfo(name); } function OperationStatisticNameImpl({name, title}: {name: string; title: string}) { const info = useStatisticInfo(name); - const emptyInfo = !info?.description && !info?.unit; - return ( - - ) - } - > - {title}{' '} - {!emptyInfo && ( - - - - )} - - ); -} - -const UNIT_TO_FORMATTER: Record string> = { - ['ms']: (v, settings) => - format.TimeDuration(Math.round(v!), {format: 'milliseconds', ...settings}), - ['bytes']: (v, settings) => formatBytes(v, settings), - ['bytes * sec']: (v, settings) => formatBytes(v, settings, ' * sec'), - ['ms * bytes']: (v, settings) => formatBytes(v, settings, ' * ms'), - ['Mb*sec']: (v, settings) => formatBytes(v! * 1024 * 1024, settings, ' * sec'), -}; - -function formatBytes(v?: number, settings?: object, suffix = '') { - return isNaN(v!) ? format.NO_VALUE : format.Bytes(Math.round(v!), settings) + suffix; + return ; } function OperationStatisticValueImpl({ @@ -92,11 +27,10 @@ function OperationStatisticValueImpl({ settings?: {significantDigits: number}; }) { const info = useStatisticInfo(name); - const formatFn = UNIT_TO_FORMATTER[info?.unit || '']; - + const asStr = formatByUnit(value, info?.unit, settings); const asNumber = format.Number(value, settings); - return formatFn ? {formatFn(value, settings)} : asNumber; + return asStr !== undefined ? {asStr} : asNumber; } export const OperationStatisticValue = React.memo(OperationStatisticValueImpl); diff --git a/packages/ui/src/ui/store/reducers/global/supported-features.ts b/packages/ui/src/ui/store/reducers/global/supported-features.ts index 24237203c..bce63e86c 100644 --- a/packages/ui/src/ui/store/reducers/global/supported-features.ts +++ b/packages/ui/src/ui/store/reducers/global/supported-features.ts @@ -6,6 +6,7 @@ import { SUPPORTED_FEATURES_SUCCESS, } from '../../../constants/global'; import {mergeStateOnClusterChange} from '../../../store/reducers/utils'; +import type {StatisticInfo} from '../../../components/StatisticTable'; export interface SupportedFeaturesState { loaded: boolean; @@ -22,10 +23,7 @@ export interface SupportedFeaturesState { }; } -export interface OperationStatisticInfo { - description?: string; - unit?: string; -} +export type OperationStatisticInfo = StatisticInfo; const initialState: SupportedFeaturesState = { loaded: false, diff --git a/packages/ui/src/ui/store/selectors/global/supported-features.ts b/packages/ui/src/ui/store/selectors/global/supported-features.ts index 0ab7786d0..55196d611 100644 --- a/packages/ui/src/ui/store/selectors/global/supported-features.ts +++ b/packages/ui/src/ui/store/selectors/global/supported-features.ts @@ -180,9 +180,33 @@ export const getOperationStatisticsDescription = createSelector( } }); + const cache = new Map(); + function putToCache(key: string, info?: OperationStatisticInfo) { + cache.set(key, info); + return info; + } + return { - byName, - byRegexp, + getStatisticInfo: (name: string) => { + if (cache.has(name)) { + return cache.get(name); + } + + const key = name.startsWith('/') ? name.substring('/'.length) : name; + const res = key.endsWith('/$$') + ? byName[key.substring(0, key.length - 3)] + : byName[key]; + if (res) { + return putToCache(name, res); + } + + return putToCache( + name, + find_(byRegexp, ({regexp}) => { + return regexp.test(key); + }), + ); + }, }; }, ); diff --git a/packages/ui/tests/screenshots/pages/operations/jobs.base.screen.ts b/packages/ui/tests/screenshots/pages/operations/jobs.base.screen.ts index ecaac5b98..73a2b9089 100644 --- a/packages/ui/tests/screenshots/pages/operations/jobs.base.screen.ts +++ b/packages/ui/tests/screenshots/pages/operations/jobs.base.screen.ts @@ -68,7 +68,9 @@ test('Job - Details', async ({page}) => { await test.step('Statistics', async () => { await page.click('.tabs :text("Statistics")'); - await page.waitForSelector('.job-statistics__table-container .job-statistics__group'); + await page.waitForSelector( + '.yt-statistics-table__table-container .yt-statistics-table__group', + ); await expect(page).toHaveScreenshot(); }); diff --git a/packages/ui/tests/screenshots/pages/operations/jobs.base.screen.ts-snapshots/Job---Details-3-chromium-linux.png b/packages/ui/tests/screenshots/pages/operations/jobs.base.screen.ts-snapshots/Job---Details-3-chromium-linux.png index ec5200a5c..43b01d33e 100644 Binary files a/packages/ui/tests/screenshots/pages/operations/jobs.base.screen.ts-snapshots/Job---Details-3-chromium-linux.png and b/packages/ui/tests/screenshots/pages/operations/jobs.base.screen.ts-snapshots/Job---Details-3-chromium-linux.png differ