Improve search parameters on trial detail page (#3651)

Co-authored-by: Lijiao <Lijiaoa@outlook.com>
This commit is contained in:
Lijiaoa 2021-05-26 15:50:39 +08:00 коммит произвёл GitHub
Родитель 5df75c33cb
Коммит 4ccc94024f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 866 добавлений и 100 удалений

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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 => {}
});
@ -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;
@ -207,6 +215,7 @@ class App extends React.Component<{}, AppState> {
bestTrialEntries,
changeEntries: this.changeEntries,
updateOverviewPage: this.updateOverviewPage,
updateDetailPage: this.updateDetailPage,
expandRowIDs,
changeExpandRowIDs: this.changeExpandRowIDs
}}

Просмотреть файл

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

Просмотреть файл

@ -0,0 +1,7 @@
import { IStackTokens } from '@fluentui/react';
const searchConditonsGap: IStackTokens = {
childrenGap: 10
};
export { searchConditonsGap };

Просмотреть файл

@ -1,10 +1,8 @@
import React from 'react';
import {
DefaultButton,
Dropdown,
IColumn,
Icon,
IDropdownOption,
PrimaryButton,
Stack,
StackItem,
@ -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';
@ -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'];
@ -76,7 +70,7 @@ function _inferColumnTitle(columnKey: string): string {
interface TableListProps {
tableSource: TableObj[];
trialsUpdateBroadcast: number;
updateDetailPage: () => void;
}
interface TableListState {
@ -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> {
@ -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 {
@ -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,
@ -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 }));
@ -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: [],
@ -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)}
/>
@ -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({
@ -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();
@ -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 (
@ -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'
@ -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

Просмотреть файл

@ -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;

Просмотреть файл

@ -0,0 +1,259 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
Stack,
DefaultButton,
IContextualMenuProps,
IContextualMenuItem,
DirectionalHint,
SearchBox
} from '@fluentui/react';
import { EXPERIMENT } from '../../../static/datamodel';
import { SearchItems } from '../../../static/interface';
import SearchParameterConditions from './SearchParameterConditions';
import GeneralSearch from './GeneralSearch';
import { classNames, isChoiceType } from './searchFunction';
// TableList search layout
function Search(props): any {
const { searchFilter, changeSearchFilterList, updatePage } = props;
const [searchInputVal, setSearchInputVal] = useState('');
function getSearchMenu(parameterList): IContextualMenuProps {
const menu: Array<object> = [];
parameterList.unshift('StatusNNI');
['Trial id', 'Trial No.'].forEach(item => {
menu.push({
key: item,
text: item,
subMenuProps: {
items: [
{
key: item,
text: item,
// component: GeneralSearch.tsx
onRender: renderIdAndNoComponent.bind(item)
}
]
}
});
});
parameterList.forEach(item => {
menu.push({
key: item,
text: item === 'StatusNNI' ? 'Status' : item,
subMenuProps: {
items: [
{
key: item,
text: item,
// component: SearchParameterConditions.tsx
onRender: renderParametersSearchComponent.bind(item)
}
]
}
});
});
const filterMenu: IContextualMenuProps = {
shouldFocusOnMount: true,
directionalHint: DirectionalHint.bottomLeftEdge,
className: classNames.menu,
items: menu as any
};
return filterMenu;
}
// Avoid nested experiments, nested experiments do not support hyperparameter search
const searchMenuProps: IContextualMenuProps = getSearchMenu(
EXPERIMENT.isNestedExp() ? [] : Object.keys(EXPERIMENT.searchSpace)
);
function renderParametersSearchComponent(item: IContextualMenuItem, dismissMenu: () => void): JSX.Element {
return (
<SearchParameterConditions
parameter={item.text}
searchFilter={searchFilter} // search filter list
changeSearchFilterList={changeSearchFilterList}
updatePage={updatePage}
setSearchInputVal={setSearchInputVal}
dismiss={dismissMenu} // close menu
/>
);
}
function renderIdAndNoComponent(item: IContextualMenuItem, dismissMenu: () => void): JSX.Element {
return (
<GeneralSearch
searchName={item.text}
searchFilter={searchFilter} // search fliter list
changeSearchFilterList={changeSearchFilterList}
setSearchInputVal={setSearchInputVal}
updatePage={updatePage}
dismiss={dismissMenu} // after click Apply button to close menu
/>
);
}
function updateSearchText(_, newValue): void {
setSearchInputVal(newValue);
}
// update TableList page
function changeTableListPage(searchFilterList: Array<SearchItems>): void {
changeSearchFilterList(searchFilterList);
updatePage();
}
// "[hello, world]", JSON.parse(it) doesn't work so write this function
function convertStringArrToList(str: string): string[] {
const value = str.slice(1, str.length - 1); // delete []
// delete ""
const result: string[] = [];
if (value.includes(',')) {
const arr = value.split(',');
arr.forEach(item => {
if (item !== '') {
result.push(item);
}
});
return result;
} else {
if (value === '') {
return result;
} else {
return [value];
}
}
}
// SearchBox onSearch event: Filter based on the filter criteria entered by the user
function startFilter(): void {
const regEn = /`~!@#$%^&*()+?"{}.'/im;
const regCn = /·!#¥(——):;“”‘、,|《。》?、【】[\]]/im;
if (regEn.test(searchInputVal) || regCn.test(searchInputVal)) {
alert('Please delete special characters in the conditions!');
return;
}
// according [input val] to change searchFilter list
const allFilterConditions = searchInputVal.trim().split(';');
const newSearchFilter: any = [];
// delete '' in filter list
if (allFilterConditions.includes('')) {
allFilterConditions.splice(
allFilterConditions.findIndex(item => item === ''),
1
);
}
allFilterConditions.forEach(eachFilterConditionStr => {
let eachFilterConditionArr: string[] = [];
// EXPERIMENT.searchSpace[parameter]._type === 'choice'
if (eachFilterConditionStr.includes('>' || '<')) {
const operator = eachFilterConditionStr.includes('>') === true ? '>' : '<';
eachFilterConditionArr = eachFilterConditionStr.trim().split(operator);
newSearchFilter.push({
name: eachFilterConditionArr[0],
operator: operator,
value1: eachFilterConditionArr[1],
value2: '',
choice: [],
isChoice: false
});
} else if (eachFilterConditionStr.includes('≠')) {
// drop_rate≠6; status≠[x,xx,xxx]; conv_size≠[3,7]
eachFilterConditionArr = eachFilterConditionStr.trim().split('≠');
const filterName = eachFilterConditionArr[0] === 'Status' ? 'StatusNNI' : eachFilterConditionArr[0];
const isChoicesType = isChoiceType(filterName);
newSearchFilter.push({
name: filterName,
operator: '≠',
value1: isChoicesType ? '' : JSON.parse(eachFilterConditionArr[1]),
value2: '',
choice: isChoicesType ? convertStringArrToList(eachFilterConditionArr[1]) : [],
isChoice: isChoicesType ? true : false
});
} else {
// = : conv_size:[1,2,3,4]; Trial id:3; hidden_size:[1,2], status:[val1,val2,val3]
eachFilterConditionArr = eachFilterConditionStr.trim().split(':');
const filterName = eachFilterConditionArr[0] === 'Status' ? 'StatusNNI' : eachFilterConditionArr[0];
const isChoicesType = isChoiceType(filterName);
const isArray =
eachFilterConditionArr.length > 1 && eachFilterConditionArr[1].includes('[' || ']') ? true : false;
if (isArray === true) {
if (isChoicesType === true) {
// status:[SUCCEEDED]
newSearchFilter.push({
name: filterName,
operator: '=',
value1: '',
value2: '',
choice: convertStringArrToList(eachFilterConditionArr[1]),
isChoice: true
});
} else {
// drop_rate:[1,10]
newSearchFilter.push({
name: eachFilterConditionArr[0],
operator: 'between',
value1: JSON.parse(eachFilterConditionArr[1])[0],
value2: JSON.parse(eachFilterConditionArr[1])[1],
choice: [],
isChoice: false
});
}
} else {
newSearchFilter.push({
name: eachFilterConditionArr[0],
operator: '=',
value1: eachFilterConditionArr[1],
value2: '',
choice: [],
isChoice: false
});
}
}
});
changeTableListPage(newSearchFilter);
}
// clear search input all value, clear all search filter
function clearFliter(): void {
changeTableListPage([]);
}
return (
<div>
<Stack horizontal>
<DefaultButton text='Filter' menuProps={searchMenuProps} />
{/* search input: store filter conditons, also, user could input filter conditions, could search */}
<SearchBox
styles={{ root: { width: 530 } }}
placeholder='Search'
onChange={updateSearchText}
value={searchInputVal}
onSearch={startFilter}
onEscape={clearFliter}
onClear={clearFliter}
/>
</Stack>
</div>
);
}
Search.propTypes = {
searchFilter: PropTypes.array,
changeSearchFilterList: PropTypes.func,
updatePage: PropTypes.func
};
export default Search;

Просмотреть файл

@ -0,0 +1,197 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Stack, PrimaryButton, Dropdown, IDropdownOption } from '@fluentui/react';
import { EXPERIMENT } from '../../../static/datamodel';
import { getDropdownOptions, getSearchInputValueBySearchList } from './searchFunction';
import { searchConditonsGap } from '../../modals/ChildrenGap';
// This file is for filtering trial parameters and trial status
function SearchParameterConditions(props): any {
const { parameter, searchFilter, dismiss, changeSearchFilterList, updatePage, setSearchInputVal } = props;
const isChoiceTypeSearchFilter = parameter === 'StatusNNI' || EXPERIMENT.searchSpace[parameter]._type === 'choice';
const operatorList = isChoiceTypeSearchFilter ? ['=', '≠'] : ['between', '>', '<', '=', '≠'];
const initValueList = getInitVal();
const [operatorVal, setOperatorVal] = useState(initValueList[0]);
const [firstInputVal, setFirstInputVal] = useState(initValueList[1] as string);
const [secondInputVal, setSecondInputVal] = useState(initValueList[2] as string);
// status or choice parameter dropdown selected value list
const [choiceList, setChoiceList] = useState(initValueList[3] as string[]);
function getInitVal(): Array<string | string[]> {
// push value: operator, firstInputVal(value1), secondInputVal(value2), choiceValue
const str: Array<string | string[]> = [];
if (searchFilter.length > 0) {
const filterElement = searchFilter.find(ele => ele.name === parameter);
if (filterElement !== undefined) {
str.push(
filterElement.operator,
filterElement.value1.toString(),
filterElement.value2.toString(),
filterElement.choice.toString().split(',')
);
} else {
// set init value
str.push(`${isChoiceTypeSearchFilter ? '=' : 'between'}`, '', '', [] as string[]);
}
} else {
str.push(`${isChoiceTypeSearchFilter ? '=' : 'between'}`, '', '', [] as string[]);
}
return str;
}
function updateOperatorDropdown(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption | undefined): void {
if (item !== undefined) {
setOperatorVal(item.key.toString());
}
}
// get [status | parameters that type is choice] list
function updateChoiceDropdown(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption | undefined): void {
if (item !== undefined) {
const result = item.selected
? [...choiceList, item.key as string]
: choiceList.filter(key => key !== item.key);
setChoiceList(result);
}
}
function updateFirstInputVal(ev: React.ChangeEvent<HTMLInputElement>): void {
setFirstInputVal(ev.target.value);
}
function updateSecondInputVal(ev: React.ChangeEvent<HTMLInputElement>): void {
setSecondInputVal(ev.target.value);
}
function getSecondInputVal(): string {
if (secondInputVal === '' && operatorVal === 'between') {
// if user uses 'between' operator and doesn't write the second input value,
// help to set second value as this parameter max value
return EXPERIMENT.searchSpace[parameter]._value[1].toString();
}
return secondInputVal as string;
}
// click Apply button
function startFilterTrials(): void {
if (isChoiceTypeSearchFilter === false) {
if (firstInputVal === '') {
alert('Please input related value!');
return;
}
}
if (firstInputVal.match(/[a-zA-Z]/) || secondInputVal.match(/[a-zA-Z]/)) {
alert('Please input a number!');
return;
}
let newSearchFilters = JSON.parse(JSON.stringify(searchFilter));
const find = newSearchFilters.filter(ele => ele.name === parameter);
if (find.length > 0) {
// if user clear all selected options, will clear this filter condition on the searchFilter list
// eg: conv_size -> choiceList = [], searchFilter will remove (name === 'conv_size')
if ((isChoiceTypeSearchFilter && choiceList.length !== 0) || isChoiceTypeSearchFilter === false) {
newSearchFilters.forEach(item => {
if (item.name === parameter) {
item.operator = operatorVal;
item.value1 = firstInputVal;
item.value2 = getSecondInputVal();
item.choice = choiceList;
item.isChoice = isChoiceTypeSearchFilter ? true : false;
}
});
} else {
newSearchFilters = newSearchFilters.filter(item => item.name !== parameter);
}
} else {
if ((isChoiceTypeSearchFilter && choiceList.length !== 0) || isChoiceTypeSearchFilter === false) {
newSearchFilters.push({
name: parameter,
operator: operatorVal,
value1: firstInputVal,
value2: getSecondInputVal(),
choice: choiceList,
isChoice: isChoiceTypeSearchFilter ? true : false
});
}
}
setSearchInputVal(getSearchInputValueBySearchList(newSearchFilters));
changeSearchFilterList(newSearchFilters);
updatePage();
dismiss(); // close menu
}
return (
// for trial parameters & Status
<Stack horizontal className='filterConditions' tokens={searchConditonsGap}>
<Dropdown
selectedKey={operatorVal}
options={operatorList.map(item => ({
key: item,
text: item
}))}
onChange={updateOperatorDropdown}
className='btn-vertical-middle'
styles={{ root: { width: 100 } }}
/>
{isChoiceTypeSearchFilter ? (
<Dropdown
// selectedKeys:[] multiy, selectedKey: string
selectedKeys={choiceList}
multiSelect
options={getDropdownOptions(parameter)}
onChange={updateChoiceDropdown}
className='btn-vertical-middle'
styles={{ root: { width: 190 } }}
/>
) : (
<React.Fragment>
{operatorVal === 'between' ? (
<div>
<input
type='text'
className='input input-padding'
onChange={updateFirstInputVal}
value={firstInputVal}
/>
<span className='and'>and</span>
<input
type='text'
className='input input-padding'
onChange={updateSecondInputVal}
value={secondInputVal}
/>
</div>
) : (
<input
type='text'
className='input input-padding'
onChange={updateFirstInputVal}
value={firstInputVal}
/>
)}
</React.Fragment>
)}
<PrimaryButton text='Apply' className='btn-vertical-middle' onClick={startFilterTrials} />
</Stack>
);
}
SearchParameterConditions.propTypes = {
parameter: PropTypes.string,
searchFilter: PropTypes.array,
dismiss: PropTypes.func,
setSearchInputVal: PropTypes.func,
changeSearchFilterList: PropTypes.func,
updatePage: PropTypes.func
};
export default SearchParameterConditions;

Просмотреть файл

@ -0,0 +1,203 @@
import { mergeStyleSets } from '@fluentui/react';
import { trialJobStatus } from '../../../static/const';
import { EXPERIMENT } from '../../../static/datamodel';
import { TableObj, SearchItems } from '../../../static/interface';
const classNames = mergeStyleSets({
menu: {
textAlign: 'center',
maxWidth: 600,
selectors: {
'.ms-ContextualMenu-item': {
height: 'auto'
}
}
},
item: {
display: 'inline-block',
width: 40,
height: 40,
lineHeight: 40,
textAlign: 'center',
verticalAlign: 'middle',
marginBottom: 8,
cursor: 'pointer',
selectors: {
'&:hover': {
backgroundColor: '#eaeaea'
}
}
},
categoriesList: {
margin: 0,
padding: 0,
listStyleType: 'none'
},
button: {
width: '40%',
margin: '2%'
}
});
function getDropdownOptions(parameter): any {
if (parameter === 'StatusNNI') {
return trialJobStatus.map(item => ({
key: item,
text: item
}));
} else {
return EXPERIMENT.searchSpace[parameter]._value.map(item => ({
key: item.toString(),
text: item.toString()
}));
}
}
// change origin data according to parameter type, string -> number
const convertParametersValue = (searchItems: SearchItems[], relation: Map<string, string>): SearchItems[] => {
const choice: any[] = [];
searchItems.forEach(item => {
if (relation.get(item.name) === 'number') {
if (item.isChoice === true) {
item.choice.forEach(ele => {
choice.push(JSON.parse(ele));
});
item.choice = choice;
} else {
item.value1 = JSON.parse(item.value1);
if (item.value2 !== '') {
item.value2 = JSON.parse(item.value2);
}
}
}
});
return searchItems;
};
// relation: trial parameter -> type {conv_size -> number}
const getTrialsBySearchFilters = (
arr: TableObj[],
searchItems: SearchItems[],
relation: Map<string, string>
): TableObj[] => {
const que = convertParametersValue(searchItems, relation);
// start to filter data by ['Trial id', 'Trial No.', 'Status'] [...parameters]...
que.forEach(element => {
if (element.name === 'Trial id') {
arr = arr.filter(trial => trial.id.toUpperCase().includes(element.value1.toUpperCase()));
} else if (element.name === 'Trial No.') {
arr = arr.filter(trial => trial.sequenceId.toString() === element.value1);
} else if (element.name === 'StatusNNI') {
arr = searchChoiceFilter(arr, element, 'status');
} else {
const parameter = `space/${element.name}`;
if (element.isChoice === true) {
arr = searchChoiceFilter(arr, element, element.name);
} else {
if (element.operator === '=') {
arr = arr.filter(trial => trial[parameter] === element.value1);
} else if (element.operator === '>') {
arr = arr.filter(trial => trial[parameter] > element.value1);
} else if (element.operator === '<') {
arr = arr.filter(trial => trial[parameter] < element.value1);
} else if (element.operator === 'between') {
arr = arr.filter(trial => trial[parameter] > element.value1 && trial[parameter] < element.value2);
} else {
// operator is '≠'
arr = arr.filter(trial => trial[parameter] !== element.value1);
}
}
}
});
return arr;
};
// isChoice = true: status and trial parameters
function findTrials(arr: TableObj[], choice: string[], filed: string): TableObj[] {
const newResult: TableObj[] = [];
const parameter = filed === 'status' ? 'status' : `space/${filed}`;
arr.forEach(trial => {
choice.forEach(item => {
if (trial[parameter] === item) {
newResult.push(trial);
}
});
});
return newResult;
}
function searchChoiceFilter(arr: TableObj[], element: SearchItems, field: string): TableObj[] {
if (element.operator === '=') {
return findTrials(arr, element.choice, field);
} else {
let choice;
if (field === 'status') {
choice = trialJobStatus.filter(index => !new Set(element.choice).has(index));
} else {
choice = EXPERIMENT.searchSpace[field]._value.filter(index => !new Set(element.choice).has(index));
}
return findTrials(arr, choice, field);
}
}
// click Apply btn: set searchBox value now
function getSearchInputValueBySearchList(searchFilter): string {
let str = ''; // store search input value
searchFilter.forEach(item => {
const filterName = item.name === 'StatusNNI' ? 'Status' : item.name;
if (item.isChoice === false) {
// id, No, !choice parameter
if (item.name === 'Trial id' || item.name === 'Trial No.') {
str = str + `${item.name}:${item.value1}; `;
} else {
// !choice parameter
if (['=', '≠', '>', '<'].includes(item.operator)) {
str = str + `${filterName}${item.operator === '=' ? ':' : item.operator}${item.value1}; `;
} else {
// between
str = str + `${filterName}:[${item.value1},${item.value2}]; `;
}
}
} else {
// status, choice parameter
str = str + `${filterName}${item.operator === '=' ? ':' : '≠'}[${[...item.choice]}]; `;
}
});
return str;
}
/***
* from experiment search space
* "conv_size": {
"_type": "choice", // is choice type
"_value": [
2,
3,
5,
7
]
},
*/
function isChoiceType(parameterName): boolean {
// 判断是 [choice, status] 还是普通的类型
let flag = false; // normal type
if (parameterName === 'StatusNNI') {
flag = true;
}
if (parameterName in EXPERIMENT.searchSpace) {
flag = EXPERIMENT.searchSpace[parameterName]._type === 'choice' ? true : false;
}
return flag;
}
export { classNames, getDropdownOptions, getTrialsBySearchFilters, getSearchInputValueBySearchList, isChoiceType };

Просмотреть файл

@ -2,6 +2,7 @@ import * as JSON5 from 'json5';
import axios from 'axios';
import { IContextualMenuProps } from '@fluentui/react';
import { MANAGER_IP } from './const';
import { EXPERIMENT } from './datamodel';
import { MetricDataRecord, FinalType, TableObj, Tensorboard } from './interface';
function getPrefix(): string | undefined {
@ -356,6 +357,19 @@ function getTensorboardMenu(queryTensorboardList: Tensorboard[], stopFunc, seeDe
return tensorboardMenu;
}
// search space type map list: now get type from search space
const parametersType = (): Map<string, string> => {
const parametersTypeMap = new Map();
const trialParameterlist = Object.keys(EXPERIMENT.searchSpace);
trialParameterlist.forEach(item => {
parametersTypeMap.set(item, typeof EXPERIMENT.searchSpace[item]._value[0]);
});
return parametersTypeMap;
};
export {
getPrefix,
convertTime,
@ -381,5 +395,6 @@ export {
caclMonacoEditorHeight,
copyAndSort,
disableTensorboard,
getTensorboardMenu
getTensorboardMenu,
parametersType
};

Просмотреть файл

@ -203,6 +203,16 @@ interface Tensorboard {
port: string;
}
// for TableList search
interface SearchItems {
name: string;
operator: string;
value1: string; // first input value
value2: string; // second input value
choice: string[]; // use select multiy value list
isChoice: boolean; // for parameters: type = choice and status also as choice type
}
export {
TableObj,
TableRecord,
@ -226,5 +236,6 @@ export {
MultipleAxes,
SortInfo,
AllExperimentList,
Tensorboard
Tensorboard,
SearchItems
};

Просмотреть файл

@ -73,3 +73,7 @@ $themeBlue: #0071bc;
.bold {
font-weight: bold;
}
.input-padding {
padding-left: 10px;
}

Просмотреть файл

@ -3,23 +3,17 @@
width: 96%;
margin: 0 auto;
margin-top: 15px;
position: relative;
&-compare {
margin-top: 15px;
}
&-entry {
line-height: 32px;
}
/* compare button style */
&-button-gap {
margin-right: 10px;
}
&-search-input {
padding-left: 10px;
}
}
/* each row's Intermediate btn -> Modal */
@ -33,3 +27,32 @@
width: 120px;
}
}
$filterConditionsHeight: 54px;
.filterConditions {
height: $filterConditionsHeight;
line-height: $filterConditionsHeight;
padding: 0 15px;
.btn-vertical-middle {
margin-top: 11px;
}
.input {
width: 100px;
height: 24px;
margin-top: 13px;
border: 1px solid #333;
border-radius: 20px;
outline: none;
}
.and {
margin: 0 4px;
}
}
.ms-ContextualMenu-Callout {
display: block;
}