Skip to content

Commit

Permalink
chore(tables): unify the job statistics tables
Browse files Browse the repository at this point in the history
  • Loading branch information
vitshev committed May 15, 2024
1 parent 4c1fb20 commit aba3674
Show file tree
Hide file tree
Showing 10 changed files with 373 additions and 481 deletions.
250 changes: 250 additions & 0 deletions packages/ui/src/ui/components/StatisticTable/StatisticTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import * as React from 'react';
import {useCallback} from 'react';
import cn from 'bem-cn-lite';

import ErrorBoundary from '../ErrorBoundary/ErrorBoundary';
import ElementsTableRaw from '../ElementsTable/ElementsTable';
import Icon from '../Icon/Icon';
import Toolbar from './Toolbar';

import hammer from '../../common/hammer';

import './Statistics.scss';

const block = cn('job-statistics');

const ElementsTable: any = ElementsTableRaw;

export const LEVEL_OFFSET = 40;

type Statistic = {
count: number;
max: number;
min: number;
sum: number;
last: number;
};

interface TreeItem {
name: string;
level: number;
attributes: {
name: string;
path: string;
prefix: string;
value?: Statistic;
};
isLeafNode?: boolean;
}

interface AvgProps {
item: TreeItem;
}

function Avg({item}: 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});
} else {
return hammer.format.Number(result);
}
}

return hammer.format.NO_VALUE;
}

interface StatisticProps {
item: TreeItem;
aggregation: 'avg' | 'min' | 'max' | 'sum' | 'count' | 'last';
}

export function JobStatisticTableStaticCell({item, aggregation}: StatisticProps) {
if (item.isLeafNode) {
const statistic: Statistic = item.attributes.value as Statistic;

if (aggregation === 'avg') {
return <Avg item={item} />;
} else {
return hammer.format['Number'](statistic[aggregation]);
}
}

return hammer.format.NO_VALUE;
}

interface ItemState {
empty: boolean;
collapsed: boolean;
visible: boolean;
}

interface MetricProps {
item: TreeItem;
itemState: ItemState;
toggleItemState: Function;
renderValue: (item: TreeItem) => React.ReactChild;
minWidth?: number;
}

export function ExpandedCell({
item,
itemState,
toggleItemState,
minWidth = undefined,
renderValue,
}: MetricProps) {
const offsetStyle = React.useMemo(() => {
return {minWidth, paddingLeft: (item?.level || 0) * LEVEL_OFFSET};
}, [item.level, minWidth]);

const toggleItemAndTreeState = useCallback(() => {
if (!itemState.empty) {
toggleItemState();
}
}, [itemState, toggleItemState]);

if (item.isLeafNode) {
return (
<span className={block('metric')} style={offsetStyle}>
<Icon awesome="chart-line" className={block('metric-icon')} />

<span>{renderValue(item)}</span>
</span>
);
} else {
return (
<span className={block('group')} style={offsetStyle} onClick={toggleItemAndTreeState}>
<Icon
awesome={itemState.collapsed || itemState.empty ? 'angle-down' : 'angle-up'}
className={block('group-icon-toggler')}
/>
<Icon
awesome={itemState.collapsed || itemState.empty ? 'folder' : 'folder-open'}
className={block('group-icon')}
/>
<span>{renderValue(item)}</span>
</span>
);
}
}

type StatisticTableTemplate<Item extends Partial<TreeItem>> = {
[name: string]: (
item: Item,
colName: string,
toggleItemState: Function,
itemState: ItemState,
) => React.ReactChild | null;
};

type ColumnName = 'avg' | 'min' | 'max' | 'sum' | 'count' | 'last';
type VisibleColumns = Array<ColumnName>;

const prepareTableProps = ({visibleColumns}: {visibleColumns: VisibleColumns}) => {
const columns = visibleColumns.reduce(
(ret, col) => {
ret[col] = {
sort: false,
align: 'right',
};

return ret;
},
{
name: {
sort: false,
align: 'left',
},
} as Record<ColumnName | 'name', {sort: boolean; align: 'left' | 'right'}>,
);

return {
theme: 'light',
size: 's',
striped: false,
computeKey(item: TreeItem) {
return item.name;
},
tree: true,
columns: {
sets: {
default: {
items: Object.keys(columns),
},
},
items: columns,
mode: 'default',
},
};
};

