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

Improve search parameters on trial detail page #3651

Merged
merged 9 commits into from
May 26, 2021
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
2 changes: 1 addition & 1 deletion ts/webui/scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
}

// Tools like Cloud9 rely on this.
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 8000;
const HOST = process.env.HOST || '0.0.0.0';

if (process.env.HOST) {
Expand Down
9 changes: 9 additions & 0 deletions ts/webui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export const AppContext = React.createContext({
// eslint-disable-next-line @typescript-eslint/no-empty-function
updateOverviewPage: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
updateDetailPage: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
changeExpandRowIDs: (_val: string, _type?: string): void => {}
});

Expand Down Expand Up @@ -133,6 +135,12 @@ class App extends React.Component<{}, AppState> {
}));
};

updateDetailPage = (): void => {
this.setState(state => ({
trialsUpdateBroadcast: state.trialsUpdateBroadcast + 1
}));
};

shouldComponentUpdate(nextProps: any, nextState: AppState): boolean {
if (!(nextState.isUpdate || nextState.isUpdate === undefined)) {
nextState.isUpdate = true;
Expand Down Expand Up @@ -207,6 +215,7 @@ class App extends React.Component<{}, AppState> {
bestTrialEntries,
changeEntries: this.changeEntries,
updateOverviewPage: this.updateOverviewPage,
updateDetailPage: this.updateDetailPage,
expandRowIDs,
changeExpandRowIDs: this.changeExpandRowIDs
}}
Expand Down
5 changes: 1 addition & 4 deletions ts/webui/src/components/TrialsDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,7 @@ class TrialsDetail extends React.Component<{}, TrialDetailState> {
</div>
{/* trial table list */}
<div className='detailTable' style={{ marginTop: 10 }}>
<TableList
tableSource={source}
trialsUpdateBroadcast={this.context.trialsUpdateBroadcast}
/>
<TableList tableSource={source} updateDetailPage={this.context.updateDetailPage} />
</div>
</React.Fragment>
)}
Expand Down
7 changes: 7 additions & 0 deletions ts/webui/src/components/modals/ChildrenGap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IStackTokens } from '@fluentui/react';

const searchConditonsGap: IStackTokens = {
childrenGap: 10
};

export { searchConditonsGap };
131 changes: 45 additions & 86 deletions ts/webui/src/components/trial-detail/TableList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React from 'react';
import {
DefaultButton,
Dropdown,
IColumn,
Icon,
IDropdownOption,
PrimaryButton,
Stack,
StackItem,
Expand All @@ -14,13 +12,15 @@ import {
} from '@fluentui/react';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import { TOOLTIP_BACKGROUND_COLOR } from '../../static/const';
import { convertDuration, formatTimestamp, copyAndSort } from '../../static/function';
import { TableObj, SortInfo } from '../../static/interface';
import { convertDuration, formatTimestamp, copyAndSort, parametersType } from '../../static/function';
import { TableObj, SortInfo, SearchItems } from '../../static/interface';
import { getTrialsBySearchFilters } from './search/searchFunction';
import { blocked, copy, LineChart, tableListIcon } from '../buttons/Icon';
import ChangeColumnComponent from '../modals/ChangeColumnComponent';
import Compare from '../modals/Compare';
import Customize from '../modals/CustomizedTrial';
import TensorboardUI from '../modals/tensorboard/TensorboardUI';
import Search from './search/Search';
import KillJob from '../modals/Killjob';
import ExpandableDetails from '../public-child/ExpandableDetails';
import PaginationTable from '../public-child/PaginationTable';
Expand All @@ -41,12 +41,6 @@ require('echarts/lib/component/tooltip');
require('echarts/lib/component/title');

type SearchOptionType = 'id' | 'trialnum' | 'status' | 'parameters';
const searchOptionLiterals = {
id: 'ID',
trialnum: 'Trial No.',
status: 'Status',
parameters: 'Parameters'
};

const defaultDisplayedColumns = ['sequenceId', 'id', 'duration', 'status', 'latestAccuracy'];

Expand Down Expand Up @@ -76,7 +70,7 @@ function _inferColumnTitle(columnKey: string): string {

interface TableListProps {
tableSource: TableObj[];
trialsUpdateBroadcast: number;
updateDetailPage: () => void;
}

interface TableListState {
Expand All @@ -91,6 +85,8 @@ interface TableListState {
intermediateDialogTrial: TableObj | undefined;
copiedTrialId: string | undefined;
sortInfo: SortInfo;
searchItems: Array<SearchItems>;
relation: Map<string, string>;
}

class TableList extends React.Component<TableListProps, TableListState> {
Expand All @@ -114,47 +110,14 @@ class TableList extends React.Component<TableListProps, TableListState> {
selectedRowIds: [],
intermediateDialogTrial: undefined,
copiedTrialId: undefined,
sortInfo: { field: '', isDescend: true }
sortInfo: { field: '', isDescend: true },
searchItems: [],
relation: parametersType()
};

this._expandedTrialIds = new Set<string>();
}

/* Search related methods */

// 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);
}

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);
}
}
}

private _updateSearchText(ev: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ searchText: ev.target.value }, this._updateTableSource);
}

/* Table basic function related methods */

private _onColumnClick(ev: React.MouseEvent<HTMLElement>, column: IColumn): void {
Expand All @@ -180,7 +143,7 @@ class TableList extends React.Component<TableListProps, TableListState> {
const ret = {
sequenceId: trial.sequenceId,
id: trial.id,
checked: selectedRowIds.includes(trial.id) ? true : false,
_checked: selectedRowIds.includes(trial.id) ? true : false,
startTime: (trial as Trial).info.startTime, // FIXME: why do we need info here?
endTime: (trial as Trial).info.endTime,
duration: trial.duration,
Expand Down Expand Up @@ -221,7 +184,7 @@ class TableList extends React.Component<TableListProps, TableListState> {
}
items.forEach(item => {
if (item.id === id) {
item.checked = !!checked;
item._checked = !!checked;
}
});
this.setState(() => ({ displayedItems: items, selectedRowIds: temp }));
Expand All @@ -231,7 +194,7 @@ class TableList extends React.Component<TableListProps, TableListState> {
const { displayedItems } = this.state;
const newDisplayedItems = displayedItems;
newDisplayedItems.forEach(item => {
item.checked = false;
item._checked = false;
});
this.setState(() => ({
selectedRowIds: [],
Expand All @@ -253,7 +216,7 @@ class TableList extends React.Component<TableListProps, TableListState> {
onRender: (record): React.ReactNode => (
<Checkbox
label={undefined}
checked={record.checked}
checked={record._checked}
className='detail-check'
onChange={this.selectedTrialOnChangeEvent.bind(this, record.id)}
/>
Expand Down Expand Up @@ -438,7 +401,11 @@ class TableList extends React.Component<TableListProps, TableListState> {

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));
const { searchItems, relation } = this.state;
let items = this._trialsToTableItems(this.props.tableSource);
if (searchItems.length > 0) {
items = getTrialsBySearchFilters(items, searchItems, relation); // use search filter to filter data
}
if (items.length > 0) {
const columns = this._buildColumnsFromTableItems(items);
this.setState({
Expand Down Expand Up @@ -496,6 +463,12 @@ class TableList extends React.Component<TableListProps, TableListState> {
);
}

private changeSearchFilterList = (arr: Array<SearchItems>): void => {
this.setState(() => ({
searchItems: arr
}));
};

componentDidUpdate(prevProps: TableListProps): void {
if (this.props.tableSource !== prevProps.tableSource) {
this._updateTableSource();
Expand All @@ -510,13 +483,13 @@ class TableList extends React.Component<TableListProps, TableListState> {
const {
displayedItems,
columns,
searchType,
customizeColumnsDialogVisible,
compareDialogVisible,
displayedColumns,
selectedRowIds,
intermediateDialogTrial,
copiedTrialId
copiedTrialId,
searchItems
} = this.state;

return (
Expand All @@ -526,7 +499,24 @@ class TableList extends React.Component<TableListProps, TableListState> {
<span>Trial jobs</span>
</Stack>
<Stack horizontal className='allList'>
<StackItem grow={50}>
<StackItem>
<Stack horizontal horizontalAlign='end' className='allList'>
<Search
searchFilter={searchItems} // search filter list
changeSearchFilterList={this.changeSearchFilterList}
updatePage={this.props.updateDetailPage}
/>
</Stack>
</StackItem>

<StackItem styles={{ root: { position: 'absolute', right: '0' } }}>
<DefaultButton
className='allList-button-gap'
text='Add/Remove columns'
onClick={(): void => {
this.setState({ customizeColumnsDialogVisible: true });
}}
/>
<DefaultButton
text='Compare'
className='allList-compare'
Expand All @@ -540,37 +530,6 @@ class TableList extends React.Component<TableListProps, TableListState> {
changeSelectTrialIds={this.changeSelectTrialIds}
/>
</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 });
}}
/>
<Dropdown
selectedKey={searchType}
options={Object.entries(searchOptionLiterals).map(([k, v]) => ({
key: k,
text: v
}))}
onChange={this._updateSearchFilterType.bind(this)}
styles={{ root: { width: 150 } }}
/>
<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>
</StackItem>
</Stack>
{columns && displayedItems && (
<PaginationTable
Expand Down
82 changes: 82 additions & 0 deletions ts/webui/src/components/trial-detail/search/GeneralSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Stack, PrimaryButton } from '@fluentui/react';
import { searchConditonsGap } from '../../modals/ChildrenGap';
import { getSearchInputValueBySearchList } from './searchFunction';

// This file is for search trial ['Trial id', 'Trial No.']

function GeneralSearch(props): any {
// searchName val: Trial No. | Trial id
const { searchName, searchFilter, dismiss, changeSearchFilterList, setSearchInputVal, updatePage } = props;
const [firstInputVal, setFirstInputVal] = useState(getSearchNameInit());

function updateFirstInputVal(ev: React.ChangeEvent<HTMLInputElement>): void {
setFirstInputVal(ev.target.value);
}

function getSearchNameInit(): string {
let str = ''; // init ''
const find = searchFilter.find(item => item.name === searchName);

if (find !== undefined) {
str = find.value1; // init by filter value
}

return str;
}

function startFilterTrial(): void {
const { searchFilter } = props;
const searchFilterConditions = JSON.parse(JSON.stringify(searchFilter));
const find = searchFilterConditions.filter(item => item.name === searchName);

if (firstInputVal === '') {
alert('Please input related value!');
return;
}

if (find.length > 0) {
// change this record
// Trial id | Trial No. only need {search name, search value} these message
searchFilterConditions.forEach(item => {
if (item.name === searchName) {
item.value1 = firstInputVal;
// item.operator = '';
item.isChoice = false;
}
});
} else {
searchFilterConditions.push({
name: searchName,
// operator: '',
value1: firstInputVal,
isChoice: false
});
}
setSearchInputVal(getSearchInputValueBySearchList(searchFilterConditions));
changeSearchFilterList(searchFilterConditions);
updatePage();
dismiss(); // close menu
}

return (
// Trial id & Trial No.
<Stack horizontal className='filterConditions' tokens={searchConditonsGap}>
<span>{searchName === 'Trial id' ? 'Includes' : 'Equals to'}</span>
<input type='text' className='input input-padding' onChange={updateFirstInputVal} value={firstInputVal} />
<PrimaryButton text='Apply' className='btn-vertical-middle' onClick={startFilterTrial} />
</Stack>
);
}

GeneralSearch.propTypes = {
searchName: PropTypes.string,
searchFilter: PropTypes.array,
dismiss: PropTypes.func,
setSearchInputVal: PropTypes.func,
changeSearchFilterList: PropTypes.func,
updatePage: PropTypes.func
};

export default GeneralSearch;
Loading