Unverified Commit c55300df authored by Lijiaoa's avatar Lijiaoa Committed by GitHub
Browse files

[webui] Add Expanded icon for table (#3103)

parent e9c62bac
......@@ -6,6 +6,7 @@ import NavCon from './components/NavCon';
import MessageInfo from './components/modals/MessageInfo';
import { SlideNavBtns } from './components/slideNav/SlideNavBtns';
import './App.scss';
import './static/style/common.scss';
interface AppState {
interval: number;
......
......@@ -168,8 +168,11 @@ class Overview extends React.Component<{}, OverviewState> {
</Stack>
<div className='overviewChart'>
<Accuracy accuracyData={accuracyGraphData} accNodata={noDataMessage} />
<SuccessTable
trialIds={bestTrials.map(trial => trial.info.trialJobId)}
updateOverviewPage={updateOverviewPage}
/>
</div>
<SuccessTable trialIds={bestTrials.map(trial => trial.info.trialJobId)} />
</div>
<div className='overviewCommand1'>
<Command1 />
......
......@@ -188,16 +188,16 @@ export const EditExperimentParam = (): any => {
/>
)}
{isShowPencil && (
<span className='edit' onClick={hidePencil}>
<span className='edit cursor' onClick={hidePencil}>
{Edit}
</span>
)}
{!isShowPencil && (
<span className='series'>
<span className='confirm' onClick={confirmEdit}>
<span className='confirm cursor' onClick={confirmEdit}>
{CheckMark}
</span>
<span className='cancel' onClick={cancelEdit}>
<span className='cancel cursor' onClick={cancelEdit}>
{Cancel}
</span>
</span>
......
import * as React from 'react';
import { DetailsRow, IDetailsRowBaseProps } from '@fluentui/react';
import OpenRow from '../../public-child/OpenRow';
interface DetailsProps {
detailsProps: IDetailsRowBaseProps;
}
interface DetailsState {
isExpand: boolean;
}
class Details extends React.Component<DetailsProps, DetailsState> {
constructor(props: DetailsProps) {
super(props);
this.state = { isExpand: false };
}
render(): React.ReactNode {
const { detailsProps } = this.props;
const { isExpand } = this.state;
return (
<div>
<div
onClick={(): void => {
this.setState(() => ({ isExpand: !isExpand }));
}}
>
<DetailsRow {...detailsProps} />
</div>
{isExpand && <OpenRow trialId={detailsProps.item.id} />}
</div>
);
}
}
export default Details;
......@@ -3,6 +3,8 @@ import {
DetailsList,
IDetailsListProps,
IColumn,
Icon,
DetailsRow,
IRenderFunction,
IDetailsHeaderProps,
Sticky,
......@@ -11,9 +13,10 @@ import {
ScrollbarVisibility
} from '@fluentui/react';
import DefaultMetric from '../../public-child/DefaultMetric';
import Details from './Details';
import { convertDuration } from '../../../static/function';
import OpenRow from '../../public-child/OpenRow';
import { convertDuration, copyAndSort } from '../../../static/function';
import { TRIALS } from '../../../static/datamodel';
import { SortInfo } from '../../../static/interface';
import { DETAILTABS } from '../../stateless-component/NNItabs';
import '../../../static/style/succTable.scss';
import '../../../static/style/tableStatus.css';
......@@ -21,12 +24,15 @@ import '../../../static/style/openRow.scss';
interface SuccessTableProps {
trialIds: string[];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
updateOverviewPage: () => void;
}
interface SuccessTableState {
columns: IColumn[];
source: Array<any>;
innerWidth: number;
expandRowIdList: Set<string>;
sortInfo: SortInfo;
}
class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> {
......@@ -35,18 +41,42 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
this.state = {
columns: this.columns,
source: TRIALS.table(this.props.trialIds),
innerWidth: window.innerWidth
sortInfo: { field: '', isDescend: false },
expandRowIdList: new Set() // store expanded row's trial id
};
}
private onRenderRow: IDetailsListProps['onRenderRow'] = props => {
if (props) {
return <Details detailsProps={props} />;
componentDidUpdate(prevProps: SuccessTableProps): void {
if (this.props.trialIds !== prevProps.trialIds) {
const { trialIds } = this.props;
this.setState(() => ({ source: TRIALS.table(trialIds) }));
}
return null;
};
}
onColumnClick = (ev: React.MouseEvent<HTMLElement>, getColumn: IColumn): void => {
render(): React.ReactNode {
const { columns, source, sortInfo } = this.state;
const keepSortedSource = copyAndSort(source, sortInfo.field, sortInfo.isDescend);
const isNoneData = source.length === 0 ? true : false;
return (
<div id='succTable'>
<ScrollablePane className='scrollPanel' scrollbarVisibility={ScrollbarVisibility.auto}>
<DetailsList
columns={columns}
items={keepSortedSource}
setKey='set'
compact={true}
onRenderRow={this.onRenderRow}
onRenderDetailsHeader={this.onRenderDetailsHeader}
selectionMode={0} // close selector function
className='succTable'
/>
</ScrollablePane>
{isNoneData && <div className='succTable-tooltip'>{this.tooltipStr}</div>}
</div>
);
}
private onColumnClick = (_ev: React.MouseEvent<HTMLElement>, getColumn: IColumn): void => {
const { columns, source } = this.state;
const newColumns: IColumn[] = columns.slice();
const currColumn: IColumn = newColumns.filter(item => getColumn.key === item.key)[0];
......@@ -60,32 +90,51 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
}
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const newItems = this.copyAndSort(source, currColumn.fieldName!, currColumn.isSortedDescending);
const newItems = copyAndSort(source, currColumn.fieldName!, currColumn.isSortedDescending);
this.setState({
columns: newColumns,
source: newItems
source: newItems,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sortInfo: { field: currColumn.fieldName!, isDescend: currColumn.isSortedDescending }
});
};
private copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
const key = columnKey as keyof T;
return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
}
tooltipStr = (
private tooltipStr = (
<React.Fragment>
The experiment is running, please wait for the final metric patiently. You could also find status of trial
job with <span>{DETAILTABS}</span> button.
</React.Fragment>
);
columns = [
private columns = [
{
key: '_expand',
name: '',
onRender: (item: any): any => (
<Icon
aria-hidden={true}
iconName='ChevronRight'
styles={{
root: {
transition: 'all 0.2s',
transform: `rotate(${this.state.expandRowIdList.has(item.id) ? 90 : 0}deg)`
}
}}
className='cursor'
onClick={this.expandTrialId.bind(this, Event, item.id)}
/>
),
fieldName: 'expand',
isResizable: false,
minWidth: 20,
maxWidth: 20
},
{
name: 'Trial No.',
key: 'sequenceId',
fieldName: 'sequenceId', // required!
minWidth: 65,
maxWidth: 119,
minWidth: 60,
maxWidth: 100,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
......@@ -95,8 +144,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
name: 'ID',
key: 'id',
fieldName: 'id',
minWidth: 65,
maxWidth: 119,
minWidth: 60,
maxWidth: 118,
isResizable: true,
className: 'tableHead leftTitle',
data: 'string',
......@@ -106,7 +155,7 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
{
name: 'Duration',
key: 'duration',
minWidth: 90,
minWidth: 85,
maxWidth: 166,
isResizable: true,
fieldName: 'duration',
......@@ -121,7 +170,7 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
{
name: 'Status',
key: 'status',
minWidth: 108,
minWidth: 98,
maxWidth: 160,
isResizable: true,
fieldName: 'status',
......@@ -133,7 +182,7 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
name: 'Default metric',
key: 'accuracy',
fieldName: 'accuracy',
minWidth: 108,
minWidth: 100,
maxWidth: 166,
isResizable: true,
data: 'number',
......@@ -142,7 +191,7 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
}
];
onRenderDetailsHeader: IRenderFunction<IDetailsHeaderProps> = (props, defaultRender) => {
private onRenderDetailsHeader: IRenderFunction<IDetailsHeaderProps> = (props, defaultRender) => {
if (!props) {
return null;
}
......@@ -156,46 +205,35 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
);
};
setInnerWidth = (): void => {
this.setState(() => ({ innerWidth: window.innerWidth }));
private onRenderRow: IDetailsListProps['onRenderRow'] = props => {
const { expandRowIdList } = this.state;
if (props) {
return (
<div>
<div>
<DetailsRow {...props} />
</div>
{Array.from(expandRowIdList).map(
item => item === props.item.id && <OpenRow key={item} trialId={item} />
)}
</div>
);
}
return null;
};
componentDidMount(): void {
window.addEventListener('resize', this.setInnerWidth);
}
componentWillUnmount(): void {
window.removeEventListener('resize', this.setInnerWidth);
}
componentDidUpdate(prevProps: SuccessTableProps): void {
if (this.props.trialIds !== prevProps.trialIds) {
const { trialIds } = this.props;
this.setState(() => ({ source: TRIALS.table(trialIds) }));
private expandTrialId = (_event: any, id: string): void => {
const { expandRowIdList } = this.state;
const { updateOverviewPage } = this.props;
const copyExpandList = expandRowIdList;
if (copyExpandList.has(id)) {
copyExpandList.delete(id);
} else {
copyExpandList.add(id);
}
}
render(): React.ReactNode {
const { columns, source } = this.state;
const isNoneData = source.length === 0 ? true : false;
return (
<div id='succTable'>
<ScrollablePane className='scrollPanel' scrollbarVisibility={ScrollbarVisibility.auto}>
<DetailsList
columns={columns}
items={source}
setKey='set'
compact={true}
onRenderRow={this.onRenderRow}
onRenderDetailsHeader={this.onRenderDetailsHeader}
selectionMode={0} // close selector function
className='succTable'
/>
</ScrollablePane>
{isNoneData && <div className='succTable-tooltip'>{this.tooltipStr}</div>}
</div>
);
}
this.setState(() => ({ expandRowIdList: copyExpandList }));
updateOverviewPage();
};
}
export default SuccessTable;
......@@ -15,8 +15,8 @@ import {
import React from 'react';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import { TOOLTIP_BACKGROUND_COLOR } from '../../static/const';
import { convertDuration, formatTimestamp } from '../../static/function';
import { TableObj } from '../../static/interface';
import { convertDuration, formatTimestamp, copyAndSort } from '../../static/function';
import { TableObj, SortInfo } from '../../static/interface';
import '../../static/style/search.scss';
import '../../static/style/tableStatus.css';
import '../../static/style/logPath.scss';
......@@ -56,36 +56,6 @@ const searchOptionLiterals = {
const defaultDisplayedColumns = ['sequenceId', 'id', 'duration', 'status', 'latestAccuracy'];
interface SortInfo {
field: string;
isDescend?: boolean;
}
function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): any {
const key = columnKey as keyof T;
return items.slice(0).sort(function(a: T, b: T): any {
if (
a[key] === undefined ||
Object.is(a[key], NaN) ||
Object.is(a[key], Infinity) ||
Object.is(a[key], -Infinity) ||
typeof a[key] === 'object'
) {
return 1;
}
if (
b[key] === undefined ||
Object.is(b[key], NaN) ||
Object.is(b[key], Infinity) ||
Object.is(b[key], -Infinity) ||
typeof b[key] === 'object'
) {
return -1;
}
return (isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1;
});
}
function _inferColumnTitle(columnKey: string): string {
if (columnKey === 'sequenceId') {
return 'Trial No.';
......@@ -238,7 +208,7 @@ class TableList extends React.Component<TableListProps, TableListState> {
const { sortInfo } = this.state;
if (sortInfo.field !== '') {
return _copyAndSort(items, sortInfo.field, sortInfo.isDescend);
return copyAndSort(items, sortInfo.field, sortInfo.isDescend);
} else {
return items;
}
......@@ -255,6 +225,7 @@ class TableList extends React.Component<TableListProps, TableListState> {
<Icon
aria-hidden={true}
iconName='ChevronRight'
className='cursor'
styles={{
root: {
transition: 'all 0.2s',
......
......@@ -269,6 +269,30 @@ function caclMonacoEditorHeight(height): number {
return height - 178;
}
function copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): any {
const key = columnKey as keyof T;
return items.slice(0).sort(function(a: T, b: T): any {
if (
a[key] === undefined ||
Object.is(a[key], NaN) ||
Object.is(a[key], Infinity) ||
Object.is(a[key], -Infinity) ||
typeof a[key] === 'object'
) {
return 1;
}
if (
b[key] === undefined ||
Object.is(b[key], NaN) ||
Object.is(b[key], Infinity) ||
Object.is(b[key], -Infinity) ||
typeof b[key] === 'object'
) {
return -1;
}
return (isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1;
});
}
export {
convertTime,
convertDuration,
......@@ -288,5 +312,6 @@ export {
requestAxios,
isNaNorInfinity,
formatComplexTypeValue,
caclMonacoEditorHeight
caclMonacoEditorHeight,
copyAndSort
};
......@@ -212,6 +212,12 @@ interface EventMap {
[key: string]: () => void;
}
// table column sort
interface SortInfo {
field: string;
isDescend?: boolean;
}
export {
TableObj,
TableRecord,
......@@ -233,5 +239,6 @@ export {
NNIManagerStatus,
EventMap,
SingleAxis,
MultipleAxes
MultipleAxes,
SortInfo
};
.cursor{
&:hover, & i:hover{
cursor: pointer;
}
}
\ No newline at end of file
......@@ -3,10 +3,6 @@ $seriesIconMargin: 10px;
.ExpDuration {
margin-top: 20px;
span:hover {
cursor: pointer;
}
.maxTrialNum {
margin-bottom: 10px;
}
......
......@@ -64,8 +64,3 @@
.ms-ProgressIndicator-itemProgress {
padding: 0;
}
.cursor,
.cursor:hover {
cursor: pointer;
}
......@@ -30,4 +30,4 @@ $tableHeight: 381px;
padding-left: 6px;
box-sizing: border-box;
}
}
}
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment