Unverified Commit 88a225f8 authored by Yuge Zhang's avatar Yuge Zhang Committed by GitHub
Browse files

Chevron icon before table row and TableList refactoring (#2900)

parent 842f6cdb
......@@ -28,6 +28,7 @@
"@typescript-eslint/no-inferrable-types": 0,
"@typescript-eslint/no-use-before-define": [2, "nofunc"],
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }],
"arrow-parens": [2, "as-needed"],
"no-inner-declarations": 0,
"no-empty": 2,
......
import * as React from 'react';
import { Stack, StackItem, Pivot, PivotItem, Dropdown, IDropdownOption, DefaultButton } from '@fluentui/react';
import { Stack, Pivot, PivotItem } from '@fluentui/react';
import { EXPERIMENT, TRIALS } from '../static/datamodel';
import { Trial } from '../static/model/trial';
import { AppContext } from '../App';
import { Title } from './overview/Title';
import { TitleContext } from './overview/TitleContext';
import DefaultPoint from './trial-detail/DefaultMetricPoint';
import Duration from './trial-detail/Duration';
import Para from './trial-detail/Para';
......@@ -13,18 +10,8 @@ import TableList from './trial-detail/TableList';
import '../static/style/trialsDetail.scss';
import '../static/style/search.scss';
const searchOptions = [
{ key: 'id', text: 'Id' },
{ key: 'Trial No.', text: 'Trial No.' },
{ key: 'status', text: 'Status' },
{ key: 'parameters', text: 'Parameters' }
];
interface TrialDetailState {
tablePageSize: number; // table components val
whichChart: string;
searchType: string;
searchFilter: (trial: Trial) => boolean;
}
class TrialsDetail extends React.Component<{}, TrialDetailState> {
......@@ -39,71 +26,22 @@ class TrialsDetail extends React.Component<{}, TrialDetailState> {
constructor(props) {
super(props);
this.state = {
tablePageSize: 20,
whichChart: 'Default metric',
searchType: 'id',
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/explicit-function-return-type
searchFilter: trial => true
whichChart: 'Default metric'
};
}
// search a trial by trial No. | trial id | Parameters | Status
searchTrial = (event: React.ChangeEvent<HTMLInputElement>): void => {
const targetValue = event.target.value;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let filter = (trial: Trial): boolean => true;
if (!targetValue.trim()) {
this.setState({ searchFilter: filter });
return;
}
switch (this.state.searchType) {
case 'id':
filter = (trial): boolean => trial.info.id.toUpperCase().includes(targetValue.toUpperCase());
break;
case 'Trial No.':
filter = (trial): boolean => trial.info.sequenceId.toString() === targetValue;
break;
case 'status':
filter = (trial): boolean => trial.info.status.toUpperCase().includes(targetValue.toUpperCase());
break;
case 'parameters':
// TODO: support filters like `x: 2` (instead of `"x": 2`)
filter = (trial): boolean => JSON.stringify(trial.info.hyperParameters, null, 4).includes(targetValue);
break;
default:
alert(`Unexpected search filter ${this.state.searchType}`);
}
this.setState({ searchFilter: filter });
};
handleTablePageSizeSelect = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption | undefined): void => {
if (item !== undefined) {
this.setState({ tablePageSize: item.text === 'all' ? -1 : parseInt(item.text, 10) });
}
};
handleWhichTabs = (item: any): void => {
this.setState({ whichChart: item.props.headerText });
};
updateSearchFilterType = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption | undefined): void => {
// clear input value and re-render table
if (item !== undefined) {
if (this.searchInput !== null) {
this.searchInput.value = '';
}
this.setState(() => ({ searchType: item.key.toString() }));
}
};
render(): React.ReactNode {
const { tablePageSize, whichChart, searchType } = this.state;
const source = TRIALS.filter(this.state.searchFilter);
const trialIds = TRIALS.filter(this.state.searchFilter).map(trial => trial.id);
const { whichChart } = this.state;
const source = TRIALS.toArray();
const trialIds = TRIALS.toArray().map(trial => trial.id);
return (
<AppContext.Consumer>
{(value): React.ReactNode => (
{(_value): React.ReactNode => (
<React.Fragment>
<div className='trial' id='tabsty'>
<Pivot
......@@ -144,61 +82,10 @@ class TrialsDetail extends React.Component<{}, TrialDetailState> {
</Pivot>
</div>
{/* trial table list */}
<div className='bulletedList' style={{ marginTop: 18 }}>
<Stack className='title'>
<TitleContext.Provider value={{ text: 'Trial jobs', icon: 'BulletedList' }}>
<Title />
</TitleContext.Provider>
</Stack>
<Stack horizontal className='allList'>
<StackItem grow={50}>
<DefaultButton
text='Compare'
className='allList-compare'
// use child-component tableList's function, the function is in child-component.
onClick={(): void => {
if (this.tableList) {
this.tableList.compareBtn();
}
}}
/>
</StackItem>
<StackItem grow={50}>
<Stack horizontal horizontalAlign='end' className='allList'>
<DefaultButton
className='allList-button-gap'
text='Add column'
onClick={(): void => {
if (this.tableList) {
this.tableList.addColumn();
}
}}
/>
<Dropdown
selectedKey={searchType}
options={searchOptions}
onChange={this.updateSearchFilterType}
styles={{ root: { width: 150 } }}
/>
<input
type='text'
className='allList-search-input'
placeholder={`Search by ${this.state.searchType}`}
onChange={this.searchTrial}
style={{ width: 230 }}
ref={(text): any => (this.searchInput = text)}
/>
</Stack>
</StackItem>
</Stack>
<div style={{ backgroundColor: '#fff' }}>
<TableList
pageSize={tablePageSize}
tableSource={source.map(trial => trial.tableRecord)}
columnList={value.columnList}
changeColumn={value.changeColumn}
tableSource={source}
trialsUpdateBroadcast={this.context.trialsUpdateBroadcast}
// TODO: change any to specific type
ref={(tabList): any => (this.tableList = tabList)}
/>
</div>
</React.Fragment>
......
import * as React from 'react';
import { Dialog, DialogType, DialogFooter, Checkbox, PrimaryButton, DefaultButton } from '@fluentui/react';
import { OPERATION } from '../../static/const';
interface ChangeColumnState {
userSelectColumnList: string[];
originSelectColumnList: string[];
// buffer, not saved yet
currentSelected: string[];
}
interface ChangeColumnProps {
isHideDialog: boolean;
showColumn: string[]; // all column List
selectedColumn: string[]; // user selected column list
changeColumn: (val: string[]) => void;
hideShowColumnDialog: () => void;
allColumns: SimpleColumn[]; // all column List
selectedColumns: string[]; // user selected column list
onSelectedChange: (val: string[]) => void;
onHideDialog: () => void;
minSelected?: number;
}
interface SimpleColumn {
key: string; // key for management
name: string; // name to display
}
interface CheckBoxItems {
......@@ -20,12 +24,12 @@ interface CheckBoxItems {
checked: boolean;
onChange: () => void;
}
class ChangeColumnComponent extends React.Component<ChangeColumnProps, ChangeColumnState> {
constructor(props: ChangeColumnProps) {
super(props);
this.state = {
userSelectColumnList: this.props.selectedColumn,
originSelectColumnList: this.props.selectedColumn
currentSelected: this.props.selectedColumns
};
}
......@@ -38,97 +42,50 @@ class ChangeColumnComponent extends React.Component<ChangeColumnProps, ChangeCol
label: string,
val?: boolean
): void => {
const source: string[] = JSON.parse(JSON.stringify(this.state.userSelectColumnList));
const source: string[] = [...this.state.currentSelected];
if (val === true) {
if (!source.includes(label)) {
source.push(label);
this.setState(() => ({ userSelectColumnList: source }));
this.setState({ currentSelected: source });
}
} else {
if (source.includes(label)) {
// remove from source
const result = source.filter(item => item !== label);
this.setState(() => ({ userSelectColumnList: result }));
}
this.setState({ currentSelected: result });
}
};
saveUserSelectColumn = (): void => {
const { userSelectColumnList } = this.state;
const { showColumn } = this.props;
// sort by Trial No. | ID | Duration | Start Time | End Time | ...
const sortColumn: string[] = [];
/**
*
* TODO: use this function to refactor sort column
* search space might orderless
showColumn.map(item => {
userSelectColumnList.map(key => {
if (item === key || key.includes('search space')) {
if (!sortColumn.includes(key)) {
sortColumn.push(key);
}
}
});
});
*/
// push ![Operation] ![search space] column
showColumn.map(item => {
userSelectColumnList.map(key => {
if (item === key && item !== OPERATION) {
sortColumn.push(key);
}
});
});
// push search space key
userSelectColumnList.map(index => {
if (index.includes('search space')) {
if (!sortColumn.includes(index)) {
sortColumn.push(index);
}
}
});
// push Operation
if (userSelectColumnList.includes(OPERATION)) {
sortColumn.push(OPERATION);
}
this.props.changeColumn(sortColumn);
this.hideDialog(); // hide dialog
};
hideDialog = (): void => {
this.props.hideShowColumnDialog();
const { currentSelected } = this.state;
const { allColumns, onSelectedChange } = this.props;
const selectedColumns = allColumns.map(column => column.key).filter(key => currentSelected.includes(key));
onSelectedChange(selectedColumns);
this.hideDialog();
};
// user exit dialog
cancelOption = (): void => {
// reset select column
const { originSelectColumnList } = this.state;
this.setState({ userSelectColumnList: originSelectColumnList }, () => {
this.setState({ currentSelected: this.props.selectedColumns }, () => {
this.hideDialog();
});
};
private hideDialog = (): void => {
this.props.onHideDialog();
};
render(): React.ReactNode {
const { showColumn, isHideDialog } = this.props;
const { userSelectColumnList } = this.state;
const renderOptions: Array<CheckBoxItems> = [];
showColumn.map(item => {
if (userSelectColumnList.includes(item)) {
// selected column name
renderOptions.push({ label: item, checked: true, onChange: this.makeChangeHandler(item) });
} else {
renderOptions.push({ label: item, checked: false, onChange: this.makeChangeHandler(item) });
}
});
const { allColumns, minSelected } = this.props;
const { currentSelected } = this.state;
return (
<div>
<Dialog
hidden={isHideDialog} // required field!
hidden={false}
dialogContentProps={{
type: DialogType.largeHeader,
title: 'Change table column',
subText: 'You can chose which columns you want to see in the table.'
title: 'Customize columns',
subText: 'You can choose which columns you wish to see.'
}}
modalProps={{
isBlocking: false,
......@@ -136,12 +93,22 @@ class ChangeColumnComponent extends React.Component<ChangeColumnProps, ChangeCol
}}
>
<div className='columns-height'>
{renderOptions.map(item => {
return <Checkbox key={item.label} {...item} styles={{ root: { marginBottom: 8 } }} />;
})}
{allColumns.map(item => (
<Checkbox
key={item.key}
label={item.name}
checked={currentSelected.includes(item.key)}
onChange={this.makeChangeHandler(item.key)}
styles={{ root: { marginBottom: 8 } }}
/>
))}
</div>
<DialogFooter>
<PrimaryButton text='Save' onClick={this.saveUserSelectColumn} />
<PrimaryButton
text='Save'
onClick={this.saveUserSelectColumn}
disabled={currentSelected.length < (minSelected === undefined ? 1 : minSelected)}
/>
<DefaultButton text='Cancel' onClick={this.cancelOption} />
</DialogFooter>
</Dialog>
......
import * as React from 'react';
import { renderToString } from 'react-dom/server';
import { Stack, Modal, IconButton, IDragOptions, ContextualMenu } from '@fluentui/react';
import ReactEcharts from 'echarts-for-react';
import IntermediateVal from '../public-child/IntermediateVal';
import { TRIALS } from '../../static/datamodel';
import { TableRecord, Intermedia, TooltipForIntermediate } from '../../static/interface';
import { TooltipForIntermediate, TableObj, SingleAxis } from '../../static/interface';
import { contentStyles, iconButtonStyles } from '../buttons/ModalTheme';
import '../../static/style/compare.scss';
import { convertDuration, parseMetrics } from '../../static/function';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
function _getWebUIWidth(): number {
return window.innerWidth;
}
const dragOptions: IDragOptions = {
moveMenuItemText: 'Move',
......@@ -13,79 +18,81 @@ const dragOptions: IDragOptions = {
menu: ContextualMenu
};
// the modal of trial compare
// TODO: this should be refactored to the common modules
// copied from trial.ts
function _parseIntermediates(trial: TableObj): number[] {
const intermediates: number[] = [];
for (const metric of trial.intermediates) {
if (metric === undefined) {
break;
}
const parsedMetric = parseMetrics(metric.data);
if (typeof parsedMetric === 'object') {
// TODO: should handle more types of metric keys
intermediates.push(parsedMetric.default);
} else {
intermediates.push(parsedMetric);
}
}
return intermediates;
}
interface Item {
id: string;
sequenceId: number;
duration: string;
parameters: Map<string, any>;
metrics: Map<string, any>;
intermediates: number[];
}
interface CompareProps {
compareStacks: Array<TableRecord>;
cancelFunc: () => void;
trials: TableObj[];
title: string;
showDetails: boolean;
onHideDialog: () => void;
}
class Compare extends React.Component<CompareProps, {}> {
public _isCompareMount!: boolean;
constructor(props: CompareProps) {
super(props);
}
intermediate = (): React.ReactNode => {
const { compareStacks } = this.props;
const trialIntermediate: Array<Intermedia> = [];
const idsList: string[] = [];
compareStacks.forEach(element => {
const trial = TRIALS.getTrial(element.id);
trialIntermediate.push({
name: element.id,
data: trial.description.intermediate,
type: 'line',
hyperPara: trial.description.parameters
});
idsList.push(element.id);
});
// find max intermediate number
trialIntermediate.sort((a, b) => {
return b.data.length - a.data.length;
});
const legend: string[] = [];
// max length
const length = trialIntermediate[0] !== undefined ? trialIntermediate[0].data.length : 0;
const xAxis: number[] = [];
trialIntermediate.forEach(element => {
legend.push(element.name);
});
for (let i = 1; i <= length; i++) {
xAxis.push(i);
private _generateTooltipSummary(row: Item, metricKey: string): string {
return renderToString(
<div className='tooldetailAccuracy'>
<div>Trial ID: {row.id}</div>
<div>Default metric: {row.metrics.get(metricKey) || 'N/A'}</div>
</div>
);
}
private _intermediates(items: Item[], metricKey: string): React.ReactNode {
// Precondition: make sure `items` is not empty
const xAxisMax = Math.max(...items.map(item => item.intermediates.length));
const xAxis = Array(xAxisMax)
.fill(0)
.map((_, i) => i + 1); // [1, 2, 3, ..., xAxisMax]
const dataForEchart = items.map(item => ({
name: item.id,
data: item.intermediates,
type: 'line'
}));
const legend = dataForEchart.map(item => item.name);
const option = {
tooltip: {
trigger: 'item',
enterable: true,
position: function(point: number[], data: TooltipForIntermediate): number[] {
position: (point: number[], data: TooltipForIntermediate): [number, number] => {
if (data.dataIndex < length / 2) {
return [point[0], 80];
} else {
return [point[0] - 300, 80];
}
},
formatter: function(data: TooltipForIntermediate): React.ReactNode {
const trialId = data.seriesName;
let obj = {};
const temp = trialIntermediate.find(key => key.name === trialId);
if (temp !== undefined) {
obj = temp.hyperPara;
}
return (
'<div class="tooldetailAccuracy">' +
'<div>Trial ID: ' +
trialId +
'</div>' +
'<div>Intermediate: ' +
data.data +
'</div>' +
'<div>Parameters: ' +
'<pre>' +
JSON.stringify(obj, null, 4) +
'</pre>' +
'</div>' +
'</div>'
);
formatter: (data: TooltipForIntermediate): string => {
const item = items.find(k => k.id === data.seriesName) as Item;
return this._generateTooltipSummary(item, metricKey);
}
},
grid: {
......@@ -96,12 +103,11 @@ class Compare extends React.Component<CompareProps, {}> {
legend: {
type: 'scroll',
right: 40,
left: idsList.length > 6 ? 80 : null,
data: idsList
left: legend.length > 6 ? 80 : null,
data: legend
},
xAxis: {
type: 'category',
// name: '# Intermediate',
boundaryGap: false,
data: xAxis
},
......@@ -110,7 +116,7 @@ class Compare extends React.Component<CompareProps, {}> {
name: 'Metric',
scale: true
},
series: trialIntermediate
series: dataForEchart
};
return (
<ReactEcharts
......@@ -119,108 +125,92 @@ class Compare extends React.Component<CompareProps, {}> {
notMerge={true} // update now
/>
);
};
}
// render table column ---
initColumn = (): React.ReactNode => {
const idList: string[] = [];
const sequenceIdList: number[] = [];
const durationList: number[] = [];
private _renderRow(
key: string,
rowName: string,
className: string,
items: Item[],
formatter: (item: Item) => string
): React.ReactNode {
return (
<tr key={key}>
<td className='column'>{rowName}</td>
{items.map(item => (
<td className={className} key={item.id}>
{formatter(item)}
</td>
))}
</tr>
);
}
const compareStacks = this.props.compareStacks.map(tableRecord => TRIALS.getTrial(tableRecord.id));
private _overlapKeys(s: Map<string, any>[]): string[] {
// Calculate the overlapped keys for multiple
const intersection: string[] = [];
for (const i of s[0].keys()) {
let inAll = true;
for (const t of s) {
if (!Array.from(t.keys()).includes(i)) {
inAll = false;
break;
}
}
if (inAll) {
intersection.push(i);
}
}
return intersection;
}
const parameterList: Array<object> = [];
let parameterKeys: string[] = [];
if (compareStacks.length !== 0) {
parameterKeys = Object.keys(compareStacks[0].description.parameters);
}
compareStacks.forEach(temp => {
idList.push(temp.id);
sequenceIdList.push(temp.sequenceId);
durationList.push(temp.duration);
parameterList.push(temp.description.parameters);
});
let isComplexSearchSpace;
if (parameterList.length > 0) {
isComplexSearchSpace = typeof parameterList[0][parameterKeys[0]] === 'object' ? true : false;
}
const width = this.getWebUIWidth();
let scrollClass;
// render table column ---
private _columns(items: Item[]): React.ReactNode {
// Precondition: make sure `items` is not empty
const width = _getWebUIWidth();
let scrollClass: string = '';
if (width > 1200) {
scrollClass = idList.length > 3 ? 'flex' : '';
scrollClass = items.length > 3 ? 'flex' : '';
} else if (width < 700) {
scrollClass = idList.length > 1 ? 'flex' : '';
scrollClass = items.length > 1 ? 'flex' : '';
} else {
scrollClass = idList.length > 2 ? 'flex' : '';
scrollClass = items.length > 2 ? 'flex' : '';
}
const parameterKeys = this._overlapKeys(items.map(item => item.parameters));
const metricKeys = this._overlapKeys(items.map(item => item.metrics));
return (
<table className={`compare-modal-table ${scrollClass}`}>
<tbody>
<tr>
<td className='column'>Id</td>
{Object.keys(idList).map(key => (
<td className='value idList' key={key}>
{idList[key]}
</td>
))}
</tr>
<tr>
<td className='column'>Trial No.</td>
{Object.keys(sequenceIdList).map(key => (
<td className='value idList' key={key}>
{sequenceIdList[key]}
</td>
))}
</tr>
<tr>
<td className='column'>Default metric</td>
{Object.keys(compareStacks).map(index => (
<td className='value' key={index}>
<IntermediateVal trialId={compareStacks[index].id} />
</td>
))}
</tr>
<tr>
<td className='column'>duration</td>
{Object.keys(durationList).map(index => (
<td className='value' key={index}>
{durationList[index]}
</td>
))}
</tr>
{isComplexSearchSpace
? null
: Object.keys(parameterKeys).map(index => (
<tr key={index}>
<td className='column' key={index}>
{parameterKeys[index]}
</td>
{Object.keys(parameterList).map(key => (
<td key={key} className='value'>
{parameterList[key][parameterKeys[index]]}
</td>
))}
</tr>
))}
{this._renderRow('id', 'ID', 'value idList', items, item => item.id)}
{this._renderRow('trialnum', 'Trial No.', 'value', items, item => item.sequenceId.toString())}
{this._renderRow('duration', 'Duration', 'value', items, item => item.duration)}
{parameterKeys.map(k =>
this._renderRow(`space_${k}`, k, 'value', items, item => item.parameters.get(k))
)}
{metricKeys.map(k =>
this._renderRow(`metrics_${k}`, `Metric: ${k}`, 'value', items, item => item.metrics.get(k))
)}
</tbody>
</table>
);
};
getWebUIWidth = (): number => {
return window.innerWidth;
};
componentDidMount(): void {
this._isCompareMount = true;
}
componentWillUnmount(): void {
this._isCompareMount = false;
}
render(): React.ReactNode {
const { cancelFunc } = this.props;
const { onHideDialog, trials, title, showDetails } = this.props;
const flatten = (m: Map<SingleAxis, any>): Map<string, any> => {
return new Map(Array.from(m).map(([key, value]) => [key.baseName, value]));
};
const inferredSearchSpace = TRIALS.inferredSearchSpace(EXPERIMENT.searchSpaceNew);
const items: Item[] = trials.map(trial => ({
id: trial.id,
sequenceId: trial.sequenceId,
duration: convertDuration(trial.duration),
parameters: flatten(trial.parameters(inferredSearchSpace)),
metrics: flatten(trial.metrics(TRIALS.inferredMetricSpace())),
intermediates: _parseIntermediates(trial)
}));
const metricKeys = this._overlapKeys(items.map(item => item.metrics));
const defaultMetricKey = !metricKeys || metricKeys.includes('default') ? 'default' : metricKeys[0];
return (
<Modal
......@@ -229,22 +219,23 @@ class Compare extends React.Component<CompareProps, {}> {
className='compare-modal'
allowTouchBodyScroll={true}
dragOptions={dragOptions}
onDismiss={onHideDialog}
>
<div>
<div className={contentStyles.header}>
<span>Compare trials</span>
<span>{title}</span>
<IconButton
styles={iconButtonStyles}
iconProps={{ iconName: 'Cancel' }}
ariaLabel='Close popup modal'
onClick={cancelFunc}
onClick={onHideDialog}
/>
</div>
<Stack className='compare-modal-intermediate'>
{this.intermediate()}
{this._intermediates(items, defaultMetricKey)}
<Stack className='compare-yAxis'># Intermediate result</Stack>
</Stack>
<Stack>{this.initColumn()}</Stack>
{showDetails && <Stack>{this._columns(items)}</Stack>}
</div>
</Modal>
);
......
import * as React from 'react';
import { DetailsRow, IDetailsRowBaseProps } from '@fluentui/react';
import OpenRow from '../public-child/OpenRow';
interface ExpandableDetailsProps {
detailsProps: IDetailsRowBaseProps;
isExpand: boolean;
}
class ExpandableDetails extends React.Component<ExpandableDetailsProps, {}> {
render(): React.ReactNode {
const { detailsProps, isExpand } = this.props;
return (
<div>
<DetailsRow {...detailsProps} />
{isExpand && <OpenRow trialId={detailsProps.item.id} />}
</div>
);
}
}
export default ExpandableDetails;
import * as React from 'react';
import { DetailsList, Dropdown, Icon, IDetailsListProps, IDropdownOption, IStackTokens, Stack } from '@fluentui/react';
import ReactPaginate from 'react-paginate';
interface PaginationTableState {
itemsPerPage: number;
currentPage: number;
itemsOnPage: any[]; // this needs to be stored in state to prevent re-rendering
}
const horizontalGapStackTokens: IStackTokens = {
childrenGap: 20,
padding: 10
};
function _currentTableOffset(perPage: number, currentPage: number, source: any[]): number {
return perPage === -1 ? 0 : Math.min(currentPage, Math.floor((source.length - 1) / perPage)) * perPage;
}
function _obtainPaginationSlice(perPage: number, currentPage: number, source: any[]): any[] {
if (perPage === -1) {
return source;
} else {
const offset = _currentTableOffset(perPage, currentPage, source);
return source.slice(offset, offset + perPage);
}
}
class PaginationTable extends React.PureComponent<IDetailsListProps, PaginationTableState> {
constructor(props: IDetailsListProps) {
super(props);
this.state = {
itemsPerPage: 20,
currentPage: 0,
itemsOnPage: []
};
}
private _onItemsPerPageSelect(event: React.FormEvent<HTMLDivElement>, item: IDropdownOption | undefined): void {
if (item !== undefined) {
const { items } = this.props;
// use current offset to calculate the next `current_page`
const currentOffset = _currentTableOffset(this.state.itemsPerPage, this.state.currentPage, items);
const itemsPerPage = item.key as number;
const currentPage = Math.floor(currentOffset / itemsPerPage);
this.setState({
itemsPerPage: itemsPerPage,
currentPage: currentPage,
itemsOnPage: _obtainPaginationSlice(itemsPerPage, currentPage, this.props.items)
});
}
}
private _onPageSelect(event: any): void {
const currentPage = event.selected;
this.setState({
currentPage: currentPage,
itemsOnPage: _obtainPaginationSlice(this.state.itemsPerPage, currentPage, this.props.items)
});
}
componentDidUpdate(prevProps: IDetailsListProps): void {
if (prevProps.items !== this.props.items) {
this.setState({
itemsOnPage: _obtainPaginationSlice(this.state.itemsPerPage, this.state.currentPage, this.props.items)
});
}
}
render(): React.ReactNode {
const { itemsPerPage, itemsOnPage } = this.state;
const detailListProps = {
...this.props,
items: itemsOnPage
};
const itemsCount = this.props.items.length;
const pageCount = itemsPerPage === -1 ? 1 : Math.ceil(itemsCount / itemsPerPage);
const perPageOptions = [
{ key: 10, text: '10 items per page' },
{ key: 20, text: '20 items per page' },
{ key: 50, text: '50 items per page' },
{ key: -1, text: 'All items' }
];
return (
<div>
<DetailsList {...detailListProps} />
<Stack
horizontal
horizontalAlign='end'
verticalAlign='baseline'
styles={{ root: { padding: 10 } }}
tokens={horizontalGapStackTokens}
>
<Dropdown
selectedKey={itemsPerPage}
options={perPageOptions}
onChange={this._onItemsPerPageSelect.bind(this)}
styles={{ dropdown: { width: 150 } }}
/>
<ReactPaginate
previousLabel={<Icon aria-hidden={true} iconName='ChevronLeft' />}
nextLabel={<Icon aria-hidden={true} iconName='ChevronRight' />}
breakLabel={'...'}
breakClassName={'break'}
pageCount={pageCount}
marginPagesDisplayed={2}
pageRangeDisplayed={2}
onPageChange={this._onPageSelect.bind(this)}
containerClassName={itemsCount === 0 ? 'pagination hidden' : 'pagination'}
subContainerClassName={'pages pagination'}
disableInitialCallback={false}
activeClassName={'active'}
/>
</Stack>
</div>
);
}
}
export default PaginationTable;
import * as d3 from 'd3';
import { Dropdown, IDropdownOption, Stack } from '@fluentui/react';
import { Dropdown, IDropdownOption, Stack, DefaultButton } from '@fluentui/react';
import ParCoords from 'parcoord-es';
import 'parcoord-es/dist/parcoords.css';
import * as React from 'react';
......@@ -9,12 +9,16 @@ import { filterByStatus } from '../../static/function';
import { TableObj, SingleAxis, MultipleAxes } from '../../static/interface';
import '../../static/style/button.scss';
import '../../static/style/para.scss';
import ChangeColumnComponent from '../modals/ChangeColumnComponent';
interface ParaState {
dimName: string[];
selectedPercent: string;
primaryMetricKey: string;
noChart: boolean;
customizeColumnsDialogVisible: boolean;
availableDimensions: string[];
chosenDimensions: string[];
}
interface ParaProps {
......@@ -45,7 +49,10 @@ class Para extends React.Component<ParaProps, ParaState> {
dimName: [],
primaryMetricKey: 'default',
selectedPercent: '1',
noChart: true
noChart: true,
customizeColumnsDialogVisible: false,
availableDimensions: [],
chosenDimensions: []
};
}
......@@ -82,11 +89,24 @@ class Para extends React.Component<ParaProps, ParaState> {
}
render(): React.ReactNode {
const { selectedPercent, noChart } = this.state;
const {
selectedPercent,
noChart,
customizeColumnsDialogVisible,
availableDimensions,
chosenDimensions
} = this.state;
return (
<div className='parameter'>
<Stack horizontal className='para-filter' horizontalAlign='end'>
<DefaultButton
text='Add/Remove axes'
onClick={(): void => {
this.setState({ customizeColumnsDialogVisible: true });
}}
styles={{ root: { marginRight: 10 } }}
/>
<Dropdown
selectedKey={selectedPercent}
onChange={this.percentNum}
......@@ -101,6 +121,21 @@ class Para extends React.Component<ParaProps, ParaState> {
/>
{this.finalKeysDropdown()}
</Stack>
{customizeColumnsDialogVisible && availableDimensions.length > 0 && (
<ChangeColumnComponent
selectedColumns={chosenDimensions}
allColumns={availableDimensions.map(dim => ({ key: dim, name: dim }))}
onSelectedChange={(selected: string[]): void => {
this.setState({ chosenDimensions: selected }, () => {
this.renderParallelCoordinates();
});
}}
onHideDialog={(): void => {
this.setState({ customizeColumnsDialogVisible: false });
}}
minSelected={2}
/>
)}
<div className='parcoords' style={this.chartMulineStyle} ref={this.paraRef} />
{noChart && <div className='nodata'>No data</div>}
</div>
......@@ -143,13 +178,13 @@ class Para extends React.Component<ParaProps, ParaState> {
private renderParallelCoordinates(): void {
const { searchSpace } = this.props;
const percent = parseFloat(this.state.selectedPercent);
const { primaryMetricKey } = this.state;
const { primaryMetricKey, chosenDimensions } = this.state;
const inferredSearchSpace = TRIALS.inferredSearchSpace(searchSpace);
const inferredMetricSpace = TRIALS.inferredMetricSpace();
let convertedTrials = this.getTrialsAsObjectList(inferredSearchSpace, inferredMetricSpace);
const dimensions: [any, any][] = [];
const dimensions: [string, any][] = [];
let colorDim: string | undefined = undefined,
colorScale: any = undefined;
// treat every axis as numeric to fit for brush
......@@ -213,7 +248,11 @@ class Para extends React.Component<ParaProps, ParaState> {
}
this.pcs
.data(convertedTrials)
.dimensions(dimensions.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}));
.dimensions(
dimensions
.filter(([d, _]) => chosenDimensions.length === 0 || chosenDimensions.includes(d))
.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {})
);
if (firstRun) {
this.pcs
.margin(this.innerChartMargins)
......@@ -230,6 +269,12 @@ class Para extends React.Component<ParaProps, ParaState> {
if (firstRun) {
this.setState({ noChart: false });
}
// set new available dims
this.setState({
availableDimensions: dimensions.map(e => e[0]),
chosenDimensions: chosenDimensions.length === 0 ? dimensions.map(e => e[0]) : chosenDimensions
});
}
private getTrialsAsObjectList(inferredSearchSpace: MultipleAxes, inferredMetricSpace: MultipleAxes): {}[] {
......
import React, { lazy } from 'react';
import axios from 'axios';
import ReactEcharts from 'echarts-for-react';
import {
Stack,
DefaultButton,
Dropdown,
DetailsList,
IDetailsListProps,
DetailsListLayoutMode,
PrimaryButton,
Modal,
IDropdownOption,
IColumn,
Icon,
IDropdownOption,
PrimaryButton,
Selection,
SelectionMode,
IconButton,
TooltipHost,
IStackTokens
Stack,
StackItem,
TooltipHost
} from '@fluentui/react';
import ReactPaginate from 'react-paginate';
import { LineChart, blocked, copy } from '../buttons/Icon';
import { MANAGER_IP, COLUMNPro } from '../../static/const';
import { convertDuration, formatTimestamp, intermediateGraphOption, parseMetrics } from '../../static/function';
import React from 'react';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import { TableRecord, TrialJobInfo } from '../../static/interface';
const Details = lazy(() => import('../overview/table/Details'));
const ChangeColumnComponent = lazy(() => import('../modals/ChangeColumnComponent'));
const Compare = lazy(() => import('../modals/Compare'));
const KillJob = lazy(() => import('../modals/Killjob'));
const Customize = lazy(() => import('../modals/CustomizedTrial'));
import { contentStyles, iconButtonStyles } from '../buttons/ModalTheme';
import { convertDuration, formatTimestamp } from '../../static/function';
import { TableObj } from '../../static/interface';
import '../../static/style/search.scss';
import '../../static/style/tableStatus.css';
import '../../static/style/logPath.scss';
import '../../static/style/table.scss';
import '../../static/style/button.scss';
import '../../static/style/logPath.scss';
import '../../static/style/openRow.scss';
import '../../static/style/pagination.scss';
import '../../static/style/search.scss';
import '../../static/style/table.scss';
import '../../static/style/tableStatus.css';
import { blocked, copy, LineChart, tableListIcon } from '../buttons/Icon';
import ChangeColumnComponent from '../modals/ChangeColumnComponent';
import Compare from '../modals/Compare';
import Customize from '../modals/CustomizedTrial';
import KillJob from '../modals/Killjob';
import ExpandableDetails from '../public-child/ExpandableDetails';
import PaginationTable from '../public-child/PaginationTable';
import { Trial } from '../../static/model/trial';
const echarts = require('echarts/lib/echarts');
require('echarts/lib/chart/line');
......@@ -45,462 +43,379 @@ echarts.registerTheme('my_theme', {
color: '#3c8dbc'
});
const horizontalGapStackTokens: IStackTokens = {
childrenGap: 20,
padding: 10
type SearchOptionType = 'id' | 'trialnum' | 'status' | 'parameters';
const searchOptionLiterals = {
id: 'ID',
trialnum: 'Trial No.',
status: 'Status',
parameters: 'Parameters'
};
interface TableListProps {
pageSize: number;
tableSource: Array<TableRecord>;
columnList: string[]; // user select columnKeys
changeColumn: (val: string[]) => void;
trialsUpdateBroadcast: number;
}
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.';
} else if (columnKey === 'id') {
return 'ID';
} else if (columnKey === 'intermediateCount') {
return 'Intermediate results (#)';
} else if (columnKey.startsWith('space/')) {
return columnKey.split('/', 2)[1] + ' (space)';
} else if (columnKey === 'latestAccuracy') {
return 'Default metric'; // to align with the original design
} else if (columnKey.startsWith('metric/')) {
return columnKey.split('/', 2)[1] + ' (metric)';
} else if (columnKey.startsWith('_')) {
return columnKey;
} else {
// camel case to verbose form
const withSpace = columnKey.replace(/[A-Z]/g, letter => ` ${letter.toLowerCase()}`);
return withSpace.charAt(0).toUpperCase() + withSpace.slice(1);
}
}
interface TableListProps {
tableSource: TableObj[];
trialsUpdateBroadcast: number;
}
interface TableListState {
intermediateOption: object;
modalVisible: boolean;
isObjFinal: boolean;
isShowColumn: boolean;
selectRows: Array<any>;
isShowCompareModal: boolean;
selectedRowKeys: string[] | number[];
intermediateData: Array<object>; // a trial's intermediate results (include dict)
intermediateId: string;
intermediateOtherKeys: string[];
isShowCustomizedModal: boolean;
copyTrialId: string; // user copy trial to submit a new customized trial
isCalloutVisible: boolean; // kill job button callout [kill or not kill job window]
intermediateKey: string; // intermeidate modal: which key is choosed.
isExpand: boolean;
modalIntermediateWidth: number;
modalIntermediateHeight: number;
tableColumns: IColumn[];
allColumnList: string[];
tableSourceForSort: Array<TableRecord>;
sortMessage: SortInfo;
offset: number;
tablePerPage: Array<TableRecord>;
perPage: number;
currentPage: number;
pageCount: number;
displayedItems: any[];
displayedColumns: string[];
columns: IColumn[];
searchType: SearchOptionType;
searchText: string;
selectedRowIds: string[];
customizeColumnsDialogVisible: boolean;
compareDialogVisible: boolean;
intermediateDialogTrial: TableObj | undefined;
copiedTrialId: string | undefined;
sortInfo: SortInfo;
}
class TableList extends React.Component<TableListProps, TableListState> {
public intervalTrialLog = 10;
public trialId!: string;
private _selection: Selection;
private _expandedTrialIds: Set<string>;
constructor(props: TableListProps) {
super(props);
this.state = {
intermediateOption: {},
modalVisible: false,
isObjFinal: false,
isShowColumn: false,
isShowCompareModal: false,
selectRows: [],
selectedRowKeys: [], // close selected trial message after modal closed
intermediateData: [],
intermediateId: '',
intermediateOtherKeys: [],
isShowCustomizedModal: false,
isCalloutVisible: false,
copyTrialId: '',
intermediateKey: 'default',
isExpand: false,
modalIntermediateWidth: window.innerWidth,
modalIntermediateHeight: window.innerHeight,
tableColumns: this.initTableColumnList(this.props.columnList),
allColumnList: this.getAllColumnKeys(),
sortMessage: { field: '', isDescend: false },
offset: 0,
tablePerPage: [],
perPage: 20,
currentPage: 0,
pageCount: 0,
tableSourceForSort: this.props.tableSource
displayedItems: [],
displayedColumns: defaultDisplayedColumns,
columns: [],
searchType: 'id',
searchText: '',
customizeColumnsDialogVisible: false,
compareDialogVisible: false,
selectedRowIds: [],
intermediateDialogTrial: undefined,
copiedTrialId: undefined,
sortInfo: { field: '', isDescend: true }
};
}
// sort for table column
onColumnClick = (ev: React.MouseEvent<HTMLElement>, getColumn: IColumn): void => {
const { tableColumns } = this.state;
const newColumns: IColumn[] = tableColumns.slice();
const currColumn: IColumn = newColumns.filter(item => getColumn.key === item.key)[0];
newColumns.forEach((newCol: IColumn) => {
if (newCol === currColumn) {
currColumn.isSortedDescending = !currColumn.isSortedDescending;
currColumn.isSorted = true;
} else {
newCol.isSorted = false;
newCol.isSortedDescending = true;
this._selection = new Selection({
onSelectionChanged: (): void => {
this.setState({
selectedRowIds: this._selection.getSelection().map(s => (s as any).id)
});
}
});
this.setState(
{
tableColumns: newColumns,
sortMessage: { field: getColumn.key, isDescend: currColumn.isSortedDescending }
},
() => {
this.updateData();
this._expandedTrialIds = new Set<string>();
}
);
};
AccuracyColumnConfig: any = {
name: 'Default metric',
className: 'leftTitle',
key: 'latestAccuracy',
fieldName: 'latestAccuracy',
minWidth: 200,
maxWidth: 300,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
onRender: (item): React.ReactNode => (
<TooltipHost content={item.formattedLatestAccuracy}>
<div className='ellipsis'>{item.formattedLatestAccuracy}</div>
</TooltipHost>
)
};
SequenceIdColumnConfig: any = {
name: 'Trial No.',
key: 'sequenceId',
fieldName: 'sequenceId',
minWidth: 80,
maxWidth: 240,
className: 'tableHead',
data: 'number',
onColumnClick: this.onColumnClick
};
/* Search related methods */
IdColumnConfig: any = {
name: 'ID',
key: 'id',
fieldName: 'id',
minWidth: 150,
maxWidth: 200,
isResizable: true,
data: 'string',
onColumnClick: this.onColumnClick,
className: 'tableHead leftTitle'
};
// This functions as the filter for the final trials displayed in the current table
private _filterTrials(trials: TableObj[]): TableObj[] {
const { searchText, searchType } = this.state;
// search a trial by Trial No. | Trial ID | Parameters | Status
let searchFilter = (_: TableObj): boolean => true; // eslint-disable-line no-unused-vars
if (searchText.trim()) {
if (searchType === 'id') {
searchFilter = (trial): boolean => trial.id.toUpperCase().includes(searchText.toUpperCase());
} else if (searchType === 'trialnum') {
searchFilter = (trial): boolean => trial.sequenceId.toString() === searchText;
} else if (searchType === 'status') {
searchFilter = (trial): boolean => trial.status.toUpperCase().includes(searchText.toUpperCase());
} else if (searchType === 'parameters') {
// TODO: support filters like `x: 2` (instead of `'x': 2`)
searchFilter = (trial): boolean => JSON.stringify(trial.description.parameters).includes(searchText);
}
}
return trials.filter(searchFilter);
}
StartTimeColumnConfig: any = {
name: 'Start time',
key: 'startTime',
fieldName: 'startTime',
minWidth: 150,
maxWidth: 400,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
onRender: (record): React.ReactNode => <span>{formatTimestamp(record.startTime)}</span>
};
private _updateSearchFilterType(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption | undefined): void {
if (item !== undefined) {
const value = item.key.toString();
if (searchOptionLiterals.hasOwnProperty(value)) {
this.setState({ searchType: value as SearchOptionType }, this._updateTableSource);
}
}
}
EndTimeColumnConfig: any = {
name: 'End time',
key: 'endTime',
fieldName: 'endTime',
minWidth: 200,
maxWidth: 400,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
onRender: (record): React.ReactNode => <span>{formatTimestamp(record.endTime, '--')}</span>
};
private _updateSearchText(ev: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ searchText: ev.target.value }, this._updateTableSource);
}
DurationColumnConfig: any = {
name: 'Duration',
key: 'duration',
fieldName: 'duration',
minWidth: 150,
maxWidth: 300,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
onRender: (record): React.ReactNode => <span className='durationsty'>{convertDuration(record.duration)}</span>
};
/* Table basic function related methods */
StatusColumnConfig: any = {
name: 'Status',
key: 'status',
fieldName: 'status',
className: 'tableStatus',
minWidth: 150,
maxWidth: 250,
isResizable: true,
data: 'string',
onColumnClick: this.onColumnClick,
onRender: (record): React.ReactNode => <span className={`${record.status} commonStyle`}>{record.status}</span>
};
private _onColumnClick(ev: React.MouseEvent<HTMLElement>, column: IColumn): void {
// handle the click events on table header (do sorting)
const { columns } = this.state;
const newColumns: IColumn[] = columns.slice();
const currColumn: IColumn = newColumns.filter(currCol => column.key === currCol.key)[0];
const isSortedDescending = !currColumn.isSortedDescending;
this.setState(
{
sortInfo: { field: column.key, isDescend: isSortedDescending }
},
this._updateTableSource
);
}
IntermediateCountColumnConfig: any = {
name: 'Intermediate result',
dataIndex: 'intermediateCount',
fieldName: 'intermediateCount',
minWidth: 150,
maxWidth: 200,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
onRender: (record): React.ReactNode => <span>{`#${record.intermediateCount}`}</span>
private _trialsToTableItems(trials: TableObj[]): any[] {
// TODO: use search space and metrics space from TRIALS will cause update issues.
const searchSpace = TRIALS.inferredSearchSpace(EXPERIMENT.searchSpaceNew);
const metricSpace = TRIALS.inferredMetricSpace();
const items = trials.map(trial => {
const ret = {
sequenceId: trial.sequenceId,
id: trial.id,
startTime: (trial as Trial).info.startTime, // FIXME: why do we need info here?
endTime: (trial as Trial).info.endTime,
duration: trial.duration,
status: trial.status,
intermediateCount: trial.intermediates.length,
_expandDetails: this._expandedTrialIds.has(trial.id) // hidden field names should start with `_`
};
showIntermediateModal = async (record: TrialJobInfo, event: React.SyntheticEvent<EventTarget>): Promise<void> => {
event.preventDefault();
event.stopPropagation();
const res = await axios.get(`${MANAGER_IP}/metric-data/${record.id}`);
if (res.status === 200) {
const intermediateArr: number[] = [];
// support intermediate result is dict because the last intermediate result is
// final result in a succeed trial, it may be a dict.
// get intermediate result dict keys array
const { intermediateKey } = this.state;
const otherkeys: string[] = [];
const metricDatas = res.data;
if (metricDatas.length !== 0) {
// just add type=number keys
const intermediateMetrics = parseMetrics(metricDatas[0].data);
for (const key in intermediateMetrics) {
if (typeof intermediateMetrics[key] === 'number') {
otherkeys.push(key);
}
}
}
// intermediateArr just store default val
metricDatas.map(item => {
if (item.type === 'PERIODICAL') {
const temp = parseMetrics(item.data);
if (typeof temp === 'object') {
intermediateArr.push(temp[intermediateKey]);
} else {
intermediateArr.push(temp);
for (const [k, v] of trial.parameters(searchSpace)) {
ret[`space/${k.baseName}`] = v;
}
for (const [k, v] of trial.metrics(metricSpace)) {
ret[`metric/${k.baseName}`] = v;
}
ret['latestAccuracy'] = (trial as Trial).latestAccuracy;
ret['_formattedLatestAccuracy'] = (trial as Trial).formatLatestAccuracy();
return ret;
});
const intermediate = intermediateGraphOption(intermediateArr, record.id);
this.setState({
intermediateData: res.data, // store origin intermediate data for a trial
intermediateOption: intermediate,
intermediateOtherKeys: otherkeys,
intermediateId: record.id
});
}
this.setState({ modalVisible: true });
};
// intermediate button click -> intermediate graph for each trial
// support intermediate is dict
selectOtherKeys = (event: React.FormEvent<HTMLDivElement>, item?: IDropdownOption): void => {
if (item !== undefined) {
const value = item.text;
const isShowDefault: boolean = value === 'default' ? true : false;
const { intermediateData, intermediateId } = this.state;
const intermediateArr: number[] = [];
// just watch default key-val
if (isShowDefault === true) {
Object.keys(intermediateData).map(item => {
if (intermediateData[item].type === 'PERIODICAL') {
const temp = parseMetrics(intermediateData[item].data);
if (typeof temp === 'object') {
intermediateArr.push(temp[value]);
const { sortInfo } = this.state;
if (sortInfo.field !== '') {
return _copyAndSort(items, sortInfo.field, sortInfo.isDescend);
} else {
intermediateArr.push(temp);
return items;
}
}
});
private _buildColumnsFromTableItems(tableItems: any[]): IColumn[] {
// extra column, for a icon to expand the trial details panel
const columns: IColumn[] = [
{
key: '_expand',
name: '',
onRender: (item, index): any => {
return (
<Icon
aria-hidden={true}
iconName='ChevronRight'
styles={{
root: {
transition: 'all 0.2s',
transform: `rotate(${item._expandDetails ? 90 : 0}deg)`
}
}}
onClick={(event): void => {
event.stopPropagation();
const newItem: any = { ...item, _expandDetails: !item._expandDetails };
if (newItem._expandDetails) {
// preserve to be restored when refreshed
this._expandedTrialIds.add(newItem.id);
} else {
Object.keys(intermediateData).map(item => {
const temp = parseMetrics(intermediateData[item].data);
if (typeof temp === 'object') {
intermediateArr.push(temp[value]);
this._expandedTrialIds.delete(newItem.id);
}
const newItems = [...this.state.displayedItems];
newItems[index as number] = newItem;
this.setState({
displayedItems: newItems
});
}}
onMouseDown={(e): void => {
e.stopPropagation();
}}
onMouseUp={(e): void => {
e.stopPropagation();
}}
/>
);
},
fieldName: 'expand',
isResizable: false,
minWidth: 20,
maxWidth: 20
}
const intermediate = intermediateGraphOption(intermediateArr, intermediateId);
// re-render
this.setState({
intermediateKey: value,
intermediateOption: intermediate
];
// looking at the first row only for now
for (const k of Object.keys(tableItems[0])) {
if (k === 'metric/default') {
// FIXME: default metric is hacked as latestAccuracy currently
continue;
}
const lengths = tableItems.map(item => `${item[k]}`.length);
const avgLengths = lengths.reduce((a, b) => a + b) / lengths.length;
const columnTitle = _inferColumnTitle(k);
const columnWidth = Math.max(columnTitle.length, avgLengths);
// TODO: add blacklist
columns.push({
name: columnTitle,
key: k,
fieldName: k,
minWidth: columnWidth * 13,
maxWidth: columnWidth * 18,
isResizable: true,
onColumnClick: this._onColumnClick.bind(this),
...(k === 'status' && {
// color status
onRender: (record): React.ReactNode => (
<span className={`${record.status} commonStyle`}>{record.status}</span>
)
}),
...((k.startsWith('metric/') || k.startsWith('space/')) && {
// show tooltip
onRender: (record): React.ReactNode => (
<TooltipHost content={record[k]}>
<div className='ellipsis'>{record[k]}</div>
</TooltipHost>
)
}),
...(k === 'latestAccuracy' && {
// FIXME: this is ad-hoc
onRender: (record): React.ReactNode => (
<TooltipHost content={record._formattedLatestAccuracy}>
<div className='ellipsis'>{record._formattedLatestAccuracy}</div>
</TooltipHost>
)
}),
...(['startTime', 'endTime'].includes(k) && {
onRender: (record): React.ReactNode => <span>{formatTimestamp(record[k], '--')}</span>
}),
...(k === 'duration' && {
onRender: (record): React.ReactNode => (
<span className='durationsty'>{convertDuration(record[k])}</span>
)
})
});
}
};
hideIntermediateModal = (): void => {
this.setState({
modalVisible: false
// operations column
columns.push({
name: 'Operation',
key: '_operation',
fieldName: 'operation',
minWidth: 160,
maxWidth: 200,
isResizable: true,
className: 'detail-table',
onRender: this._renderOperationColumn.bind(this)
});
};
hideShowColumnModal = (): void => {
this.setState(() => ({ isShowColumn: false }));
};
// click add column btn, just show the modal of addcolumn
addColumn = (): void => {
// show user select check button
this.setState(() => ({ isShowColumn: true }));
};
fillSelectedRowsTostate = (selected: number[] | string[], selectedRows: Array<TableRecord>): void => {
this.setState({ selectRows: selectedRows, selectedRowKeys: selected });
};
// open Compare-modal
compareBtn = (): void => {
const { selectRows } = this.state;
if (selectRows.length === 0) {
alert('Please select datas you want to compare!');
const { sortInfo } = this.state;
for (const column of columns) {
if (column.key === sortInfo.field) {
column.isSorted = true;
column.isSortedDescending = sortInfo.isDescend;
} else {
this.setState({ isShowCompareModal: true });
column.isSorted = false;
column.isSortedDescending = true;
}
}
return columns;
}
};
// close Compare-modal
hideCompareModal = (): void => {
// close modal. clear select rows data, clear selected track
this.setState({ isShowCompareModal: false, selectedRowKeys: [], selectRows: [] });
};
// open customized trial modal
private setCustomizedTrial = (trialId: string, event: React.SyntheticEvent<EventTarget>): void => {
event.preventDefault();
event.stopPropagation();
private _updateTableSource(): void {
// call this method when trials or the computation of trial filter has changed
const items = this._trialsToTableItems(this._filterTrials(this.props.tableSource));
if (items.length > 0) {
const columns = this._buildColumnsFromTableItems(items);
this.setState({
isShowCustomizedModal: true,
copyTrialId: trialId
displayedItems: items,
columns: columns
});
};
private closeCustomizedTrial = (): void => {
} else {
this.setState({
isShowCustomizedModal: false,
copyTrialId: ''
displayedItems: [],
columns: []
});
};
private onWindowResize = (): void => {
this.setState(() => ({
modalIntermediateHeight: window.innerHeight,
modalIntermediateWidth: window.innerWidth
}));
};
private onRenderRow: IDetailsListProps['onRenderRow'] = props => {
if (props) {
return <Details detailsProps={props} />;
}
return null;
};
private getSelectedRows = new Selection({
onSelectionChanged: (): void => {
this.setState(() => ({ selectRows: this.getSelectedRows.getSelection() }));
}
});
// trial parameters & dict final keys & Trial No. Id ...
private getAllColumnKeys = (): string[] => {
const tableSource: Array<TableRecord> = JSON.parse(JSON.stringify(this.props.tableSource));
// parameter as table column
const parameterStr: string[] = [];
if (!EXPERIMENT.isNestedExp()) {
if (tableSource.length > 0) {
const trialMess = TRIALS.getTrial(tableSource[0].id);
const trial = trialMess.description.parameters;
const parameterColumn: string[] = Object.keys(trial);
parameterColumn.forEach(value => {
parameterStr.push(`${value} (search space)`);
private _updateDisplayedColumns(displayedColumns: string[]): void {
this.setState({
displayedColumns: displayedColumns
});
}
}
// concat trial all final keys and remove dup "default" val, return list
const finalKeysList = TRIALS.finalKeys().filter(item => item !== 'default');
return COLUMNPro.concat(parameterStr).concat(finalKeysList);
};
// get IColumn[]
// when user click [Add Column] need to use the function
private initTableColumnList = (columnList: string[]): IColumn[] => {
// const { columnList } = this.props;
private _renderOperationColumn(record: any): React.ReactNode {
const runningTrial: boolean = ['RUNNING', 'UNKNOWN'].includes(record.status) ? false : true;
const disabledAddCustomizedTrial = ['DONE', 'ERROR', 'STOPPED'].includes(EXPERIMENT.status);
const showColumn: IColumn[] = [];
for (const item of columnList) {
const paraColumn = item.match(/ \(search space\)$/);
let result;
if (paraColumn !== null) {
result = paraColumn.input;
}
switch (item) {
case 'Trial No.':
showColumn.push(this.SequenceIdColumnConfig);
break;
case 'ID':
showColumn.push(this.IdColumnConfig);
break;
case 'Start time':
showColumn.push(this.StartTimeColumnConfig);
break;
case 'End time':
showColumn.push(this.EndTimeColumnConfig);
break;
case 'Duration':
showColumn.push(this.DurationColumnConfig);
break;
case 'Status':
showColumn.push(this.StatusColumnConfig);
break;
case 'Intermediate result':
showColumn.push(this.IntermediateCountColumnConfig);
break;
case 'Default':
showColumn.push(this.AccuracyColumnConfig);
break;
case 'Operation':
showColumn.push({
name: 'Operation',
key: 'operation',
fieldName: 'operation',
minWidth: 160,
maxWidth: 200,
isResizable: true,
className: 'detail-table',
onRender: (record: any) => {
const trialStatus = record.status;
const flag: boolean = trialStatus === 'RUNNING' || trialStatus === 'UNKNOWN' ? false : true;
return (
<Stack className='detail-button' horizontal>
{/* see intermediate result graph */}
<PrimaryButton
className='detail-button-operation'
title='Intermediate'
onClick={this.showIntermediateModal.bind(this, record)}
onClick={(): void => {
const { tableSource } = this.props;
const trial = tableSource.find(trial => trial.id === record.id) as TableObj;
this.setState({ intermediateDialogTrial: trial });
}}
>
{LineChart}
</PrimaryButton>
{/* kill job */}
{flag ? (
{runningTrial ? (
<PrimaryButton className='detail-button-operation' disabled={true} title='kill'>
{blocked}
</PrimaryButton>
) : (
<KillJob trial={record} />
)}
{/* Add a new trial-customized trial */}
<PrimaryButton
className='detail-button-operation'
title='Customized trial'
onClick={this.setCustomizedTrial.bind(this, record.id)}
onClick={(): void => {
this.setState({ copiedTrialId: record.id });
}}
disabled={disabledAddCustomizedTrial}
>
{copy}
......@@ -508,286 +423,138 @@ class TableList extends React.Component<TableListProps, TableListState> {
</Stack>
);
}
});
break;
case result:
// remove SEARCH_SPACE title
// const realItem = item.replace(' (search space)', '');
showColumn.push({
name: item.replace(' (search space)', ''),
key: item,
fieldName: item,
minWidth: 150,
onRender: (record: TableRecord) => {
const eachTrial = TRIALS.getTrial(record.id);
return <span>{eachTrial.description.parameters[item.replace(' (search space)', '')]}</span>;
}
});
break;
default:
showColumn.push({
name: item,
key: item,
fieldName: item,
minWidth: 100,
onRender: (record: TableRecord) => {
const accDictionary = record.accDictionary;
let other = '';
if (accDictionary !== undefined) {
other = accDictionary[item].toString();
}
return (
<TooltipHost content={other}>
<div className='ellipsis'>{other}</div>
</TooltipHost>
);
}
});
}
}
return showColumn;
};
componentDidMount(): void {
window.addEventListener('resize', this.onWindowResize);
this.updateData();
}
componentDidUpdate(prevProps: TableListProps): void {
if (
this.props.columnList !== prevProps.columnList ||
this.props.tableSource !== prevProps.tableSource ||
prevProps.trialsUpdateBroadcast !== this.props.trialsUpdateBroadcast
) {
const { columnList } = this.props;
this.setState(
{
tableColumns: this.initTableColumnList(columnList),
allColumnList: this.getAllColumnKeys()
},
() => {
this.updateData();
}
);
if (this.props.tableSource !== prevProps.tableSource) {
this._updateTableSource();
}
}
// slice all table data into current page data
updateData(): void {
const tableSource: Array<TableRecord> = this.props.tableSource;
const { offset, perPage, sortMessage } = this.state;
if (sortMessage.field !== '') {
tableSource.sort(function(a, b): any {
if (
a[sortMessage.field] === undefined ||
Object.is(a[sortMessage.field], NaN) ||
Object.is(a[sortMessage.field], Infinity) ||
Object.is(a[sortMessage.field], -Infinity) ||
typeof a[sortMessage.field] === 'object'
) {
return 1;
}
if (
b[sortMessage.field] === undefined ||
Object.is(b[sortMessage.field], NaN) ||
Object.is(b[sortMessage.field], Infinity) ||
Object.is(b[sortMessage.field], -Infinity) ||
typeof b[sortMessage.field] === 'object'
) {
return -1;
}
return (sortMessage.isDescend
? a[sortMessage.field] < b[sortMessage.field]
: a[sortMessage.field] > b[sortMessage.field])
? 1
: -1;
});
}
const tableSlice = tableSource.slice(offset, offset + perPage);
const curPageCount = Math.ceil(tableSource.length / perPage);
this.setState({
tablePerPage: tableSlice,
pageCount: curPageCount
});
}
// update data when click the page index of pagination
handlePageClick = (evt: any): void => {
const selectedPage = evt.selected;
const offset = selectedPage * this.state.perPage;
this.setState(
{
currentPage: selectedPage,
offset: offset
},
() => {
this.updateData();
}
);
};
// update per page items when click the dropdown of pagination
updatePerPage = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption | undefined): void => {
const { pageCount } = this.state;
if (item !== undefined) {
const currentPerPage = item.key === 'all' ? this.props.tableSource.length : Number(item.key);
const currentPageCount = this.props.tableSource.length <= currentPerPage ? 1 : pageCount;
this.setState(
{
perPage: currentPerPage,
offset: 0,
currentPage: 0,
pageCount: currentPageCount
},
() => {
this.updateData();
}
);
componentDidMount(): void {
this._updateTableSource();
}
};
render(): React.ReactNode {
const {
intermediateKey,
modalIntermediateWidth,
modalIntermediateHeight,
tableColumns,
allColumnList,
isShowColumn,
modalVisible,
selectRows,
isShowCompareModal,
intermediateOtherKeys,
isShowCustomizedModal,
copyTrialId,
intermediateOption,
tablePerPage
displayedItems,
columns,
searchType,
customizeColumnsDialogVisible,
compareDialogVisible,
displayedColumns,
selectedRowIds,
intermediateDialogTrial,
copiedTrialId
} = this.state;
const { columnList } = this.props;
const perPageOptions = [
{ key: '10', text: '10 items per page' },
{ key: '20', text: '20 items per page' },
{ key: '50', text: '50 items per page' },
{ key: 'all', text: 'All items' }
];
return (
<Stack>
<div id='tableList'>
<DetailsList
columns={tableColumns}
items={tablePerPage}
setKey='set'
compact={true}
onRenderRow={this.onRenderRow}
layoutMode={DetailsListLayoutMode.justified}
selectionMode={SelectionMode.multiple}
selection={this.getSelectedRows}
<Stack horizontal className='panelTitle' style={{ marginTop: 10 }}>
<span style={{ marginRight: 12 }}>{tableListIcon}</span>
<span>Trial jobs</span>
</Stack>
<Stack horizontal className='allList'>
<StackItem grow={50}>
<DefaultButton
text='Compare'
className='allList-compare'
onClick={(): void => {
this.setState({ compareDialogVisible: true });
}}
disabled={selectedRowIds.length === 0}
/>
</StackItem>
<StackItem grow={50}>
<Stack horizontal horizontalAlign='end' className='allList'>
<DefaultButton
className='allList-button-gap'
text='Add/Remove columns'
onClick={(): void => {
this.setState({ customizeColumnsDialogVisible: true });
}}
/>
<Stack
horizontal
horizontalAlign='end'
verticalAlign='baseline'
styles={{ root: { padding: 10 } }}
tokens={horizontalGapStackTokens}
>
<Dropdown
selectedKey={
this.state.perPage === this.props.tableSource.length
? 'all'
: String(this.state.perPage)
}
options={perPageOptions}
onChange={this.updatePerPage}
styles={{ dropdown: { width: 150 } }}
selectedKey={searchType}
options={Object.entries(searchOptionLiterals).map(([k, v]) => ({
key: k,
text: v
}))}
onChange={this._updateSearchFilterType.bind(this)}
styles={{ root: { width: 150 } }}
/>
<ReactPaginate
previousLabel={'<'}
nextLabel={'>'}
breakLabel={'...'}
breakClassName={'break'}
pageCount={this.state.pageCount}
marginPagesDisplayed={2}
pageRangeDisplayed={2}
onPageChange={this.handlePageClick}
containerClassName={this.props.tableSource.length == 0 ? 'pagination hidden' : 'pagination'}
subContainerClassName={'pages pagination'}
disableInitialCallback={false}
activeClassName={'active'}
forcePage={this.state.currentPage}
<input
type='text'
className='allList-search-input'
placeholder={`Search by ${
['id', 'trialnum'].includes(searchType)
? searchOptionLiterals[searchType]
: searchType
}`}
onChange={this._updateSearchText.bind(this)}
style={{ width: 230 }}
/>
</Stack>
</div>
{/* Intermediate Result Modal */}
<Modal
isOpen={modalVisible}
onDismiss={this.hideIntermediateModal}
containerClassName={contentStyles.container}
>
<div className={contentStyles.header}>
<span>Intermediate result</span>
<IconButton
styles={iconButtonStyles}
iconProps={{ iconName: 'Cancel' }}
ariaLabel='Close popup modal'
onClick={this.hideIntermediateModal as any}
</StackItem>
</Stack>
{columns && displayedItems && (
<PaginationTable
columns={columns.filter(
column =>
displayedColumns.includes(column.key) || ['_expand', '_operation'].includes(column.key)
)}
items={displayedItems}
compact={true}
selection={this._selection}
selectionMode={SelectionMode.multiple}
selectionPreservedOnEmptyClick={true}
onRenderRow={(props): any => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return <ExpandableDetails detailsProps={props!} isExpand={props!.item._expandDetails} />;
}}
/>
</div>
{intermediateOtherKeys.length > 1 ? (
<Stack horizontalAlign='end' className='selectKeys'>
<Dropdown
className='select'
selectedKey={intermediateKey}
options={intermediateOtherKeys.map((key, item) => {
return {
key: key,
text: intermediateOtherKeys[item]
};
})}
onChange={this.selectOtherKeys}
)}
{compareDialogVisible && (
<Compare
title='Compare trials'
showDetails={true}
trials={this.props.tableSource.filter(trial => selectedRowIds.includes(trial.id))}
onHideDialog={(): void => {
this.setState({ compareDialogVisible: false });
}}
/>
</Stack>
) : null}
<div className='intermediate-graph'>
<ReactEcharts
option={intermediateOption}
style={{
width: 0.5 * modalIntermediateWidth,
height: 0.7 * modalIntermediateHeight,
maxHeight: 534,
padding: 20
)}
{intermediateDialogTrial !== undefined && (
<Compare
title='Intermediate results'
showDetails={false}
trials={[intermediateDialogTrial]}
onHideDialog={(): void => {
this.setState({ intermediateDialogTrial: undefined });
}}
theme='my_theme'
/>
<div className='xAxis'>#Intermediate result</div>
</div>
</Modal>
{/* Add Column Modal */}
{isShowColumn && (
)}
{customizeColumnsDialogVisible && (
<ChangeColumnComponent
hideShowColumnDialog={this.hideShowColumnModal}
isHideDialog={!isShowColumn}
showColumn={allColumnList}
selectedColumn={columnList}
changeColumn={this.props.changeColumn}
selectedColumns={displayedColumns}
allColumns={columns
.filter(column => !column.key.startsWith('_'))
.map(column => ({ key: column.key, name: column.name }))}
onSelectedChange={this._updateDisplayedColumns.bind(this)}
onHideDialog={(): void => {
this.setState({ customizeColumnsDialogVisible: false });
}}
/>
)}
{/* compare trials based message */}
{isShowCompareModal && <Compare compareStacks={selectRows} cancelFunc={this.hideCompareModal} />}
{/* clone trial parameters and could submit a customized trial */}
{/* Clone a trial and customize a set of new parameters */}
{/* visible is done inside because prompt is needed even when the dialog is closed */}
<Customize
visible={isShowCustomizedModal}
copyTrialId={copyTrialId}
closeCustomizeModal={this.closeCustomizedTrial}
visible={copiedTrialId !== undefined}
copyTrialId={copiedTrialId || ''}
closeCustomizeModal={(): void => {
this.setState({ copiedTrialId: undefined });
}}
/>
</Stack>
</div>
);
}
}
......
......@@ -33,6 +33,7 @@ interface TableObj {
color?: string;
startTime?: number;
endTime?: number;
intermediates: (MetricDataRecord | undefined)[];
parameters(axes: MultipleAxes): Map<SingleAxis, any>;
metrics(axes: MultipleAxes): Map<SingleAxis, any>;
}
......
......@@ -60,7 +60,7 @@ function inferTrialParameters(
class Trial implements TableObj {
private metricsInitialized: boolean = false;
private infoField: TrialJobInfo | undefined;
private intermediates: (MetricDataRecord | undefined)[] = [];
public intermediates: (MetricDataRecord | undefined)[] = [];
public final: MetricDataRecord | undefined;
private finalAcc: number | undefined;
......@@ -224,24 +224,29 @@ class Trial implements TableObj {
}
public parameters(axes: MultipleAxes): Map<SingleAxis, any> {
const ret = new Map<SingleAxis, any>(Array.from(axes.axes.values()).map(k => [k, null]));
if (this.info === undefined || this.info.hyperParameters === undefined) {
throw new Map();
throw ret;
} else {
const tempHyper = this.info.hyperParameters;
let params = JSON.parse(tempHyper[tempHyper.length - 1]).parameters;
if (typeof params === 'string') {
params = JSON.parse(params);
}
const [result, unexpectedEntries] = inferTrialParameters(params, axes);
const [updated, unexpectedEntries] = inferTrialParameters(params, axes);
if (unexpectedEntries.size) {
throw unexpectedEntries;
}
return result;
for (const [k, v] of updated) {
ret.set(k, v);
}
return ret;
}
}
public metrics(space: MultipleAxes): Map<SingleAxis, any> {
const ret = new Map<SingleAxis, any>();
// set default value: null
const ret = new Map<SingleAxis, any>(Array.from(space.axes.values()).map(k => [k, null]));
const unexpectedEntries = new Map<string, any>();
if (this.acc === undefined) {
return ret;
......
......@@ -58,7 +58,7 @@
}
.detail-table {
padding: 5px 0 0 0;
padding-top: 5px;
}
.columns-height {
......
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