export function StatisticTable({
items,
virtual,
minWidth,
onFilterChange,
visibleColumns,
fixedHeader,
}: {
virtual?: boolean;
fixedHeader?: boolean;
items: TreeItem[];
minWidth?: number;
onFilterChange(text: string): void;
visibleColumns: Array<'avg' | 'min' | 'max' | 'sum' | 'count' | 'last'>;
}) {
const [treeState, setTreeState] = React.useState('expanded');
const templates = React.useMemo(
() =>
({
name(item, _, toggleItemState, itemState) {
return (
<ExpandedCell
item={item}
minWidth={minWidth}
toggleItemState={toggleItemState}
itemState={itemState}
renderValue={(item) => item?.attributes?.name}
/>
);
},
__default__(item, columnName: ColumnName) {
if (item.isLeafNode) {
return <JobStatisticTableStaticCell item={item} aggregation={columnName} />;
}

return null;
},
} as StatisticTableTemplate<TreeItem>),
[minWidth],
);
const tableProps = React.useMemo(() => {
return prepareTableProps({
visibleColumns,
});
}, [...visibleColumns]);

return (
<ErrorBoundary>
<div className={block()}>
<Toolbar onFilterChange={onFilterChange} onTreeStateChange={setTreeState} />

<div className={block('table-container')}>
<ElementsTable
{...tableProps}
virtual={virtual}
treeState={treeState}
templates={templates}
items={items}
css={block()}
headerClassName={block('header', {fixed: fixedHeader})}
/>
</div>
</div>
</ErrorBoundary>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@
display: inline-block;
min-width: 550px;
}

&__header_fixed {
width: 100%;
position: sticky;
top: 0;
z-index: 1;
background-color: rgba(256, 256, 256, 1);
}
}
Original file line number Diff line number Diff line change
@@ -1,54 +1,60 @@
import React, {useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {useDispatch} from 'react-redux';
import cn from 'bem-cn-lite';

import Filter from '../../../../components/Filter/Filter';
import Button from '../../../../components/Button/Button';
import YTIcon from '../../../../components/Icon/Icon';
import HelpLink from '../../../../components/HelpLink/HelpLink';
import Filter from '../Filter/Filter';
import Button from '../Button/Button';
import YTIcon from '../Icon/Icon';
import HelpLink from '../HelpLink/HelpLink';

import {RootState} from '../../../../store/reducers';
import {changeFilter, collapseTable, expandTable} from '../../../../store/actions/job/statistics';
import {isDocsAllowed} from '../../../../config';
import UIFactory from '../../../../UIFactory';
import {isDocsAllowed} from '../../config';
import UIFactory from '../../UIFactory';

const toolbarBlock = cn('elements-toolbar');
const Icon: any = YTIcon;

const block = cn('job-statistics');

export default function Toolbar() {
interface StoreParams {
filter: string;
}
export default function Toolbar(props: {
onFilterChange: (value: string) => void;
onTreeStateChange: (state: 'expanded' | 'mixed' | 'collapsed') => void;
}) {
const collapseTable = React.useCallback(
() => props.onTreeStateChange('collapsed'),
[props.onTreeStateChange],
);
const expandTable = React.useCallback(
() => props.onTreeStateChange('expanded'),
[props.onTreeStateChange],
);
// const mixedTable = React.useCallback(
// () => props.onTreeStateChange('mixed'),
// [props.onTreeStateChange],
// );

const dispatch = useDispatch();
const {filter}: StoreParams = useSelector((state: RootState) => state.job.statistics);

const handleExpand = useCallback(() => dispatch(expandTable()), [dispatch]);
const handleCollapse = useCallback(() => dispatch(collapseTable()), [dispatch]);
const handleFilterChange = useCallback(
(val: string) => dispatch(changeFilter(val)),
(val: string) => dispatch(props.onFilterChange(val)),
[dispatch],
);

return (
<div className={toolbarBlock(null, block('toolbar'))}>
<div className={toolbarBlock('container')}>
<div className={toolbarBlock('component', block('filter'))}>
<Filter size="m" debounce={500} value={filter} onChange={handleFilterChange} />
<Filter size="m" debounce={500} value={''} onChange={handleFilterChange} />
</div>

<div className={toolbarBlock('component', block('expand-collapse'))}>
<span className={block('expand-metrics')}>
<Button size="m" title="Expand All" onClick={handleExpand}>
<Icon awesome="arrow-to-bottom" />
<Button size="m" title="Expand All" onClick={expandTable}>
<YTIcon awesome="arrow-to-bottom" />
</Button>
</span>

<span className={block('collapse-metrics')}>
<Button size="m" title="Collapse All" onClick={handleCollapse}>
<Icon awesome="arrow-to-top" />
<Button size="m" title="Collapse All" onClick={collapseTable}>
<YTIcon awesome="arrow-to-top" />
</Button>
</span>
</div>
Expand Down
5 changes: 0 additions & 5 deletions packages/ui/src/ui/constants/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ export const LOAD_JOB_SPECIFICATION_REQUEST = 'JOB:GENERAL:LOAD_JOB_SPECIFICATIO
export const LOAD_JOB_SPECIFICATION_SUCCESS = 'JOB:GENERAL:LOAD_JOB_SPECIFICATION_SUCCESS';
export const LOAD_JOB_SPECIFICATION_FAILURE = 'JOB:GENERAL:LOAD_JOB_SPECIFICATION_FAILURE';
export const LOAD_JOB_SPECIFICATION_CANCELLED = 'JOB:GENERAL:LOAD_JOB_SPECIFICATION_CANCELLED';

export const COLLAPSE_TABLE = 'JOB:STATISTICS:COLLAPSE_TABLE';
export const EXPAND_TABLE = 'JOB:STATISTICS:EXPAND_TABLE';
export const CHANGE_FILTER = 'JOB:STATISTICS:CHANGE_FILTER';
export const MIX_TABLE = 'JOB:STATISTICS:MIX_TABLE';

export const CHANGE_OMIT_NODE_DIRECTORY = 'JOB:SPECIFICATION:CHANGE_OMIT_NODE_DIRECTORY';
export const CHANGE_OMIT_INPUT_TABLES_SPECS = 'JOB:SPECIFICATION:CHANGE_OMIT_INPUT_TABLES_SPECS';
export const CHANGE_OMIT_OUTPUT_TABLES_SPECS = 'JOB:SPECIFICATION:CHANGE_OMIT_OUTPUT_TABLES_SPECS';
Expand Down
Loading

0 comments on commit aba3674

Please sign in to comment.