Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

Chevron icon before table row and TableList refactoring #2900

Merged
merged 24 commits into from
Oct 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/webui/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
129 changes: 8 additions & 121 deletions src/webui/src/components/TrialsDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
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';
Expand All @@ -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> {
Expand All @@ -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
Expand Down Expand Up @@ -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>
Expand Down
129 changes: 48 additions & 81 deletions src/webui/src/components/modals/ChangeColumnComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
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 {
label: string;
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
};
}

Expand All @@ -38,110 +42,73 @@ 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 }));
}
// remove from source
const result = source.filter(item => item !== label);
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,
styles: { main: { maxWidth: 450 } }
}}
>
<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>
Expand Down
Loading