From 82a757b49f18cf24c6dc351dc7917314b3e243e2 Mon Sep 17 00:00:00 2001 From: Lijiao Date: Wed, 4 Nov 2020 06:41:57 +0000 Subject: [PATCH 01/15] first update --- ts/webui/.eslintrc | 4 +- ts/webui/scripts/start.js | 2 +- ts/webui/src/App.scss | 2 + ts/webui/src/App.tsx | 47 ++- ts/webui/src/components/NavCon.tsx | 91 ++++-- ts/webui/src/components/buttons/Icon.tsx | 8 +- .../components/managementExp/Experiment.tsx | 281 ++++++++++++++++++ .../components/managementExp/experiment.json | 62 ++++ .../managementExp/experimentConst.ts | 34 +++ ts/webui/src/index.tsx | 22 +- ts/webui/src/static/const.ts | 2 +- ts/webui/src/static/function.ts | 7 +- .../static/style/experiment/experiment.scss | 39 +++ ts/webui/src/static/style/nav/nav.scss | 150 ++++++---- 14 files changed, 615 insertions(+), 136 deletions(-) create mode 100644 ts/webui/src/components/managementExp/Experiment.tsx create mode 100644 ts/webui/src/components/managementExp/experiment.json create mode 100644 ts/webui/src/components/managementExp/experimentConst.ts create mode 100644 ts/webui/src/static/style/experiment/experiment.scss diff --git a/ts/webui/.eslintrc b/ts/webui/.eslintrc index e220b1f541..7e7703264c 100644 --- a/ts/webui/.eslintrc +++ b/ts/webui/.eslintrc @@ -21,7 +21,8 @@ "prettier" ], "rules": { - "prettier/prettier": 2, + "prettier/prettier": 0, + "eslint/eslint": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-namespace": 0, "@typescript-eslint/consistent-type-assertions": 0, @@ -31,6 +32,7 @@ "@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }], "arrow-parens": [2, "as-needed"], "no-inner-declarations": 0, + "no-console": 0, "no-empty": 2, "no-multiple-empty-lines": [2, { "max": 1 }], "react/display-name": 0 diff --git a/ts/webui/scripts/start.js b/ts/webui/scripts/start.js index e6236bd6a2..247f0fcccc 100644 --- a/ts/webui/scripts/start.js +++ b/ts/webui/scripts/start.js @@ -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) || 9999; const HOST = process.env.HOST || '0.0.0.0'; if (process.env.HOST) { diff --git a/ts/webui/src/App.scss b/ts/webui/src/App.scss index 3811759ea8..1561d1a32d 100644 --- a/ts/webui/src/App.scss +++ b/ts/webui/src/App.scss @@ -27,6 +27,8 @@ .content { width: 87%; + /* test */ + min-height: calc(100vh - 56); margin: 0 auto; min-width: 1200px; margin-top: 74px; diff --git a/ts/webui/src/App.tsx b/ts/webui/src/App.tsx index e2a2489cc7..8f22203960 100644 --- a/ts/webui/src/App.tsx +++ b/ts/webui/src/App.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Stack } from '@fluentui/react'; import { COLUMN } from './static/const'; import { EXPERIMENT, TRIALS } from './static/datamodel'; +import { isManagerExperimentPage } from './static/function'; import NavCon from './components/NavCon'; import MessageInfo from './components/modals/MessageInfo'; import { TrialConfigButton } from './components/public-child/config/TrialConfigButton'; @@ -28,16 +29,16 @@ export const AppContext = React.createContext({ metricGraphMode: 'max', bestTrialEntries: '10', maxDurationUnit: 'm', - // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars - changeColumn: (val: string[]) => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars - changeMetricGraphMode: (val: 'max' | 'min') => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars - changeMaxDurationUnit: (val: string) => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars - changeEntries: (val: string) => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars - updateOverviewPage: () => {} + // eslint-disable-next-line @typescript-eslint/no-empty-function + changeColumn: (_val: string[]) => { }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + changeMetricGraphMode: (_val: 'max' | 'min') => { }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + changeMaxDurationUnit: (_val: string) => { }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + changeEntries: (_val: string) => { }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + updateOverviewPage: () => { } }); class App extends React.Component<{}, AppState> { @@ -138,6 +139,7 @@ class App extends React.Component<{}, AppState> { { errorWhere: TRIALS.latestMetricDataError(), errorMessage: TRIALS.getLatestMetricDataErrorMessage() }, { errorWhere: TRIALS.metricDataRangeError(), errorMessage: TRIALS.metricDataRangeErrorMessage() } ]; + return (
@@ -148,24 +150,13 @@ class App extends React.Component<{}, AppState> { {/* search space & config */} - - - + { + isManagerExperimentPage() + ? + null + : + + } {/* if api has error field, show error message */} {errorList.map( (item, key) => diff --git a/ts/webui/src/components/NavCon.tsx b/ts/webui/src/components/NavCon.tsx index b1bc216fae..8ec1ab4a28 100644 --- a/ts/webui/src/components/NavCon.tsx +++ b/ts/webui/src/components/NavCon.tsx @@ -10,11 +10,13 @@ import { IStackTokens, IStackStyles } from '@fluentui/react'; +import { Link } from 'react-router-dom'; import LogPanel from './modals/LogPanel'; import ExperimentPanel from './modals/ExperimentPanel'; -import { downLoadIcon, infoIconAbout, timeIcon, disableUpdates, requency, closeTimer } from './buttons/Icon'; +import { downLoadIcon, infoIconAbout, timeIcon, disableUpdates, requency, closeTimer, RevToggleKey, ChevronRightMed } from './buttons/Icon'; import { OVERVIEWTABS, DETAILTABS, NNILOGO } from './stateless-component/NNItabs'; import { EXPERIMENT } from '../static/datamodel'; +import { isManagerExperimentPage } from '../static/function'; import '../static/style/nav/nav.scss'; import '../static/style/icon.scss'; @@ -146,38 +148,63 @@ class NavCon extends React.Component { }; return ( - - {NNILOGO} - {OVERVIEWTABS} - {DETAILTABS} - - - - {/* refresh button danyi*/} - {/* TODO: fix bug */} - {/* */} -
- -
{refreshFrequency}
-
- - -
-
- {/* the drawer for dispatcher & nnimanager log message */} - {isvisibleLogDrawer && } - {isvisibleExperimentDrawer && ( - - )} + { + isManagerExperimentPage() + ? + + + {NNILOGO} + Neural Network Intelligence + + + + + + + + + + : + + + {NNILOGO} + {OVERVIEWTABS} + {DETAILTABS} + + + + {/* refresh button danyi*/} + {/* TODO: fix bug */} + {/* */} +
+ +
{refreshFrequency}
+
+ + + +
All experiment{ChevronRightMed}
+ {/* */} + +
+
+ {/* the drawer for dispatcher & nnimanager log message */} + {isvisibleLogDrawer && } + {isvisibleExperimentDrawer && ( + + )} +
+ }
+ ); } diff --git a/ts/webui/src/components/buttons/Icon.tsx b/ts/webui/src/components/buttons/Icon.tsx index b552de115d..3c221180da 100644 --- a/ts/webui/src/components/buttons/Icon.tsx +++ b/ts/webui/src/components/buttons/Icon.tsx @@ -19,6 +19,9 @@ const LineChart = ; const Edit = ; const CheckMark = ; const Cancel = ; +const ReplyAll = { iconName: 'ReplyAll' }; +const RevToggleKey = { iconName: 'RevToggleKey' }; +const ChevronRightMed = ; export { infoIcon, @@ -37,5 +40,8 @@ export { LineChart, Edit, CheckMark, - Cancel + Cancel, + ReplyAll, + RevToggleKey, + ChevronRightMed }; diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/Experiment.tsx new file mode 100644 index 0000000000..547c10eaa1 --- /dev/null +++ b/ts/webui/src/components/managementExp/Experiment.tsx @@ -0,0 +1,281 @@ +import * as React from 'react'; +import { Stack, DetailsList, DefaultButton, Icon, SearchBox, Dropdown, DatePicker, DayOfWeek } from '@fluentui/react'; +import { formatTimestamp } from '../../static/function'; +import DayPickerStrings from './experimentConst'; +import '../../App.scss'; +import '../../static/style/experiment/experiment.scss'; +import '../../static/style/tableStatus.css'; +const data = require('./experiment.json'); + +interface AllExperimentList { + name: string; + id: string; + status: string; + port: number; + platform: string; + startTime: number; + endTime: number; + tag: string; +} + +interface OverviewState { + hideFilter: boolean; + searchInputVal: string; + selectedStatus: string; + selectedPlatform: string; + // selectedStartDate: Date; + source: Array; +} + +class Experiment extends React.Component<{}, OverviewState> { + constructor(props) { + super(props); + this.state = { + hideFilter: false, + searchInputVal: '', + selectedStatus: '', + selectedPlatform: '', + // selectedStartDate: '', + source: data + }; + } + + // componentDidUpdate(): void { + + // } + + render(): React.ReactNode { + const { hideFilter, selectedStatus, source, selectedPlatform } = this.state; + return ( + + + + +
+ { + + this.setState(() => ({source: data})); + console.log('Custom onEscape Called'); + }} + onClear={(_ev): void => { + this.setState(() => ({source: data})); + console.log('Custom onClear Called'); + }} + onChange={(_, newValue): void => { + if(newValue !== undefined){ + this.setState(() => ({source: this.state.source.filter(item => (item.status.includes(newValue) || + item.id.includes(newValue)))})); + } + console.log('SearchBox onChange fired: ' + newValue)} + } + // onSearch={(newValue): void => console.log('SearchBox onSearch fired: ' + newValue)} + /> +
+
+ + + Filter + +
+
+ + + + date!.toString()} + onSelectDate={this.getSelectedData.bind(this)} + /> + + + + Reset + + + +
+
+
+ ); + } + + private statusOption = [ + { key: 'FAILED', text: 'FAILED' }, + { key: 'SUCCEEDED', text: 'SUCCEEDED' }, + { key: 'RUNNING', text: 'RUNNING' } + + ]; + private platformOption = [ + { key: 'local', text: 'local' }, + { key: 'pai', text: 'pai' }, + { key: 'remote', text: 'remote' } + + ]; + // TODO: 引入tag之后调整大小屏幕下的列宽 + private columns = [ + { + name: 'Name', + key: 'name', + fieldName: 'name', // required! + minWidth: 50, + maxWidth: 87, + isResizable: true, + data: 'number', + // onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode =>
{item.name}
+ }, + { + name: 'ID', + key: 'id', + fieldName: 'id', + minWidth: 50, + maxWidth: 87, + isResizable: true, + className: 'tableHead leftTitle', + data: 'string', + // onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode =>
{item.id}
+ }, + { + name: 'Status', + key: 'status', + minWidth: 80, + maxWidth: 150, + isResizable: true, + fieldName: 'status', + onRender: (item: any): React.ReactNode => ( +
{item.status}
+ ) + }, + { + name: 'Port', + key: 'port', + fieldName: 'port', + minWidth: 65, + maxWidth: 150, + isResizable: true, + data: 'number', + // onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode => ( +
+
{item.port}
+
+ ) + }, + + { + name: 'Platform', + key: 'platform', + fieldName: 'platform', + minWidth: 100, + maxWidth: 160, + isResizable: true, + data: 'string', + // onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode => ( +
{item.platform}
+ ) + }, + { + name: 'Start time', + key: 'startTime', + fieldName: 'startTime', + minWidth: 100, + maxWidth: 160, + isResizable: true, + data: 'number', + // onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode =>
+
{formatTimestamp(item.startTime)}
+
+ }, + { + name: 'End time', + key: 'endTime', + fieldName: 'endTime', + minWidth: 100, + maxWidth: 160, + isResizable: true, + data: 'number', + // onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode =>
+
{formatTimestamp(item.endTime)}
+
+ }, + ]; + + private clickFilter(_e: any): void { + const { hideFilter } = this.state; + this.setState(() => ({ hideFilter: !hideFilter })); + } + + selectStatus = (event: React.FormEvent, item: any): void => { + if (item !== undefined) { + // 只能set item.key + this.setState({selectedStatus: item.key, source: this.state.source.filter(temp => temp.status === item.key)}); + } + }; + + selectPlatform = (event: React.FormEvent, item: any): void => { + if (item !== undefined) { + // 只能set item.key + this.setState({selectedPlatform: item.key, source: this.state.source.filter(temp => temp.platform === item.key)}); + } + }; + + private getSelectedData(date: Date | null | undefined): void { + console.info('daa', date); + // const {source} = this.state; + // if + + } + private setOriginSource(): void { + this.setState(() => ({source: data, selectedStatus: '', selectedPlatform: ''})); + } +} + +export default Experiment; diff --git a/ts/webui/src/components/managementExp/experiment.json b/ts/webui/src/components/managementExp/experiment.json new file mode 100644 index 0000000000..8daaeb9843 --- /dev/null +++ b/ts/webui/src/components/managementExp/experiment.json @@ -0,0 +1,62 @@ +[ + { + "name": "example_minst", + "id": "qwe34", + "status": "RUNNING", + "port": 8080, + "platform": "local", + "startTime": 1603343843980, + "endTime": 1603343990436, + "tag": "it is an experiment for local" + }, + { + "name": "example_pai", + "id": "asd34", + "status": "SUCCEEDED", + "port": 9999, + "platform": "pai", + "startTime": 1603343773957, + "endTime": 1603343833157, + "tag": "it is an experiment for pai" + }, + { + "name": "example_minst33", + "id": "UR5G4", + "status": "FAILED", + "port": 8082, + "platform": "local", + "startTime": 1603344004004, + "endTime": 1603344167408, + "tag": "it is an experiment for local" + }, + { + "name": "example_minst", + "id": "QjqqiVBp", + "status": "SUCCEEDED", + "port": 8084, + "platform": "local", + "startTime": 1603344179024, + "endTime": 1603344238656, + "tag": "it is an experiment for local" + }, + { + "name": "example_minst", + "id": "mFT1RE6A", + "status": "FAILED", + "port": 8086, + "platform": "local", + "startTime": 1603344249043, + "endTime": 1603344348570, + "tag": "it is an experiment for local" + }, + { + "name": "example_minst", + "id": "I4WoFxzq", + "status": "USER_CANCELED", + "port": 8088, + "platform": "local", + "startTime": 1603344359065, + "endTime": 1603344388521, + "tag": "it is an experiment for local" + } +] \ No newline at end of file diff --git a/ts/webui/src/components/managementExp/experimentConst.ts b/ts/webui/src/components/managementExp/experimentConst.ts new file mode 100644 index 0000000000..d9a272f158 --- /dev/null +++ b/ts/webui/src/components/managementExp/experimentConst.ts @@ -0,0 +1,34 @@ +import {IDatePickerStrings} from '@fluentui/react'; +const DayPickerStrings: IDatePickerStrings = { + months: [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ], + + shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + + shortDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], + + goToToday: 'Go to today', + prevMonthAriaLabel: 'Go to previous month', + nextMonthAriaLabel: 'Go to next month', + prevYearAriaLabel: 'Go to previous year', + nextYearAriaLabel: 'Go to next year', + closeButtonAriaLabel: 'Close date picker', + monthPickerHeaderAriaLabel: '{0}, select to change the year', + yearPickerHeaderAriaLabel: '{0}, select to change the month', + }; + +export default DayPickerStrings; \ No newline at end of file diff --git a/ts/webui/src/index.tsx b/ts/webui/src/index.tsx index af67396b86..2b75824cba 100644 --- a/ts/webui/src/index.tsx +++ b/ts/webui/src/index.tsx @@ -4,27 +4,29 @@ import App from './App'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; const Overview = lazy(() => import('./components/Overview')); const TrialsDetail = lazy(() => import('./components/TrialsDetail')); +const Experiment = lazy(() => import('./components/managementExp/Experiment')); import './index.css'; import './static/style/loading.scss'; import * as serviceWorker from './serviceWorker'; ReactDOM.render( - + + +
+ } + > + - - - - } - > + - + - + , document.getElementById('root') diff --git a/ts/webui/src/static/const.ts b/ts/webui/src/static/const.ts index 7f4d6a79c5..54c0ad7c71 100644 --- a/ts/webui/src/static/const.ts +++ b/ts/webui/src/static/const.ts @@ -2,7 +2,7 @@ const METRIC_GROUP_UPDATE_THRESHOLD = 100; const METRIC_GROUP_UPDATE_SIZE = 20; -const MANAGER_IP = `/api/v1/nni`; +const MANAGER_IP = `http://13.77.78.63:8080/api/v1/nni`; const DOWNLOAD_IP = `/logs`; const WEBUIDOC = 'https://nni.readthedocs.io/en/latest/Tutorial/WebUI.html'; const trialJobStatus = [ diff --git a/ts/webui/src/static/function.ts b/ts/webui/src/static/function.ts index 63dfe557ed..f9ccd30b61 100644 --- a/ts/webui/src/static/function.ts +++ b/ts/webui/src/static/function.ts @@ -262,6 +262,10 @@ function formatComplexTypeValue(value: any): string | number { } } +function isManagerExperimentPage(): boolean { + return (location.href.indexOf('experiment') === -1) ? false : true; +} + export { convertTime, convertDuration, @@ -280,5 +284,6 @@ export { isArrayType, requestAxios, isNaNorInfinity, - formatComplexTypeValue + formatComplexTypeValue, + isManagerExperimentPage }; diff --git a/ts/webui/src/static/style/experiment/experiment.scss b/ts/webui/src/static/style/experiment/experiment.scss new file mode 100644 index 0000000000..a9859224d0 --- /dev/null +++ b/ts/webui/src/static/style/experiment/experiment.scss @@ -0,0 +1,39 @@ +.experimentList{ + border: 1px solid red; + padding: 42px; + + .box{ + .search{ + width: 90%; + &-input{ + width: 330px; + } + } + + .filter{ + width: 10%; + text-align: right; + + &-button{ + width: 80px; + } + } + } + + .filter-condition{ + margin-top: 26px; + .reset{ + width: 80px; + position: relative; + top: 29px; + } + } + + .hidden{ + display: none; + } + + .margin{ + margin-left: 10px; + } +} \ No newline at end of file diff --git a/ts/webui/src/static/style/nav/nav.scss b/ts/webui/src/static/style/nav/nav.scss index 58d0769267..c075868cc0 100644 --- a/ts/webui/src/static/style/nav/nav.scss +++ b/ts/webui/src/static/style/nav/nav.scss @@ -1,83 +1,111 @@ $barHeight: 56px; .navOptions { - .ms-Button-icon { - color: #fff; - - &:hover { - color: #fff; - } - } - - .ms-Button--commandBar { - background-color: #0071bc; - user-select: none; - - &:hover, - &:active { - color: #fff; - - .ms-Button-icon { - color: #fff; - } - } - - .ms-Button-textContainer { - color: #fff; - } - - .ms-Button-menuIcon { - color: #fff; - background-color: transparent; - } - } + .ms-Button-icon { + color: #fff; + + &:hover { + color: #fff; + } + } + + .ms-Button--commandBar { + background-color: #0071bc; + user-select: none; + + &:hover, + &:active { + color: #fff; + + .ms-Button-icon { + color: #fff; + } + } + + .ms-Button-textContainer { + color: #fff; + } + + .ms-Button-menuIcon { + color: #fff; + background-color: transparent; + } + } + + .experiment { + position: relative; + top: 3px; + text-decoration: none; + } } .nav { - height: $barHeight; - line-height: $barHeight; - - /* desktop mode useful */ - .desktop-logo { - position: relative; - top: 6px; - } - - &-refresh { - position: relative; - display: flex; - } - - &-refresh-num { - position: absolute; - top: -13px; - left: 17px; - color: #fff; - font-size: 12px; - } + width: 100%; + height: $barHeight; + line-height: $barHeight; + + /* desktop mode useful */ + .desktop-logo { + position: relative; + top: 6px; + } + + .logoTitle { + font-size: 18px; + color: #fff; + } + + &-refresh { + position: relative; + display: flex; + } + + &-refresh-num { + position: absolute; + top: -13px; + left: 17px; + color: #fff; + font-size: 12px; + } } /* overview and detail tabs common style */ a.common-tabs { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - font-size: 16px; - color: #b8c7ce; - text-decoration: none; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 16px; + color: #b8c7ce; + text-decoration: none; } .common-tabs:visited, .selected:hover { - color: #fff; - text-decoration: none; + color: #fff; + text-decoration: none; } .common-tabs:hover, .selected { - color: #fff; - border-bottom: 1px solid #fff; + color: #fff; + border-bottom: 1px solid #fff; } .left-right-margin { - margin-left: 20px; - margin-right: 20px; + margin-left: 20px; + margin-right: 20px; +} + +.expNavTitle { + span { + color: #fff; + text-decoration: none; + position: relative; + top: -4px; + } + i { + color: #fff; + font-size: 12px; + position: relative; + top: -3px; + padding-left: 4px; + } } From aedf2c1ce972080a82d7ead3d1d80704ebb20637 Mon Sep 17 00:00:00 2001 From: Lijiao Date: Tue, 10 Nov 2020 09:51:07 +0000 Subject: [PATCH 02/15] add click event --- .../components/managementExp/Experiment.tsx | 98 ++++++++++++++----- .../components/managementExp/experiment.json | 16 +-- .../static/style/experiment/experiment.scss | 7 ++ 3 files changed, 87 insertions(+), 34 deletions(-) diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/Experiment.tsx index 547c10eaa1..cc66379e66 100644 --- a/ts/webui/src/components/managementExp/Experiment.tsx +++ b/ts/webui/src/components/managementExp/Experiment.tsx @@ -40,10 +40,6 @@ class Experiment extends React.Component<{}, OverviewState> { }; } - // componentDidUpdate(): void { - - // } - render(): React.ReactNode { const { hideFilter, selectedStatus, source, selectedPlatform } = this.state; return ( @@ -53,26 +49,36 @@ class Experiment extends React.Component<{}, OverviewState> {
{ - this.setState(() => ({source: data})); + this.setState(() => ({ source: data })); console.log('Custom onEscape Called'); }} onClear={(_ev): void => { - this.setState(() => ({source: data})); - console.log('Custom onClear Called'); + this.setState(() => ({ source: data })); + console.log('onClear'); + // 点 × 操作 + console.info('source', this.state.source); }} onChange={(_, newValue): void => { - if(newValue !== undefined){ - this.setState(() => ({source: this.state.source.filter(item => (item.status.includes(newValue) || - item.id.includes(newValue)))})); + if (newValue !== undefined) { + // 空格回退操作 + if (newValue === '') { + this.setState(() => ({ source: data })); + console.info('source', this.state.source); + return; + } + this.setState(() => ({ + source: this.state.source.filter(item => (item.status.includes(newValue) || + item.id.includes(newValue))) + })); } - console.log('SearchBox onChange fired: ' + newValue)} + console.log('SearchBox onChange fired: ' + newValue) + } } - // onSearch={(newValue): void => console.log('SearchBox onSearch fired: ' + newValue)} + // onSearch={(newValue): void => console.log('SearchBox onSearch fired: ' + newValue)} />
@@ -92,7 +98,7 @@ class Experiment extends React.Component<{}, OverviewState> { onChange={this.selectStatus.bind(this)} placeholder="Select an option" options={this.statusOption} - /> + /> { ariaLabel="Select a date" // dateTimeFormatter={formatMonthDayYear()} // formatDate={(date?: Date): string => date!.toString()} - onSelectDate={this.getSelectedData.bind(this)} + onSelectDate={this.getSelectedData.bind(this, 'start')} /> { strings={DayPickerStrings} placeholder="Select a date..." ariaLabel="Select a date" + onSelectDate={this.getSelectedData.bind(this, 'end')} /> { key: 'port', fieldName: 'port', minWidth: 65, - maxWidth: 150, + maxWidth: 90, isResizable: true, data: 'number', // onColumnClick: this.onColumnClick, @@ -206,13 +213,12 @@ class Experiment extends React.Component<{}, OverviewState> {
) }, - { name: 'Platform', key: 'platform', fieldName: 'platform', - minWidth: 100, - maxWidth: 160, + minWidth: 80, + maxWidth: 100, isResizable: true, data: 'string', // onColumnClick: this.onColumnClick, @@ -246,6 +252,29 @@ class Experiment extends React.Component<{}, OverviewState> {
{formatTimestamp(item.endTime)}
}, + { + name: 'Tag', + key: 'tag', + fieldName: 'tag', + minWidth: 100, + maxWidth: 160, + isResizable: true, + data: 'number', + // onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode => { + return ( +
+ { + item.tag.map(tag => { + return ( + {tag} + ); + }) + } +
+ ); + } + } ]; private clickFilter(_e: any): void { @@ -256,25 +285,42 @@ class Experiment extends React.Component<{}, OverviewState> { selectStatus = (event: React.FormEvent, item: any): void => { if (item !== undefined) { // 只能set item.key - this.setState({selectedStatus: item.key, source: this.state.source.filter(temp => temp.status === item.key)}); + this.setState({ selectedStatus: item.key, source: this.state.source.filter(temp => temp.status === item.key) }); } }; selectPlatform = (event: React.FormEvent, item: any): void => { if (item !== undefined) { // 只能set item.key - this.setState({selectedPlatform: item.key, source: this.state.source.filter(temp => temp.platform === item.key)}); + this.setState({ selectedPlatform: item.key, source: this.state.source.filter(temp => temp.platform === item.key) }); } }; - private getSelectedData(date: Date | null | undefined): void { - console.info('daa', date); - // const {source} = this.state; - // if + private compareDate(date1: Date, date2: Date): boolean { + if (date1.getFullYear() === date2.getFullYear()) { + if (date1.getMonth() === date2.getMonth()) { + if (date1.getDate() === date2.getDate()) { + return true; + } + } + } + + return false; + } + + private getSelectedData(type: string, date: Date | null | undefined): void { + if (date !== null && date !== undefined) { + const { source } = this.state; + if (type === 'start') { + this.setState({ source: source.filter(item => this.compareDate(new Date(item.startTime), date)) }); + } else { + this.setState({ source: source.filter(item => this.compareDate(new Date(item.endTime), date)) }); + } + } } private setOriginSource(): void { - this.setState(() => ({source: data, selectedStatus: '', selectedPlatform: ''})); + this.setState(() => ({ source: data, selectedStatus: '', selectedPlatform: '' })); } } diff --git a/ts/webui/src/components/managementExp/experiment.json b/ts/webui/src/components/managementExp/experiment.json index 8daaeb9843..2c02abc5a3 100644 --- a/ts/webui/src/components/managementExp/experiment.json +++ b/ts/webui/src/components/managementExp/experiment.json @@ -5,9 +5,9 @@ "status": "RUNNING", "port": 8080, "platform": "local", - "startTime": 1603343843980, - "endTime": 1603343990436, - "tag": "it is an experiment for local" + "startTime": 1604574871346, + "endTime": 1604651935683, + "tag": ["local", "first", "tf-1"] }, { "name": "example_pai", @@ -17,7 +17,7 @@ "platform": "pai", "startTime": 1603343773957, "endTime": 1603343833157, - "tag": "it is an experiment for pai" + "tag": ["pai", "first", "tf-2"] }, { "name": "example_minst33", @@ -27,7 +27,7 @@ "platform": "local", "startTime": 1603344004004, "endTime": 1603344167408, - "tag": "it is an experiment for local" + "tag": ["local", "failed", "tf-1"] }, { "name": "example_minst", @@ -37,7 +37,7 @@ "platform": "local", "startTime": 1603344179024, "endTime": 1603344238656, - "tag": "it is an experiment for local" + "tag": ["local", "first", "tf-1"] }, { "name": "example_minst", @@ -47,7 +47,7 @@ "platform": "local", "startTime": 1603344249043, "endTime": 1603344348570, - "tag": "it is an experiment for local" + "tag": ["local", "first", "tf-1"] }, { "name": "example_minst", @@ -57,6 +57,6 @@ "platform": "local", "startTime": 1603344359065, "endTime": 1603344388521, - "tag": "it is an experiment for local" + "tag": ["local", "first", "tf-1"] } ] \ No newline at end of file diff --git a/ts/webui/src/static/style/experiment/experiment.scss b/ts/webui/src/static/style/experiment/experiment.scss index a9859224d0..daaf10526c 100644 --- a/ts/webui/src/static/style/experiment/experiment.scss +++ b/ts/webui/src/static/style/experiment/experiment.scss @@ -36,4 +36,11 @@ .margin{ margin-left: 10px; } + + .tag{ + font-weight: 500; + background: #f2f2f2; + margin: 0 4px; + padding: 0 6px; + } } \ No newline at end of file From 7da5e15c0406ddbfe184059b8ccdbe58ed2b0baf Mon Sep 17 00:00:00 2001 From: Lijiao Date: Tue, 17 Nov 2020 03:49:23 +0000 Subject: [PATCH 03/15] add manager exp nav --- ts/webui/src/components/NavCon.tsx | 66 ++++++++++++++++-------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/ts/webui/src/components/NavCon.tsx b/ts/webui/src/components/NavCon.tsx index 3341b528da..d6344d4230 100644 --- a/ts/webui/src/components/NavCon.tsx +++ b/ts/webui/src/components/NavCon.tsx @@ -11,7 +11,7 @@ import { IStackStyles } from '@fluentui/react'; import { Link } from 'react-router-dom'; -import { infoIconAbout, timeIcon, disableUpdates, requency, closeTimer, RevToggleKey } from './buttons/Icon'; +import { infoIconAbout, timeIcon, disableUpdates, requency, closeTimer, RevToggleKey, ChevronRightMed } from './buttons/Icon'; import ExperimentSummaryPanel from './modals/ExperimentSummaryPanel'; import { OVERVIEWTABS, DETAILTABS, NNILOGO } from './stateless-component/NNItabs'; import { EXPERIMENT } from '../static/datamodel'; @@ -159,36 +159,40 @@ class NavCon extends React.Component { {DETAILTABS} - - {/* refresh button danyi*/} - {/* TODO: fix bug */} - {/* */} -
- -
{refreshFrequency}
-
- - -
-
- {isvisibleExperimentDrawer && ( - - )} + + {/* refresh button danyi*/} + {/* TODO: fix bug */} + {/* */} +
+ +
{refreshFrequency}
+
+ + + +
All experiment{ChevronRightMed}
+ {/* */} + +
+ + {isvisibleExperimentDrawer && ( + + )} }
From f21b4e89dfb2847725689b49b3fc5beac3129772 Mon Sep 17 00:00:00 2001 From: Lijiao Date: Wed, 18 Nov 2020 09:54:23 +0000 Subject: [PATCH 04/15] first demo, fix some comments --- ts/webui/src/components/Overview.tsx | 4 +- .../components/managementExp/Experiment.tsx | 247 +++++++++++++----- .../components/managementExp/experiment.json | 30 ++- .../components/overview/command/Command1.tsx | 5 +- .../components/overview/command/Command2.tsx | 9 +- .../{experiment => params}/BasicInfo.tsx | 15 +- .../{experiment => params}/basicInfoStyles.ts | 0 ts/webui/src/index.tsx | 2 +- ts/webui/src/static/const.ts | 5 + .../src/static/style/common/ellipsis.scss | 13 + .../static/style/experiment/experiment.scss | 42 ++- ts/webui/src/static/style/nav/nav.scss | 1 + ts/webui/src/static/style/overview/basic.scss | 23 ++ .../src/static/style/overview/overview.scss | 7 +- .../style/{progress => overview}/probar.scss | 0 .../src/static/style/progress/progress.scss | 71 ----- ts/webui/src/static/style/table.scss | 6 - 17 files changed, 306 insertions(+), 174 deletions(-) rename ts/webui/src/components/overview/{experiment => params}/BasicInfo.tsx (89%) rename ts/webui/src/components/overview/{experiment => params}/basicInfoStyles.ts (100%) create mode 100644 ts/webui/src/static/style/common/ellipsis.scss create mode 100644 ts/webui/src/static/style/overview/basic.scss rename ts/webui/src/static/style/{progress => overview}/probar.scss (100%) delete mode 100644 ts/webui/src/static/style/progress/progress.scss diff --git a/ts/webui/src/components/Overview.tsx b/ts/webui/src/components/Overview.tsx index 3086c47d8c..64d9e76694 100644 --- a/ts/webui/src/components/Overview.tsx +++ b/ts/webui/src/components/Overview.tsx @@ -6,7 +6,7 @@ import { AppContext } from '../App'; import { Title } from './overview/Title'; import SuccessTable from './overview/table/SuccessTable'; import Accuracy from './overview/Accuracy'; -import { ReBasicInfo } from './overview/experiment/BasicInfo'; +import { BasicInfo } from './overview/params/BasicInfo'; import { ExpDuration } from './overview/count/ExpDuration'; import { ExpDurationContext } from './overview/count/ExpDurationContext'; import { TrialCount } from './overview/count/TrialCount'; @@ -86,7 +86,7 @@ class Overview extends React.Component<{}, OverviewState> { </TitleContext.Provider> <BestMetricContext.Provider value={{ bestAccuracy: bestAccuracy }}> - <ReBasicInfo /> + <BasicInfo /> </BestMetricContext.Provider> </div> {/* duration & trial numbers */} diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/Experiment.tsx index cc66379e66..ff48d33c0d 100644 --- a/ts/webui/src/components/managementExp/Experiment.tsx +++ b/ts/webui/src/components/managementExp/Experiment.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; -import { Stack, DetailsList, DefaultButton, Icon, SearchBox, Dropdown, DatePicker, DayOfWeek } from '@fluentui/react'; +import { Stack, DetailsList, DefaultButton, Icon, SearchBox, Dropdown, DatePicker, DayOfWeek, TooltipHost, DirectionalHint } from '@fluentui/react'; import { formatTimestamp } from '../../static/function'; +import { TOOLTIP_BACKGROUND_COLOR, EXPERIMENTSTATUS, PLATFORM } from '../../static/const'; import DayPickerStrings from './experimentConst'; import '../../App.scss'; import '../../static/style/experiment/experiment.scss'; +import '../../static/style/common/ellipsis.scss'; import '../../static/style/tableStatus.css'; const data = require('./experiment.json'); @@ -15,7 +17,7 @@ interface AllExperimentList { platform: string; startTime: number; endTime: number; - tag: string; + tag: string[]; } interface OverviewState { @@ -23,27 +25,31 @@ interface OverviewState { searchInputVal: string; selectedStatus: string; selectedPlatform: string; - // selectedStartDate: Date; + selectedStartDate?: Date; + selectedEndDate?: Date; source: Array<AllExperimentList>; + filterSource: Array<AllExperimentList>; + filterSourceOrigin: Array<AllExperimentList>; } class Experiment extends React.Component<{}, OverviewState> { constructor(props) { super(props); this.state = { - hideFilter: false, + hideFilter: true, searchInputVal: '', selectedStatus: '', selectedPlatform: '', - // selectedStartDate: '', - source: data + source: data, + filterSource: data, + filterSourceOrigin: data }; } render(): React.ReactNode { - const { hideFilter, selectedStatus, source, selectedPlatform } = this.state; + const { hideFilter, selectedStatus, source, selectedPlatform, selectedStartDate, selectedEndDate } = this.state; return ( - <Stack className='contentBox'> + <Stack className='contentBox expBackground'> <Stack className='content'> <Stack className='experimentList'> <Stack className='box' horizontal> @@ -62,29 +68,14 @@ class Experiment extends React.Component<{}, OverviewState> { // 点 × 操作 console.info('source', this.state.source); }} - onChange={(_, newValue): void => { - if (newValue !== undefined) { - // 空格回退操作 - if (newValue === '') { - this.setState(() => ({ source: data })); - console.info('source', this.state.source); - return; - } - this.setState(() => ({ - source: this.state.source.filter(item => (item.status.includes(newValue) || - item.id.includes(newValue))) - })); - } - console.log('SearchBox onChange fired: ' + newValue) - } - } - // onSearch={(newValue): void => console.log('SearchBox onSearch fired: ' + newValue)} + onChange={this.searchNameAndId.bind(this)} /> </div> <div className='filter'> <DefaultButton onClick={this.clickFilter.bind(this)} - className='fiter-button' + // className='fiter-button' + className={`${!hideFilter ? 'filter-button-open' : null}`} > <Icon iconName='Equalizer' /> <span className='margin'>Filter</span> @@ -97,35 +88,37 @@ class Experiment extends React.Component<{}, OverviewState> { selectedKey={selectedStatus} onChange={this.selectStatus.bind(this)} placeholder="Select an option" - options={this.statusOption} + options={this.fillOptions(EXPERIMENTSTATUS)} + className='filter-condition-status' /> <Dropdown label="Platform" selectedKey={selectedPlatform} onChange={this.selectPlatform.bind(this)} placeholder="Select an option" - options={this.platformOption} - // styles={dropdownStyles} + options={this.fillOptions(PLATFORM)} + className='filter-condition-platform' /> <DatePicker label='Start time' - // className={controlClass.control} firstDayOfWeek={DayOfWeek.Sunday} strings={DayPickerStrings} showMonthPickerAsOverlay={true} placeholder="Select a date..." ariaLabel="Select a date" + value={selectedStartDate} // dateTimeFormatter={formatMonthDayYear()} // formatDate={(date?: Date): string => date!.toString()} onSelectDate={this.getSelectedData.bind(this, 'start')} /> <DatePicker label='End time' - // className={controlClass.control} firstDayOfWeek={DayOfWeek.Sunday} strings={DayPickerStrings} + showMonthPickerAsOverlay={true} placeholder="Select a date..." ariaLabel="Select a date" + value={selectedEndDate} onSelectDate={this.getSelectedData.bind(this, 'end')} /> <DefaultButton @@ -142,7 +135,7 @@ class Experiment extends React.Component<{}, OverviewState> { setKey='set' compact={true} selectionMode={0} // close selector function - className='succTable' + className='table' /> </Stack> </Stack> @@ -150,18 +143,16 @@ class Experiment extends React.Component<{}, OverviewState> { ); } - private statusOption = [ - { key: 'FAILED', text: 'FAILED' }, - { key: 'SUCCEEDED', text: 'SUCCEEDED' }, - { key: 'RUNNING', text: 'RUNNING' } + private fillOptions(arr: string[]): any { + const list: Array<object> = []; - ]; - private platformOption = [ - { key: 'local', text: 'local' }, - { key: 'pai', text: 'pai' }, - { key: 'remote', text: 'remote' } + arr.map(item => { + list.push({ key: item, text: item }); + }); + + return list; + } - ]; // TODO: 引入tag之后调整大小屏幕下的列宽 private columns = [ { @@ -173,7 +164,19 @@ class Experiment extends React.Component<{}, OverviewState> { isResizable: true, data: 'number', // onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => <div className='succeed-padding'>{item.name}</div> + // onClick={() => {window.open(webuiPortal)}} + onRender: (item: any): React.ReactNode => { + const hostname = window.location.hostname; + const protocol = window.location.protocol; + const webuiPortal = `${protocol}//${hostname}:${item.port}/oview`; + return ( + <div className='succeed-padding ellipsis'> + <a href={webuiPortal} className='link' target='_blank' rel='noopener noreferrer'> + {item.name} + </a> + </div> + ); + } }, { name: 'ID', @@ -185,7 +188,7 @@ class Experiment extends React.Component<{}, OverviewState> { className: 'tableHead leftTitle', data: 'string', // onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => <div className='succeed-padding'>{item.id}</div> + onRender: (item: any): React.ReactNode => <div className='succeed-padding id'>{item.id}</div> }, { name: 'Status', @@ -263,15 +266,30 @@ class Experiment extends React.Component<{}, OverviewState> { // onColumnClick: this.onColumnClick, onRender: (item: any): React.ReactNode => { return ( - <div className='succeed-padding'> - { - item.tag.map(tag => { - return ( - <span className='tag' key={tag}>{tag}</span> - ); - }) - } - </div> + <TooltipHost + content={item.tag.join(', ')} + className='ellipsis' + directionalHint={DirectionalHint.bottomCenter} + tooltipProps={{ + calloutProps: { + styles: { + beak: { background: TOOLTIP_BACKGROUND_COLOR }, + beakCurtain: { background: TOOLTIP_BACKGROUND_COLOR }, + calloutMain: { background: TOOLTIP_BACKGROUND_COLOR } + } + } + }} + > + <div className='succeed-padding tagContainer'> + { + item.tag.map(tag => { + return ( + <span className='tag' key={tag}>{tag}</span> + ); + }) + } + </div> + </TooltipHost> ); } } @@ -279,20 +297,73 @@ class Experiment extends React.Component<{}, OverviewState> { private clickFilter(_e: any): void { const { hideFilter } = this.state; + this.setOriginSource(); this.setState(() => ({ hideFilter: !hideFilter })); + + } + + private searchNameAndId(_event, newValue): void { + if (newValue !== undefined) { + // 空格回退操作 + if (newValue === '') { + this.setState(() => ({ source: data, filterSource: data })); + console.info('source', this.state.source); + return; + } + const result = this.state.source.filter(item => (item.name.includes(newValue) || + item.id.includes(newValue) || item.tag.join(',').includes(newValue))); + this.setState(() => ({ + source: result, filterSource: result, filterSourceOrigin: result + })); + } } - selectStatus = (event: React.FormEvent<HTMLDivElement>, item: any): void => { + // status platform startTime endTime + selectStatus = (_event: React.FormEvent<HTMLDivElement>, item: any): void => { if (item !== undefined) { + const { selectedPlatform, selectedStartDate, selectedEndDate, filterSource } = this.state; // 只能set item.key - this.setState({ selectedStatus: item.key, source: this.state.source.filter(temp => temp.status === item.key) }); + const hasPlatform = selectedPlatform === '' ? false : true; + const hasStartDate = selectedStartDate === undefined ? false : true; + const hasEndDate = selectedEndDate === undefined ? false : true; + let result; + result = filterSource.filter(temp => (temp.status === item.key)); + if (hasPlatform) { + result = result.filter(temp => (temp.platform === selectedPlatform)); + } + if (hasStartDate) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result = result.filter(temp => this.compareDate(new Date(temp.startTime), selectedStartDate!)); + } + if (hasEndDate) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result = result.filter(temp => this.compareDate(new Date(temp.endTime), selectedEndDate!)); + } + this.setState({ selectedStatus: item.key, source: result }); } }; - selectPlatform = (event: React.FormEvent<HTMLDivElement>, item: any): void => { + selectPlatform = (_event: React.FormEvent<HTMLDivElement>, item: any): void => { if (item !== undefined) { + const { selectedStatus, selectedStartDate, selectedEndDate, filterSource } = this.state; // 只能set item.key - this.setState({ selectedPlatform: item.key, source: this.state.source.filter(temp => temp.platform === item.key) }); + const hasStatus = selectedStatus === '' ? false : true; + const hasStartDate = selectedStartDate === undefined ? false : true; + const hasEndDate = selectedEndDate === undefined ? false : true; + let result; + result = filterSource.filter(temp => (temp.platform === item.key)); + if (hasStatus) { + result = result.filter(temp => (temp.status === selectedStatus)); + } + if (hasStartDate) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result = result.filter(temp => this.compareDate(new Date(temp.startTime), selectedStartDate!)); + } + if (hasEndDate) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result = result.filter(temp => this.compareDate(new Date(temp.endTime), selectedEndDate!)); + } + this.setState({ selectedPlatform: item.key, source: result }); } }; @@ -310,17 +381,69 @@ class Experiment extends React.Component<{}, OverviewState> { private getSelectedData(type: string, date: Date | null | undefined): void { if (date !== null && date !== undefined) { - const { source } = this.state; + const { selectedStatus, selectedPlatform, selectedStartDate, selectedEndDate, filterSource } = this.state; + // 只能set item.key + const hasStatus = selectedStatus === '' ? false : true; + const hasPlatform = selectedStatus === '' ? false : true; + const hasStartDate = selectedStartDate === undefined ? false : true; + const hasEndDate = selectedEndDate === undefined ? false : true; + let result; + if (type === 'start') { - this.setState({ source: source.filter(item => this.compareDate(new Date(item.startTime), date)) }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result = filterSource.filter(item => this.compareDate(new Date(item.startTime), date)); + + if (hasStatus) { + result = result.filter(temp => (temp.status === selectedStatus)); + } + + if (hasPlatform) { + result = result.filter(temp => (temp.platform === selectedPlatform)); + } + + if (hasEndDate) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result = result.filter(temp => this.compareDate(new Date(temp.endTime), selectedEndDate!)); + } + + this.setState({ + source: result, + selectedStartDate: date + }); } else { - this.setState({ source: source.filter(item => this.compareDate(new Date(item.endTime), date)) }); + result = filterSource.filter(item => this.compareDate(new Date(item.endTime), date)); + + if (hasStatus) { + result = result.filter(temp => (temp.status === selectedStatus)); + } + + if (hasPlatform) { + result = result.filter(temp => (temp.platform === selectedPlatform)); + } + + if (hasStartDate) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result = result.filter(temp => this.compareDate(new Date(temp.startTime), selectedEndDate!)); + } + + this.setState({ + source: result, + selectedEndDate: date + }); } - } + } } + + // reset private setOriginSource(): void { - this.setState(() => ({ source: data, selectedStatus: '', selectedPlatform: '' })); + this.setState(() => ({ + source: this.state.filterSource, + selectedStatus: '', + selectedPlatform: '', + selectedStartDate: undefined, + selectedEndDate: undefined + })); } } diff --git a/ts/webui/src/components/managementExp/experiment.json b/ts/webui/src/components/managementExp/experiment.json index 2c02abc5a3..a79d0713c8 100644 --- a/ts/webui/src/components/managementExp/experiment.json +++ b/ts/webui/src/components/managementExp/experiment.json @@ -1,17 +1,17 @@ [ { - "name": "example_minst", - "id": "qwe34", + "name": "EXample_minst_local_something", + "id": "wk9TNiGF", "status": "RUNNING", "port": 8080, "platform": "local", "startTime": 1604574871346, "endTime": 1604651935683, - "tag": ["local", "first", "tf-1"] + "tag": ["It is a long tag", "local", "first", "tf-1"] }, { "name": "example_pai", - "id": "asd34", + "id": "kjCpGcBJ", "status": "SUCCEEDED", "port": 9999, "platform": "pai", @@ -19,6 +19,26 @@ "endTime": 1603343833157, "tag": ["pai", "first", "tf-2"] }, + { + "name": "paii", + "id": "LXRthUrH", + "status": "FAILED", + "port": 8080, + "platform": "pai", + "startTime": 1603343773957, + "endTime": 1603343833157, + "tag": ["pai", "good", "tf-2"] + }, + { + "name": "pai_example_2", + "id": "LXRthUrH", + "status": "FAILED", + "port": 3000, + "platform": "pai", + "startTime": 1603343773957, + "endTime": 1603343833157, + "tag": ["pai", "another", "tf-1"] + }, { "name": "example_minst33", "id": "UR5G4", @@ -26,7 +46,7 @@ "port": 8082, "platform": "local", "startTime": 1603344004004, - "endTime": 1603344167408, + "endTime": 1605664489998, "tag": ["local", "failed", "tf-1"] }, { diff --git a/ts/webui/src/components/overview/command/Command1.tsx b/ts/webui/src/components/overview/command/Command1.tsx index ae4fa4d595..d34190eaf9 100644 --- a/ts/webui/src/components/overview/command/Command1.tsx +++ b/ts/webui/src/components/overview/command/Command1.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { EXPERIMENT } from '../../../static/datamodel'; import '../../../static/style/overview/command.scss'; +import '../../../static/style/common/ellipsis.scss'; export const Command1 = (): any => { const tuner = EXPERIMENT.profile.params.tuner; @@ -36,9 +37,9 @@ export const Command1 = (): any => { <div className='basic'> <div> <p className='command'>Training platform</p> - <div className='nowrap'>{EXPERIMENT.profile.params.trainingServicePlatform}</div> + <div className='ellipsis'>{EXPERIMENT.profile.params.trainingServicePlatform}</div> <p className='lineMargin'>{title.join('/')}</p> - <div className='nowrap'>{builtinName.join('/')}</div> + <div className='ellipsis'>{builtinName.join('/')}</div> </div> </div> ); diff --git a/ts/webui/src/components/overview/command/Command2.tsx b/ts/webui/src/components/overview/command/Command2.tsx index d6cf4725e7..fb39aebc72 100644 --- a/ts/webui/src/components/overview/command/Command2.tsx +++ b/ts/webui/src/components/overview/command/Command2.tsx @@ -3,6 +3,7 @@ import { TooltipHost, DirectionalHint } from '@fluentui/react'; import { EXPERIMENT } from '../../../static/datamodel'; import { TOOLTIP_BACKGROUND_COLOR } from '../../../static/const'; import '../../../static/style/overview/command.scss'; +import '../../../static/style/common/ellipsis.scss'; export const Command2 = (): any => { const clusterMetaData = EXPERIMENT.profile.params.clusterMetaData; @@ -23,10 +24,10 @@ export const Command2 = (): any => { return ( <div className='basic'> <p className='command'>Log directory</p> - <div className='nowrap'> + <div className='ellipsis'> <TooltipHost content={EXPERIMENT.profile.logDir || 'unknown'} - className='nowrap' + className='ellipsis' directionalHint={DirectionalHint.bottomCenter} tooltipProps={{ calloutProps: { @@ -42,10 +43,10 @@ export const Command2 = (): any => { </TooltipHost> </div> <p className='lineMargin'>Trial command</p> - <div className='nowrap'> + <div className='ellipsis'> <TooltipHost content={trialCommand || 'unknown'} - className='nowrap' + className='ellipsis' directionalHint={DirectionalHint.bottomCenter} tooltipProps={{ calloutProps: { diff --git a/ts/webui/src/components/overview/experiment/BasicInfo.tsx b/ts/webui/src/components/overview/params/BasicInfo.tsx similarity index 89% rename from ts/webui/src/components/overview/experiment/BasicInfo.tsx rename to ts/webui/src/components/overview/params/BasicInfo.tsx index e5ca1b82d9..a1b1b641f1 100644 --- a/ts/webui/src/components/overview/experiment/BasicInfo.tsx +++ b/ts/webui/src/components/overview/params/BasicInfo.tsx @@ -6,10 +6,11 @@ import { formatTimestamp } from '../../../static/function'; import { useId } from '@uifabric/react-hooks'; import { BestMetricContext } from '../../Overview'; import { styles } from './basicInfoStyles'; -import '../../../static/style/progress/progress.scss'; -import '../../../static/style/progress/probar.scss'; +import '../../../static/style/overview/probar.scss'; +import '../../../static/style/overview/basic.scss'; +import '../../../static/style/common/ellipsis.scss'; -export const ReBasicInfo = (): any => { +export const BasicInfo = (): any => { const labelId: string = useId('callout-label'); const descriptionId: string = useId('callout-description'); const ref = React.createRef<HTMLDivElement>(); @@ -26,9 +27,9 @@ export const ReBasicInfo = (): any => { <Stack horizontal horizontalAlign='space-between' className='mess'> <div className='basic'> <p>Name</p> - <div className='nowrap'>{EXPERIMENT.profile.params.experimentName}</div> + <div className='ellipsis'>{EXPERIMENT.profile.params.experimentName}</div> <p className='margin'>ID</p> - <div className='nowrap'>{EXPERIMENT.profile.id}</div> + <div className='ellipsis'>{EXPERIMENT.profile.id}</div> </div> <div className='basic'> <p>Status</p> @@ -86,9 +87,9 @@ export const ReBasicInfo = (): any => { </div> <div className='basic'> <p>Start time</p> - <div className='nowrap'>{formatTimestamp(EXPERIMENT.profile.startTime)}</div> + <div className='ellipsis'>{formatTimestamp(EXPERIMENT.profile.startTime)}</div> <p className='margin'>End time</p> - <div className='nowrap'>{formatTimestamp(EXPERIMENT.profile.endTime)}</div> + <div className='ellipsis'>{formatTimestamp(EXPERIMENT.profile.endTime)}</div> </div> </Stack> {/* learn about click -> default active key is dispatcher. */} diff --git a/ts/webui/src/components/overview/experiment/basicInfoStyles.ts b/ts/webui/src/components/overview/params/basicInfoStyles.ts similarity index 100% rename from ts/webui/src/components/overview/experiment/basicInfoStyles.ts rename to ts/webui/src/components/overview/params/basicInfoStyles.ts diff --git a/ts/webui/src/index.tsx b/ts/webui/src/index.tsx index 2b75824cba..986e95b382 100644 --- a/ts/webui/src/index.tsx +++ b/ts/webui/src/index.tsx @@ -18,7 +18,7 @@ ReactDOM.render( </div> } > - <Route path='/experiment' component={Experiment} /> + <Route path='/experiment' component={Experiment} exact /> <Switch> <App> <Route path='/' component={Overview} exact /> diff --git a/ts/webui/src/static/const.ts b/ts/webui/src/static/const.ts index 54c0ad7c71..e1950743a1 100644 --- a/ts/webui/src/static/const.ts +++ b/ts/webui/src/static/const.ts @@ -15,6 +15,9 @@ const trialJobStatus = [ 'SYS_CANCELED', 'EARLY_STOPPED' ]; +const EXPERIMENTSTATUS = ['INITIALIZED','RUNNING','ERROR','STOPPING','STOPPED','DONE','NO_MORE_TRIAL','TUNER_NO_MORE_TRIAL']; +// 后三个正确吗 +const PLATFORM = ['local', 'remote', 'pai', 'kubeflow', 'aml', 'dlts', 'framework']; const CONTROLTYPE = ['MAX_EXEC_DURATION', 'MAX_TRIAL_NUM', 'TRIAL_CONCURRENCY', 'SEARCH_SPACE']; const MONACO = { readOnly: true, @@ -64,6 +67,8 @@ export { MANAGER_IP, DOWNLOAD_IP, trialJobStatus, + EXPERIMENTSTATUS, + PLATFORM, COLUMNPro, WEBUIDOC, CONTROLTYPE, diff --git a/ts/webui/src/static/style/common/ellipsis.scss b/ts/webui/src/static/style/common/ellipsis.scss new file mode 100644 index 0000000000..9dbcbb62a5 --- /dev/null +++ b/ts/webui/src/static/style/common/ellipsis.scss @@ -0,0 +1,13 @@ +.ellipsis { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.link{ + outline: none; + color: #0071bc; + &:active, &:visited{ + color: #0071bc; + } +} \ No newline at end of file diff --git a/ts/webui/src/static/style/experiment/experiment.scss b/ts/webui/src/static/style/experiment/experiment.scss index daaf10526c..529d92d0bf 100644 --- a/ts/webui/src/static/style/experiment/experiment.scss +++ b/ts/webui/src/static/style/experiment/experiment.scss @@ -1,5 +1,12 @@ +.expBackground{ + background: #f2f2f2; + height: 100%; + .content{ + background: #fff; + } +} + .experimentList{ - border: 1px solid red; padding: 42px; .box{ @@ -14,8 +21,8 @@ width: 10%; text-align: right; - &-button{ - width: 80px; + &-button-open{ + background: #F3F2F1; } } } @@ -27,6 +34,13 @@ position: relative; top: 29px; } + + &-status{ + width: 194px; + } + &-platform{ + width: 150px; + } } .hidden{ @@ -37,10 +51,22 @@ margin-left: 10px; } - .tag{ - font-weight: 500; - background: #f2f2f2; - margin: 0 4px; - padding: 0 6px; + .table{ + .id{ + font-variant: small-caps; + font-size: 15px; + } + } + + .tagContainer{ + + width: 100%; + + .tag{ + font-weight: 500; + background: #f2f2f2; + margin: 0 4px; + padding: 0 6px; + } } } \ No newline at end of file diff --git a/ts/webui/src/static/style/nav/nav.scss b/ts/webui/src/static/style/nav/nav.scss index c075868cc0..1dd51b08df 100644 --- a/ts/webui/src/static/style/nav/nav.scss +++ b/ts/webui/src/static/style/nav/nav.scss @@ -41,6 +41,7 @@ $barHeight: 56px; .nav { width: 100%; + min-width: 1200px; height: $barHeight; line-height: $barHeight; diff --git a/ts/webui/src/static/style/overview/basic.scss b/ts/webui/src/static/style/overview/basic.scss new file mode 100644 index 0000000000..7bf600a54c --- /dev/null +++ b/ts/webui/src/static/style/overview/basic.scss @@ -0,0 +1,23 @@ +.status { + color: #0573bc; + font-size: 20px; + font-weight: 600; + + .status-text { + display: inline-block; + } + + .color { + color: #333; + } +} + +.inputBox { + height: 32px; + margin-top: 5px; +} + +/* office-fabric-ui progressIndicator */ +.ms-ProgressIndicator-itemProgress { + padding: 0; +} diff --git a/ts/webui/src/static/style/overview/overview.scss b/ts/webui/src/static/style/overview/overview.scss index 2c062485e4..33e6c89170 100644 --- a/ts/webui/src/static/style/overview/overview.scss +++ b/ts/webui/src/static/style/overview/overview.scss @@ -90,12 +90,7 @@ $boxGapPadding: 10px; font-weight: 500; color: #484848; } - - .nowrap { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } + } .overviewBestMetric { diff --git a/ts/webui/src/static/style/progress/probar.scss b/ts/webui/src/static/style/overview/probar.scss similarity index 100% rename from ts/webui/src/static/style/progress/probar.scss rename to ts/webui/src/static/style/overview/probar.scss diff --git a/ts/webui/src/static/style/progress/progress.scss b/ts/webui/src/static/style/progress/progress.scss deleted file mode 100644 index 7c026e3275..0000000000 --- a/ts/webui/src/static/style/progress/progress.scss +++ /dev/null @@ -1,71 +0,0 @@ - -.status { - color: #0573bc; - font-size: 20px; - font-weight: 600; - - .status-text { - display: inline-block; - } - - .color { - color: #333; - } -} - -.probar { - width: 100%; - height: 34px; - margin-top: 15px; - - .showProgress { - width: 300px; - height: 30px; - } - - .name { - width: 178px; - box-sizing: border-box; - line-height: 30px; - text-align: center; - color: #fff; - background-color: #999; - border: 2px solid #e6e6e6; - border-top-left-radius: 12px; - border-bottom-left-radius: 12px; - } - - .boundary { - width: 100%; - line-height: 24px; - font-size: 12px; - color: #212121; - - .right { - text-align: right; - } - } - - .description { - line-height: 34px; - margin-left: 6px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} - -.inputBox { - height: 32px; - margin-top: 5px; -} - -/* office-fabric-ui progressIndicator */ -.ms-ProgressIndicator-itemProgress { - padding: 0; -} - -.cursor, -.cursor:hover { - cursor: pointer; -} diff --git a/ts/webui/src/static/style/table.scss b/ts/webui/src/static/style/table.scss index 054c7a63e9..c89b8e0ad8 100644 --- a/ts/webui/src/static/style/table.scss +++ b/ts/webui/src/static/style/table.scss @@ -13,12 +13,6 @@ } } -.ellipsis { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - #succeTable, #tableList { .commonTableStyle .leftTitle div { From b3963b0bcdbebdfd22696905a0b7a24c4ab4653c Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Wed, 25 Nov 2020 08:09:39 +0000 Subject: [PATCH 05/15] use api to dev --- ts/webui/src/App.tsx | 104 ++++---- ts/webui/src/components/NavCon.tsx | 113 +++----- ts/webui/src/components/NavConst.ts | 21 ++ .../components/managementExp/Experiment.tsx | 245 ++++++++++-------- ts/webui/src/static/function.ts | 2 +- ts/webui/src/static/interface.ts | 16 +- .../src/static/model/experimentsManager.ts | 27 ++ 7 files changed, 294 insertions(+), 234 deletions(-) create mode 100644 ts/webui/src/components/NavConst.ts create mode 100644 ts/webui/src/static/model/experimentsManager.ts diff --git a/ts/webui/src/App.tsx b/ts/webui/src/App.tsx index eb98aaa629..6e3d5a5c1c 100644 --- a/ts/webui/src/App.tsx +++ b/ts/webui/src/App.tsx @@ -139,59 +139,61 @@ class App extends React.Component<{}, AppState> { { errorWhere: TRIALS.latestMetricDataError(), errorMessage: TRIALS.getLatestMetricDataErrorMessage() }, { errorWhere: TRIALS.metricDataRangeError(), errorMessage: TRIALS.metricDataRangeErrorMessage() } ]; - + return ( - <Stack className='nni' style={{ minHeight: window.innerHeight }}> - <div className='header'> - <div className='headerCon'> - <NavCon changeInterval={this.changeInterval} refreshFunction={this.lastRefresh} /> - </div> - </div> - <Stack className='contentBox'> - <Stack className='content'> - {/* search space & config */} - { - isManagerExperimentPage() - ? - null - : - <SlideNavBtns /> - } - {/* if api has error field, show error message */} - {errorList.map( - (item, key) => - item.errorWhere && ( - <div key={key} className='warning'> - <MessageInfo info={item.errorMessage} typeInfo='error' /> - </div> - ) - )} - {isillegalFinal && ( - <div className='warning'> - <MessageInfo info={expWarningMessage} typeInfo='warning' /> + <React.Fragment> + { + isManagerExperimentPage() + ? + null + : + <Stack className='nni' style={{ minHeight: window.innerHeight }}> + <div className='header'> + <div className='headerCon'> + <NavCon changeInterval={this.changeInterval} refreshFunction={this.lastRefresh} /> + </div> </div> - )} - <AppContext.Provider - value={{ - interval, - columnList, - changeColumn: this.changeColumn, - experimentUpdateBroadcast, - trialsUpdateBroadcast, - metricGraphMode, - maxDurationUnit, - changeMaxDurationUnit: this.changeMaxDurationUnit, - changeMetricGraphMode: this.changeMetricGraphMode, - bestTrialEntries, - changeEntries: this.changeEntries, - updateOverviewPage: this.updateOverviewPage - }} - > - {this.props.children} - </AppContext.Provider> - </Stack> - </Stack> - </Stack> + <Stack className='contentBox'> + <Stack className='content'> + {/* search space & config */} + <SlideNavBtns /> + {/* if api has error field, show error message */} + {errorList.map( + (item, key) => + item.errorWhere && ( + <div key={key} className='warning'> + <MessageInfo info={item.errorMessage} typeInfo='error' /> + </div> + ) + )} + {isillegalFinal && ( + <div className='warning'> + <MessageInfo info={expWarningMessage} typeInfo='warning' /> + </div> + )} + <AppContext.Provider + value={{ + interval, + columnList, + changeColumn: this.changeColumn, + experimentUpdateBroadcast, + trialsUpdateBroadcast, + metricGraphMode, + maxDurationUnit, + changeMaxDurationUnit: this.changeMaxDurationUnit, + changeMetricGraphMode: this.changeMetricGraphMode, + bestTrialEntries, + changeEntries: this.changeEntries, + updateOverviewPage: this.updateOverviewPage + }} + > + {this.props.children} + </AppContext.Provider> + </Stack> + </Stack> + </Stack> + } + </React.Fragment> ); } diff --git a/ts/webui/src/components/NavCon.tsx b/ts/webui/src/components/NavCon.tsx index d6344d4230..a23a418177 100644 --- a/ts/webui/src/components/NavCon.tsx +++ b/ts/webui/src/components/NavCon.tsx @@ -3,35 +3,20 @@ import axios from 'axios'; import { WEBUIDOC, MANAGER_IP } from '../static/const'; import { Stack, - initializeIcons, StackItem, CommandBarButton, IContextualMenuProps, - IStackTokens, - IStackStyles } from '@fluentui/react'; import { Link } from 'react-router-dom'; -import { infoIconAbout, timeIcon, disableUpdates, requency, closeTimer, RevToggleKey, ChevronRightMed } from './buttons/Icon'; +import { infoIconAbout, timeIcon, disableUpdates, requency, closeTimer, ChevronRightMed } from './buttons/Icon'; import ExperimentSummaryPanel from './modals/ExperimentSummaryPanel'; import { OVERVIEWTABS, DETAILTABS, NNILOGO } from './stateless-component/NNItabs'; import { EXPERIMENT } from '../static/datamodel'; -import { isManagerExperimentPage } from '../static/function'; +import {stackTokens, + stackStyle} from './NavConst'; import '../static/style/nav/nav.scss'; import '../static/style/icon.scss'; -initializeIcons(); -const stackTokens: IStackTokens = { - childrenGap: 15 -}; -const stackStyle: IStackStyles = { - root: { - minWidth: 400, - height: 56, - display: 'flex', - verticalAlign: 'center' - } -}; - interface NavState { version: string; menuVisible: boolean; @@ -135,66 +120,48 @@ class NavCon extends React.Component<NavProps, NavState> { }; return ( <Stack horizontal className='nav'> - { - isManagerExperimentPage() - ? - <React.Fragment> - <StackItem grow={30} styles={{ root: { minWidth: 300, display: 'flex', verticalAlign: 'center' } }}> - <span className='desktop-logo'>{NNILOGO}</span> - <span className='logoTitle'>Neural Network Intelligence</span> - </StackItem> - <StackItem grow={70} className='navOptions'> - <Stack horizontal horizontalAlign='end' tokens={stackTokens} styles={stackStyle}> - <Link to="/oview" className='experiment'> - <CommandBarButton iconProps={RevToggleKey} text="Back to the experiment" /> - </Link> - </Stack> - </StackItem> - </React.Fragment> - : - <React.Fragment> - <StackItem grow={30} styles={{ root: { minWidth: 300, display: 'flex', verticalAlign: 'center' } }}> - <span className='desktop-logo'>{NNILOGO}</span> - <span className='left-right-margin'>{OVERVIEWTABS}</span> - <span>{DETAILTABS}</span> - </StackItem> - <StackItem grow={70} className='navOptions'> - <Stack horizontal horizontalAlign='end' tokens={stackTokens} styles={stackStyle}> - {/* refresh button danyi*/} - {/* TODO: fix bug */} - {/* <CommandBarButton + <React.Fragment> + <StackItem grow={30} styles={{ root: { minWidth: 300, display: 'flex', verticalAlign: 'center' } }}> + <span className='desktop-logo'>{NNILOGO}</span> + <span className='left-right-margin'>{OVERVIEWTABS}</span> + <span>{DETAILTABS}</span> + </StackItem> + <StackItem grow={70} className='navOptions'> + <Stack horizontal horizontalAlign='end' tokens={stackTokens} styles={stackStyle}> + {/* refresh button danyi*/} + {/* TODO: fix bug */} + {/* <CommandBarButton iconProps={{ iconName: 'sync' }} text="Refresh" onClick={this.props.refreshFunction} /> */} - <div className='nav-refresh'> - <CommandBarButton - iconProps={refreshFrequency === '' ? disableUpdates : timeIcon} - text={refreshText} - menuProps={this.refreshProps} - /> - <div className='nav-refresh-num'>{refreshFrequency}</div> - </div> - <CommandBarButton - iconProps={{ iconName: 'ShowResults' }} - text='Experiment summary' - onClick={this.showExpcontent} - /> - <CommandBarButton iconProps={infoIconAbout} text='About' menuProps={aboutProps} /> - <Link to="/experiment" className='experiment'> - <div className='expNavTitle'><span>All experiment</span>{ChevronRightMed}</div> - {/* <CommandBarButton iconProps={ChevronRightMed} text="All experiment" /> */} - </Link> - </Stack> - </StackItem> - {isvisibleExperimentDrawer && ( - <ExperimentSummaryPanel - closeExpDrawer={this.closeExpDrawer} - experimentProfile={EXPERIMENT.profile} + <div className='nav-refresh'> + <CommandBarButton + iconProps={refreshFrequency === '' ? disableUpdates : timeIcon} + text={refreshText} + menuProps={this.refreshProps} /> - )} - </React.Fragment> - } + <div className='nav-refresh-num'>{refreshFrequency}</div> + </div> + <CommandBarButton + iconProps={{ iconName: 'ShowResults' }} + text='Experiment summary' + onClick={this.showExpcontent} + /> + <CommandBarButton iconProps={infoIconAbout} text='About' menuProps={aboutProps} /> + <Link to="/experiment" className='experiment'> + <div className='expNavTitle'><span>All experiment</span>{ChevronRightMed}</div> + {/* <CommandBarButton iconProps={ChevronRightMed} text="All experiment" /> */} + </Link> + </Stack> + </StackItem> + {isvisibleExperimentDrawer && ( + <ExperimentSummaryPanel + closeExpDrawer={this.closeExpDrawer} + experimentProfile={EXPERIMENT.profile} + /> + )} + </React.Fragment> </Stack> ); diff --git a/ts/webui/src/components/NavConst.ts b/ts/webui/src/components/NavConst.ts new file mode 100644 index 0000000000..0c47087fcb --- /dev/null +++ b/ts/webui/src/components/NavConst.ts @@ -0,0 +1,21 @@ +import { + IStackTokens, + IStackStyles +} from '@fluentui/react'; + +const stackTokens: IStackTokens = { + childrenGap: 15 +}; +const stackStyle: IStackStyles = { + root: { + minWidth: 400, + height: 56, + display: 'flex', + verticalAlign: 'center' + } +}; + +export { + stackTokens, + stackStyle +} \ No newline at end of file diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/Experiment.tsx index ff48d33c0d..107a4089e5 100644 --- a/ts/webui/src/components/managementExp/Experiment.tsx +++ b/ts/webui/src/components/managementExp/Experiment.tsx @@ -1,38 +1,36 @@ import * as React from 'react'; -import { Stack, DetailsList, DefaultButton, Icon, SearchBox, Dropdown, DatePicker, DayOfWeek, TooltipHost, DirectionalHint } from '@fluentui/react'; +import { Stack, StackItem, DetailsList, DefaultButton, Icon, SearchBox, Dropdown, DatePicker, DayOfWeek, TooltipHost, DirectionalHint, CommandBarButton } from '@fluentui/react'; +import { Link } from 'react-router-dom'; import { formatTimestamp } from '../../static/function'; import { TOOLTIP_BACKGROUND_COLOR, EXPERIMENTSTATUS, PLATFORM } from '../../static/const'; +import { RevToggleKey } from '../buttons/Icon'; +import { NNILOGO } from '../stateless-component/NNItabs'; import DayPickerStrings from './experimentConst'; +import { + stackTokens, + stackStyle +} from '../NavConst'; import '../../App.scss'; +import '../../static/style/nav/nav.scss'; import '../../static/style/experiment/experiment.scss'; import '../../static/style/common/ellipsis.scss'; import '../../static/style/tableStatus.css'; -const data = require('./experiment.json'); +import { ExperimentsManager } from '../../static/model/experimentsManager'; +import { AllExperimentList } from '../../static/interface'; -interface AllExperimentList { - name: string; - id: string; - status: string; - port: number; - platform: string; - startTime: number; - endTime: number; - tag: string[]; -} - -interface OverviewState { +interface ExpListState { hideFilter: boolean; searchInputVal: string; selectedStatus: string; selectedPlatform: string; selectedStartDate?: Date; selectedEndDate?: Date; - source: Array<AllExperimentList>; - filterSource: Array<AllExperimentList>; - filterSourceOrigin: Array<AllExperimentList>; + source: AllExperimentList[]; + filterSource: AllExperimentList[]; + filterSourceOrigin: AllExperimentList[]; } -class Experiment extends React.Component<{}, OverviewState> { +class Experiment extends React.Component<{}, ExpListState> { constructor(props) { super(props); this.state = { @@ -40,103 +38,131 @@ class Experiment extends React.Component<{}, OverviewState> { searchInputVal: '', selectedStatus: '', selectedPlatform: '', - source: data, - filterSource: data, - filterSourceOrigin: data + source: [], + filterSource: [], + filterSourceOrigin: [] }; } + async componentDidMount(): Promise<void> { + const EXPERIMENTMANAGER = new ExperimentsManager(); + await EXPERIMENTMANAGER.init(); + const result = EXPERIMENTMANAGER.getExperimentList(); + this.setState(() => ({ + source: result, + filterSource: result, + filterSourceOrigin: result + })); + } + render(): React.ReactNode { - const { hideFilter, selectedStatus, source, selectedPlatform, selectedStartDate, selectedEndDate } = this.state; + const { hideFilter, selectedStatus, source, filterSourceOrigin, selectedPlatform, selectedStartDate, selectedEndDate } = this.state; + console.info('source', source); return ( - <Stack className='contentBox expBackground'> - <Stack className='content'> - <Stack className='experimentList'> - <Stack className='box' horizontal> - <div className='search'> - <SearchBox - className='search-input' - placeholder="Search the experiment by name, ID, tags..." - onEscape={(_ev): void => { - - this.setState(() => ({ source: data })); - console.log('Custom onEscape Called'); - }} - onClear={(_ev): void => { - this.setState(() => ({ source: data })); - console.log('onClear'); - // 点 × 操作 - console.info('source', this.state.source); - }} - onChange={this.searchNameAndId.bind(this)} + <Stack className='nni' style={{ minHeight: window.innerHeight }}> + <div className='header'> + <div className='headerCon'> + <Stack className='nav' horizontal> + <StackItem grow={30} styles={{ root: { minWidth: 300, display: 'flex', verticalAlign: 'center' } }}> + <span className='desktop-logo'>{NNILOGO}</span> + <span className='logoTitle'>Neural Network Intelligence</span> + </StackItem> + <StackItem grow={70} className='navOptions'> + <Stack horizontal horizontalAlign='end' tokens={stackTokens} styles={stackStyle}> + <Link to='/oview' className='experiment'> + <CommandBarButton iconProps={RevToggleKey} text='Back to the experiment' /> + </Link> + </Stack> + </StackItem> + </Stack> + </div> + </div> + <Stack className='contentBox expBackground'> + <Stack className='content'> + <Stack className='experimentList'> + <Stack className='box' horizontal> + <div className='search'> + <SearchBox + className='search-input' + placeholder='Search the experiment by name, ID, tags...' + onEscape={(_ev): void => { + // xxxxxxxx + this.setState(() => ({ source: filterSourceOrigin })); + }} + onClear={(_ev): void => { + // xxxxxxxx + this.setState(() => ({ source: filterSourceOrigin })); + }} + onChange={this.searchNameAndId.bind(this)} + /> + </div> + <div className='filter'> + <DefaultButton + onClick={this.clickFilter.bind(this)} + // className='fiter-button' + className={`${!hideFilter ? 'filter-button-open' : null}`} + > + <Icon iconName='Equalizer' /> + <span className='margin'>Filter</span> + </DefaultButton> + </div> + </Stack> + <Stack className={`${hideFilter ? 'hidden' : ''} filter-condition`} horizontal gap={25}> + <Dropdown + label='Status' + selectedKey={selectedStatus} + onChange={this.selectStatus.bind(this)} + placeholder='Select an option' + options={this.fillOptions(EXPERIMENTSTATUS)} + className='filter-condition-status' + /> + <Dropdown + label='Platform' + selectedKey={selectedPlatform} + onChange={this.selectPlatform.bind(this)} + placeholder='Select an option' + options={this.fillOptions(PLATFORM)} + className='filter-condition-platform' + /> + <DatePicker + label='Start time' + firstDayOfWeek={DayOfWeek.Sunday} + strings={DayPickerStrings} + showMonthPickerAsOverlay={true} + placeholder='Select a date...' + ariaLabel='Select a date' + value={selectedStartDate} + // dateTimeFormatter={formatMonthDayYear()} + // formatDate={(date?: Date): string => date!.toString()} + onSelectDate={this.getSelectedData.bind(this, 'start')} + /> + <DatePicker + label='End time' + firstDayOfWeek={DayOfWeek.Sunday} + strings={DayPickerStrings} + showMonthPickerAsOverlay={true} + placeholder='Select a date...' + ariaLabel='Select a date' + value={selectedEndDate} + onSelectDate={this.getSelectedData.bind(this, 'end')} /> - </div> - <div className='filter'> <DefaultButton - onClick={this.clickFilter.bind(this)} - // className='fiter-button' - className={`${!hideFilter ? 'filter-button-open' : null}`} + onClick={this.setOriginSource.bind(this)} + className='reset' > - <Icon iconName='Equalizer' /> - <span className='margin'>Filter</span> + <Icon iconName='Refresh' /> + <span className='margin'>Reset</span> </DefaultButton> - </div> - </Stack> - <Stack className={`${hideFilter ? 'hidden' : ''} filter-condition`} horizontal gap={25}> - <Dropdown - label="Status" - selectedKey={selectedStatus} - onChange={this.selectStatus.bind(this)} - placeholder="Select an option" - options={this.fillOptions(EXPERIMENTSTATUS)} - className='filter-condition-status' - /> - <Dropdown - label="Platform" - selectedKey={selectedPlatform} - onChange={this.selectPlatform.bind(this)} - placeholder="Select an option" - options={this.fillOptions(PLATFORM)} - className='filter-condition-platform' - /> - <DatePicker - label='Start time' - firstDayOfWeek={DayOfWeek.Sunday} - strings={DayPickerStrings} - showMonthPickerAsOverlay={true} - placeholder="Select a date..." - ariaLabel="Select a date" - value={selectedStartDate} - // dateTimeFormatter={formatMonthDayYear()} - // formatDate={(date?: Date): string => date!.toString()} - onSelectDate={this.getSelectedData.bind(this, 'start')} - /> - <DatePicker - label='End time' - firstDayOfWeek={DayOfWeek.Sunday} - strings={DayPickerStrings} - showMonthPickerAsOverlay={true} - placeholder="Select a date..." - ariaLabel="Select a date" - value={selectedEndDate} - onSelectDate={this.getSelectedData.bind(this, 'end')} + </Stack> + <DetailsList + columns={this.columns} + items={source} + setKey='set' + compact={true} + selectionMode={0} // close selector function + className='table' /> - <DefaultButton - onClick={this.setOriginSource.bind(this)} - className='reset' - > - <Icon iconName='Refresh' /> - <span className='margin'>Reset</span> - </DefaultButton> </Stack> - <DetailsList - columns={this.columns} - items={source} - setKey='set' - compact={true} - selectionMode={0} // close selector function - className='table' - /> </Stack> </Stack> </Stack> @@ -164,7 +190,6 @@ class Experiment extends React.Component<{}, OverviewState> { isResizable: true, data: 'number', // onColumnClick: this.onColumnClick, - // onClick={() => {window.open(webuiPortal)}} onRender: (item: any): React.ReactNode => { const hostname = window.location.hostname; const protocol = window.location.protocol; @@ -303,15 +328,19 @@ class Experiment extends React.Component<{}, OverviewState> { } private searchNameAndId(_event, newValue): void { + const {filterSourceOrigin} = this.state; if (newValue !== undefined) { // 空格回退操作 if (newValue === '') { - this.setState(() => ({ source: data, filterSource: data })); + // xxxxxxxx + this.setState(() => ({ source: filterSourceOrigin, filterSource: filterSourceOrigin })); console.info('source', this.state.source); return; } - const result = this.state.source.filter(item => (item.name.includes(newValue) || - item.id.includes(newValue) || item.tag.join(',').includes(newValue))); + // TODOTODO + const result = this.state.source.filter(item => (item.experimentName.includes(newValue) || + // item.id.includes(newValue) || item.tag.join(',').includes(newValue))); + item.tag.join(',').includes(newValue))); this.setState(() => ({ source: result, filterSource: result, filterSourceOrigin: result })); diff --git a/ts/webui/src/static/function.ts b/ts/webui/src/static/function.ts index 20feda5a80..7c42f61bb5 100644 --- a/ts/webui/src/static/function.ts +++ b/ts/webui/src/static/function.ts @@ -263,7 +263,7 @@ function formatComplexTypeValue(value: any): string | number { } function isManagerExperimentPage(): boolean { - return (location.href.indexOf('experiment') === -1) ? false : true; + return (location.pathname.indexOf('experiment') === -1) ? false : true; } function caclMonacoEditorHeight(height): number { diff --git a/ts/webui/src/static/interface.ts b/ts/webui/src/static/interface.ts index 5eb7fbd82b..d3c4b63305 100644 --- a/ts/webui/src/static/interface.ts +++ b/ts/webui/src/static/interface.ts @@ -212,6 +212,19 @@ interface EventMap { [key: string]: () => void; } +interface AllExperimentList{ + port: number; + startTime: number; + endTime: number; + status: string; + platform: string; + experimentName: string; + tag: string[]; + pid: number; + webuiUrl: string[]; + logDir: string[]; +} + export { TableObj, TableRecord, @@ -233,5 +246,6 @@ export { NNIManagerStatus, EventMap, SingleAxis, - MultipleAxes + MultipleAxes, + AllExperimentList }; diff --git a/ts/webui/src/static/model/experimentsManager.ts b/ts/webui/src/static/model/experimentsManager.ts new file mode 100644 index 0000000000..0aac8ddea7 --- /dev/null +++ b/ts/webui/src/static/model/experimentsManager.ts @@ -0,0 +1,27 @@ +import { MANAGER_IP } from '../const'; +import { AllExperimentList } from '../interface'; +import { requestAxios } from '../function'; + +class ExperimentsManager { + + private experimentList: AllExperimentList[] = []; + + public getExperimentList(): AllExperimentList[] { + console.info('333'); // eslint-disable-line + console.info(this.experimentList); // eslint-disable-line + return this.experimentList; + } + + public async init(): Promise<void> { + await requestAxios(`${MANAGER_IP}/experiments-info`) + .then(data => { + this.experimentList = data; + }); + // .catch(error => { + // return [] as AllExperimentList[]; + // }); + + } +} + +export { ExperimentsManager }; From 320cc7a2292d15e2cf3f1799a82bed1528d0c935 Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Thu, 26 Nov 2020 12:27:45 +0000 Subject: [PATCH 06/15] refactor code --- ts/webui/.eslintrc | 4 +- ts/webui/scripts/start.js | 2 +- ts/webui/src/App.tsx | 102 ++-- ts/webui/src/components/NavCon.tsx | 18 +- ts/webui/src/components/NavConst.ts | 10 +- .../components/managementExp/Experiment.tsx | 464 ++++++++---------- .../components/managementExp/FilterBtns.tsx | 82 ++++ .../src/components/managementExp/Header.tsx | 26 + .../components/managementExp/NameColumn.tsx | 28 ++ .../components/managementExp/expFunction.ts | 37 ++ .../components/managementExp/experiment.json | 82 ---- .../managementExp/experimentConst.ts | 40 +- .../overview/table/SuccessTable.tsx | 2 +- ts/webui/src/static/const.ts | 13 +- ts/webui/src/static/function.ts | 2 +- ts/webui/src/static/interface.ts | 9 +- .../src/static/model/experimentsManager.ts | 22 +- 17 files changed, 476 insertions(+), 467 deletions(-) create mode 100644 ts/webui/src/components/managementExp/FilterBtns.tsx create mode 100644 ts/webui/src/components/managementExp/Header.tsx create mode 100644 ts/webui/src/components/managementExp/NameColumn.tsx create mode 100644 ts/webui/src/components/managementExp/expFunction.ts delete mode 100644 ts/webui/src/components/managementExp/experiment.json diff --git a/ts/webui/.eslintrc b/ts/webui/.eslintrc index 7e7703264c..e220b1f541 100644 --- a/ts/webui/.eslintrc +++ b/ts/webui/.eslintrc @@ -21,8 +21,7 @@ "prettier" ], "rules": { - "prettier/prettier": 0, - "eslint/eslint": 0, + "prettier/prettier": 2, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-namespace": 0, "@typescript-eslint/consistent-type-assertions": 0, @@ -32,7 +31,6 @@ "@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }], "arrow-parens": [2, "as-needed"], "no-inner-declarations": 0, - "no-console": 0, "no-empty": 2, "no-multiple-empty-lines": [2, { "max": 1 }], "react/display-name": 0 diff --git a/ts/webui/scripts/start.js b/ts/webui/scripts/start.js index 247f0fcccc..e6236bd6a2 100644 --- a/ts/webui/scripts/start.js +++ b/ts/webui/scripts/start.js @@ -41,7 +41,7 @@ if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { } // Tools like Cloud9 rely on this. -const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 9999; +const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; const HOST = process.env.HOST || '0.0.0.0'; if (process.env.HOST) { diff --git a/ts/webui/src/App.tsx b/ts/webui/src/App.tsx index 2be2e5e710..d61e344bcc 100644 --- a/ts/webui/src/App.tsx +++ b/ts/webui/src/App.tsx @@ -31,15 +31,15 @@ export const AppContext = React.createContext({ bestTrialEntries: '10', maxDurationUnit: 'm', // eslint-disable-next-line @typescript-eslint/no-empty-function - changeColumn: (_val: string[]) => { }, + changeColumn: (_val: string[]) => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function - changeMetricGraphMode: (_val: 'max' | 'min') => { }, + changeMetricGraphMode: (_val: 'max' | 'min') => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function - changeMaxDurationUnit: (_val: string) => { }, + changeMaxDurationUnit: (_val: string) => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function - changeEntries: (_val: string) => { }, + changeEntries: (_val: string) => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function - updateOverviewPage: () => { } + updateOverviewPage: () => {} }); class App extends React.Component<{}, AppState> { @@ -143,57 +143,53 @@ class App extends React.Component<{}, AppState> { return ( <React.Fragment> - { - isManagerExperimentPage() - ? - null - : - <Stack className='nni' style={{ minHeight: window.innerHeight }}> - <div className='header'> - <div className='headerCon'> - <NavCon changeInterval={this.changeInterval} refreshFunction={this.lastRefresh} /> - </div> + {isManagerExperimentPage() ? null : ( + <Stack className='nni' style={{ minHeight: window.innerHeight }}> + <div className='header'> + <div className='headerCon'> + <NavCon changeInterval={this.changeInterval} refreshFunction={this.lastRefresh} /> </div> - <Stack className='contentBox'> - <Stack className='content'> - {/* search space & config */} - <SlideNavBtns /> - {/* if api has error field, show error message */} - {errorList.map( - (item, key) => - item.errorWhere && ( - <div key={key} className='warning'> - <MessageInfo info={item.errorMessage} typeInfo='error' /> - </div> - ) - )} - {isillegalFinal && ( - <div className='warning'> - <MessageInfo info={expWarningMessage} typeInfo='warning' /> - </div> - )} - <AppContext.Provider - value={{ - interval, - columnList, - changeColumn: this.changeColumn, - experimentUpdateBroadcast, - trialsUpdateBroadcast, - metricGraphMode, - maxDurationUnit, - changeMaxDurationUnit: this.changeMaxDurationUnit, - changeMetricGraphMode: this.changeMetricGraphMode, - bestTrialEntries, - changeEntries: this.changeEntries, - updateOverviewPage: this.updateOverviewPage - }} - > - {this.props.children} - </AppContext.Provider> - </Stack> + </div> + <Stack className='contentBox'> + <Stack className='content'> + {/* search space & config */} + <SlideNavBtns /> + {/* if api has error field, show error message */} + {errorList.map( + (item, key) => + item.errorWhere && ( + <div key={key} className='warning'> + <MessageInfo info={item.errorMessage} typeInfo='error' /> + </div> + ) + )} + {isillegalFinal && ( + <div className='warning'> + <MessageInfo info={expWarningMessage} typeInfo='warning' /> + </div> + )} + <AppContext.Provider + value={{ + interval, + columnList, + changeColumn: this.changeColumn, + experimentUpdateBroadcast, + trialsUpdateBroadcast, + metricGraphMode, + maxDurationUnit, + changeMaxDurationUnit: this.changeMaxDurationUnit, + changeMetricGraphMode: this.changeMetricGraphMode, + bestTrialEntries, + changeEntries: this.changeEntries, + updateOverviewPage: this.updateOverviewPage + }} + > + {this.props.children} + </AppContext.Provider> </Stack> </Stack> - } + </Stack> + )} </React.Fragment> ); } diff --git a/ts/webui/src/components/NavCon.tsx b/ts/webui/src/components/NavCon.tsx index a23a418177..e867eb20ef 100644 --- a/ts/webui/src/components/NavCon.tsx +++ b/ts/webui/src/components/NavCon.tsx @@ -1,19 +1,13 @@ import * as React from 'react'; import axios from 'axios'; import { WEBUIDOC, MANAGER_IP } from '../static/const'; -import { - Stack, - StackItem, - CommandBarButton, - IContextualMenuProps, -} from '@fluentui/react'; +import { Stack, StackItem, CommandBarButton, IContextualMenuProps } from '@fluentui/react'; import { Link } from 'react-router-dom'; import { infoIconAbout, timeIcon, disableUpdates, requency, closeTimer, ChevronRightMed } from './buttons/Icon'; import ExperimentSummaryPanel from './modals/ExperimentSummaryPanel'; import { OVERVIEWTABS, DETAILTABS, NNILOGO } from './stateless-component/NNItabs'; import { EXPERIMENT } from '../static/datamodel'; -import {stackTokens, - stackStyle} from './NavConst'; +import { stackTokens, stackStyle } from './NavConst'; import '../static/style/nav/nav.scss'; import '../static/style/icon.scss'; @@ -149,8 +143,11 @@ class NavCon extends React.Component<NavProps, NavState> { onClick={this.showExpcontent} /> <CommandBarButton iconProps={infoIconAbout} text='About' menuProps={aboutProps} /> - <Link to="/experiment" className='experiment'> - <div className='expNavTitle'><span>All experiment</span>{ChevronRightMed}</div> + <Link to='/experiment' className='experiment'> + <div className='expNavTitle'> + <span>All experiment</span> + {ChevronRightMed} + </div> {/* <CommandBarButton iconProps={ChevronRightMed} text="All experiment" /> */} </Link> </Stack> @@ -163,7 +160,6 @@ class NavCon extends React.Component<NavProps, NavState> { )} </React.Fragment> </Stack> - ); } diff --git a/ts/webui/src/components/NavConst.ts b/ts/webui/src/components/NavConst.ts index 0c47087fcb..9ebb93db7c 100644 --- a/ts/webui/src/components/NavConst.ts +++ b/ts/webui/src/components/NavConst.ts @@ -1,7 +1,4 @@ -import { - IStackTokens, - IStackStyles -} from '@fluentui/react'; +import { IStackTokens, IStackStyles } from '@fluentui/react'; const stackTokens: IStackTokens = { childrenGap: 15 @@ -15,7 +12,4 @@ const stackStyle: IStackStyles = { } }; -export { - stackTokens, - stackStyle -} \ No newline at end of file +export { stackTokens, stackStyle }; diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/Experiment.tsx index 107a4089e5..ea7aede43f 100644 --- a/ts/webui/src/components/managementExp/Experiment.tsx +++ b/ts/webui/src/components/managementExp/Experiment.tsx @@ -1,46 +1,45 @@ import * as React from 'react'; -import { Stack, StackItem, DetailsList, DefaultButton, Icon, SearchBox, Dropdown, DatePicker, DayOfWeek, TooltipHost, DirectionalHint, CommandBarButton } from '@fluentui/react'; -import { Link } from 'react-router-dom'; -import { formatTimestamp } from '../../static/function'; -import { TOOLTIP_BACKGROUND_COLOR, EXPERIMENTSTATUS, PLATFORM } from '../../static/const'; -import { RevToggleKey } from '../buttons/Icon'; -import { NNILOGO } from '../stateless-component/NNItabs'; -import DayPickerStrings from './experimentConst'; -import { - stackTokens, - stackStyle -} from '../NavConst'; +import { Stack, DetailsList, DefaultButton, Icon, SearchBox, IColumn } from '@fluentui/react'; +import { ExperimentsManager } from '../../static/model/experimentsManager'; +import { formatTimestamp, copyAndSort } from '../../static/function'; +import { AllExperimentList, SortInfo } from '../../static/interface'; +import { compareDate, filterByStatusOrPlatform, getSortedSource } from './expFunction'; +import { Hearder } from './Header'; +import NameColumn from './NameColumn'; +import FilterBtns from './FilterBtns'; import '../../App.scss'; import '../../static/style/nav/nav.scss'; import '../../static/style/experiment/experiment.scss'; import '../../static/style/common/ellipsis.scss'; import '../../static/style/tableStatus.css'; -import { ExperimentsManager } from '../../static/model/experimentsManager'; -import { AllExperimentList } from '../../static/interface'; interface ExpListState { + columns: IColumn[]; hideFilter: boolean; searchInputVal: string; selectedStatus: string; selectedPlatform: string; selectedStartDate?: Date; selectedEndDate?: Date; + sortInfo: SortInfo; source: AllExperimentList[]; - filterSource: AllExperimentList[]; - filterSourceOrigin: AllExperimentList[]; + originExperimentList: AllExperimentList[]; + searchSource: AllExperimentList[]; } class Experiment extends React.Component<{}, ExpListState> { constructor(props) { super(props); this.state = { + columns: this.columns, hideFilter: true, searchInputVal: '', selectedStatus: '', selectedPlatform: '', - source: [], - filterSource: [], - filterSourceOrigin: [] + source: [], // data in table + originExperimentList: [], // api /experiments-info + searchSource: [], // search box search result + sortInfo: { field: '', isDescend: false } }; } @@ -50,33 +49,16 @@ class Experiment extends React.Component<{}, ExpListState> { const result = EXPERIMENTMANAGER.getExperimentList(); this.setState(() => ({ source: result, - filterSource: result, - filterSourceOrigin: result + originExperimentList: result, + searchSource: result })); } render(): React.ReactNode { - const { hideFilter, selectedStatus, source, filterSourceOrigin, selectedPlatform, selectedStartDate, selectedEndDate } = this.state; - console.info('source', source); + const { hideFilter, selectedStatus, source, selectedPlatform, selectedStartDate, selectedEndDate } = this.state; return ( <Stack className='nni' style={{ minHeight: window.innerHeight }}> - <div className='header'> - <div className='headerCon'> - <Stack className='nav' horizontal> - <StackItem grow={30} styles={{ root: { minWidth: 300, display: 'flex', verticalAlign: 'center' } }}> - <span className='desktop-logo'>{NNILOGO}</span> - <span className='logoTitle'>Neural Network Intelligence</span> - </StackItem> - <StackItem grow={70} className='navOptions'> - <Stack horizontal horizontalAlign='end' tokens={stackTokens} styles={stackStyle}> - <Link to='/oview' className='experiment'> - <CommandBarButton iconProps={RevToggleKey} text='Back to the experiment' /> - </Link> - </Stack> - </StackItem> - </Stack> - </div> - </div> + <Hearder /> <Stack className='contentBox expBackground'> <Stack className='content'> <Stack className='experimentList'> @@ -84,22 +66,15 @@ class Experiment extends React.Component<{}, ExpListState> { <div className='search'> <SearchBox className='search-input' - placeholder='Search the experiment by name, ID, tags...' - onEscape={(_ev): void => { - // xxxxxxxx - this.setState(() => ({ source: filterSourceOrigin })); - }} - onClear={(_ev): void => { - // xxxxxxxx - this.setState(() => ({ source: filterSourceOrigin })); - }} + placeholder='Search the experiment by name and ID' + onEscape={this.setOriginSource.bind(this)} + onClear={this.setOriginSource.bind(this)} onChange={this.searchNameAndId.bind(this)} /> </div> <div className='filter'> <DefaultButton onClick={this.clickFilter.bind(this)} - // className='fiter-button' className={`${!hideFilter ? 'filter-button-open' : null}`} > <Icon iconName='Equalizer' /> @@ -108,51 +83,18 @@ class Experiment extends React.Component<{}, ExpListState> { </div> </Stack> <Stack className={`${hideFilter ? 'hidden' : ''} filter-condition`} horizontal gap={25}> - <Dropdown - label='Status' - selectedKey={selectedStatus} - onChange={this.selectStatus.bind(this)} - placeholder='Select an option' - options={this.fillOptions(EXPERIMENTSTATUS)} - className='filter-condition-status' - /> - <Dropdown - label='Platform' - selectedKey={selectedPlatform} - onChange={this.selectPlatform.bind(this)} - placeholder='Select an option' - options={this.fillOptions(PLATFORM)} - className='filter-condition-platform' - /> - <DatePicker - label='Start time' - firstDayOfWeek={DayOfWeek.Sunday} - strings={DayPickerStrings} - showMonthPickerAsOverlay={true} - placeholder='Select a date...' - ariaLabel='Select a date' - value={selectedStartDate} - // dateTimeFormatter={formatMonthDayYear()} - // formatDate={(date?: Date): string => date!.toString()} - onSelectDate={this.getSelectedData.bind(this, 'start')} - /> - <DatePicker - label='End time' - firstDayOfWeek={DayOfWeek.Sunday} - strings={DayPickerStrings} - showMonthPickerAsOverlay={true} - placeholder='Select a date...' - ariaLabel='Select a date' - value={selectedEndDate} - onSelectDate={this.getSelectedData.bind(this, 'end')} + <FilterBtns + selectedStatus={selectedStatus} + selectedPlatform={selectedPlatform} + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + selectedStartDate={selectedStartDate!} + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + selectedEndDate={selectedEndDate!} + selectStatus={this.selectStatus.bind(this)} + selectPlatform={this.selectPlatform.bind(this)} + getSelectedData={this.getSelectedData.bind(this)} + setSearchSource={this.setSearchSource.bind(this)} /> - <DefaultButton - onClick={this.setOriginSource.bind(this)} - className='reset' - > - <Icon iconName='Refresh' /> - <span className='margin'>Reset</span> - </DefaultButton> </Stack> <DetailsList columns={this.columns} @@ -169,59 +111,61 @@ class Experiment extends React.Component<{}, ExpListState> { ); } - private fillOptions(arr: string[]): any { - const list: Array<object> = []; - - arr.map(item => { - list.push({ key: item, text: item }); + private onColumnClick = (_ev: React.MouseEvent<HTMLElement>, getColumn: IColumn): void => { + const { columns, source } = this.state; + const newColumns: IColumn[] = columns.slice(); + const currColumn: IColumn = newColumns.filter(item => getColumn.key === item.key)[0]; + newColumns.forEach((newCol: IColumn) => { + if (newCol === currColumn) { + currColumn.isSortedDescending = !currColumn.isSortedDescending; + currColumn.isSorted = true; + } else { + newCol.isSorted = false; + newCol.isSortedDescending = true; + } }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const newItems = copyAndSort(source, currColumn.fieldName!, currColumn.isSortedDescending); + this.setState(() => ({ + columns: newColumns, + source: newItems, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sortInfo: { field: currColumn.fieldName!, isDescend: currColumn.isSortedDescending } + })); + }; - return list; - } - - // TODO: 引入tag之后调整大小屏幕下的列宽 - private columns = [ + private columns: IColumn[] = [ { name: 'Name', - key: 'name', - fieldName: 'name', // required! - minWidth: 50, - maxWidth: 87, + key: 'experimentName', + fieldName: 'experimentName', // required! + minWidth: 100, + maxWidth: 130, isResizable: true, data: 'number', - // onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => { - const hostname = window.location.hostname; - const protocol = window.location.protocol; - const webuiPortal = `${protocol}//${hostname}:${item.port}/oview`; - return ( - <div className='succeed-padding ellipsis'> - <a href={webuiPortal} className='link' target='_blank' rel='noopener noreferrer'> - {item.name} - </a> - </div> - ); - } + onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode => <NameColumn port={item.port} expName={item.experimentName} /> }, { name: 'ID', key: 'id', fieldName: 'id', - minWidth: 50, - maxWidth: 87, + minWidth: 100, + maxWidth: 130, isResizable: true, className: 'tableHead leftTitle', data: 'string', - // onColumnClick: this.onColumnClick, + onColumnClick: this.onColumnClick, onRender: (item: any): React.ReactNode => <div className='succeed-padding id'>{item.id}</div> }, { name: 'Status', key: 'status', - minWidth: 80, + fieldName: 'status', + minWidth: 100, maxWidth: 150, isResizable: true, - fieldName: 'status', + onColumnClick: this.onColumnClick, onRender: (item: any): React.ReactNode => ( <div className={`${item.status} commonStyle succeed-padding`}>{item.status}</div> ) @@ -230,11 +174,11 @@ class Experiment extends React.Component<{}, ExpListState> { name: 'Port', key: 'port', fieldName: 'port', - minWidth: 65, + minWidth: 60, maxWidth: 90, isResizable: true, data: 'number', - // onColumnClick: this.onColumnClick, + onColumnClick: this.onColumnClick, onRender: (item: any): React.ReactNode => ( <div className='succeed-padding'> <div>{item.port}</div> @@ -245,229 +189,209 @@ class Experiment extends React.Component<{}, ExpListState> { name: 'Platform', key: 'platform', fieldName: 'platform', - minWidth: 80, - maxWidth: 100, + minWidth: 110, + maxWidth: 130, isResizable: true, data: 'string', - // onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => ( - <div className='commonStyle succeed-padding'>{item.platform}</div> - ) + onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode => <div className='commonStyle succeed-padding'>{item.platform}</div> }, { name: 'Start time', key: 'startTime', fieldName: 'startTime', - minWidth: 100, + minWidth: 140, maxWidth: 160, isResizable: true, data: 'number', - // onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => <div className='succeed-padding'> - <div>{formatTimestamp(item.startTime)}</div> - </div> + onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode => ( + <div className='succeed-padding'> + <div>{formatTimestamp(item.startTime)}</div> + </div> + ) }, { name: 'End time', key: 'endTime', fieldName: 'endTime', - minWidth: 100, + minWidth: 120, maxWidth: 160, isResizable: true, data: 'number', - // onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => <div className='succeed-padding'> - <div>{formatTimestamp(item.endTime)}</div> - </div> - }, - { - name: 'Tag', - key: 'tag', - fieldName: 'tag', - minWidth: 100, - maxWidth: 160, - isResizable: true, - data: 'number', - // onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => { - return ( - <TooltipHost - content={item.tag.join(', ')} - className='ellipsis' - directionalHint={DirectionalHint.bottomCenter} - tooltipProps={{ - calloutProps: { - styles: { - beak: { background: TOOLTIP_BACKGROUND_COLOR }, - beakCurtain: { background: TOOLTIP_BACKGROUND_COLOR }, - calloutMain: { background: TOOLTIP_BACKGROUND_COLOR } - } - } - }} - > - <div className='succeed-padding tagContainer'> - { - item.tag.map(tag => { - return ( - <span className='tag' key={tag}>{tag}</span> - ); - }) - } - </div> - </TooltipHost> - ); - } + onColumnClick: this.onColumnClick, + onRender: (item: any): React.ReactNode => ( + <div className='succeed-padding'> + <div>{formatTimestamp(item.endTime)}</div> + </div> + ) } ]; private clickFilter(_e: any): void { const { hideFilter } = this.state; - this.setOriginSource(); + if (!hideFilter === true) { + this.setSearchSource(); + } this.setState(() => ({ hideFilter: !hideFilter })); + } + private setOriginSource(): void { + let { originExperimentList } = this.state; + const { sortInfo } = this.state; + if (originExperimentList !== undefined) { + originExperimentList = this.commonSelectString(originExperimentList, ''); + const sortedData = getSortedSource(originExperimentList, sortInfo); + this.setState(() => ({ + source: sortedData + })); + } } private searchNameAndId(_event, newValue): void { - const {filterSourceOrigin} = this.state; + const { originExperimentList, sortInfo } = this.state; if (newValue !== undefined) { - // 空格回退操作 if (newValue === '') { - // xxxxxxxx - this.setState(() => ({ source: filterSourceOrigin, filterSource: filterSourceOrigin })); - console.info('source', this.state.source); - return; + this.setOriginSource(); + } else { + let result = originExperimentList.filter( + item => + item.experimentName.toLowerCase().includes(newValue.toLowerCase()) || + item.id.toLowerCase().includes(newValue.toLowerCase()) + ); + result = this.commonSelectString(result, ''); + const sortedResult = getSortedSource(result, sortInfo); + this.setState(() => ({ + source: sortedResult, + searchSource: sortedResult + })); } - // TODOTODO - const result = this.state.source.filter(item => (item.experimentName.includes(newValue) || - // item.id.includes(newValue) || item.tag.join(',').includes(newValue))); - item.tag.join(',').includes(newValue))); - this.setState(() => ({ - source: result, filterSource: result, filterSourceOrigin: result - })); } } - // status platform startTime endTime - selectStatus = (_event: React.FormEvent<HTMLDivElement>, item: any): void => { - if (item !== undefined) { - const { selectedPlatform, selectedStartDate, selectedEndDate, filterSource } = this.state; - // 只能set item.key - const hasPlatform = selectedPlatform === '' ? false : true; - const hasStartDate = selectedStartDate === undefined ? false : true; - const hasEndDate = selectedEndDate === undefined ? false : true; - let result; - result = filterSource.filter(temp => (temp.status === item.key)); + /*** + * status, platform + * param + * data: searchSource + * field: no care selected filed + */ + private commonSelectString = (data: AllExperimentList[], field: string): AllExperimentList[] => { + const { selectedStatus, selectedPlatform, selectedStartDate, selectedEndDate } = this.state; + const hasStatus = selectedStatus === '' ? false : true; + const hasPlatform = selectedPlatform === '' ? false : true; + const hasStartDate = selectedStartDate === undefined ? false : true; + const hasEndDate = selectedEndDate === undefined ? false : true; + + if (field === 'status') { if (hasPlatform) { - result = result.filter(temp => (temp.platform === selectedPlatform)); - } - if (hasStartDate) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - result = result.filter(temp => this.compareDate(new Date(temp.startTime), selectedStartDate!)); - } - if (hasEndDate) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - result = result.filter(temp => this.compareDate(new Date(temp.endTime), selectedEndDate!)); + data = filterByStatusOrPlatform(selectedPlatform, 'platform', data); } - this.setState({ selectedStatus: item.key, source: result }); } - }; - - selectPlatform = (_event: React.FormEvent<HTMLDivElement>, item: any): void => { - if (item !== undefined) { - const { selectedStatus, selectedStartDate, selectedEndDate, filterSource } = this.state; - // 只能set item.key - const hasStatus = selectedStatus === '' ? false : true; - const hasStartDate = selectedStartDate === undefined ? false : true; - const hasEndDate = selectedEndDate === undefined ? false : true; - let result; - result = filterSource.filter(temp => (temp.platform === item.key)); + if (field === 'platform') { if (hasStatus) { - result = result.filter(temp => (temp.status === selectedStatus)); + data = filterByStatusOrPlatform(selectedStatus, 'status', data); } - if (hasStartDate) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - result = result.filter(temp => this.compareDate(new Date(temp.startTime), selectedStartDate!)); + } + + if (field === '') { + if (hasPlatform) { + data = filterByStatusOrPlatform(selectedPlatform, 'platform', data); } - if (hasEndDate) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - result = result.filter(temp => this.compareDate(new Date(temp.endTime), selectedEndDate!)); + if (hasStatus) { + data = filterByStatusOrPlatform(selectedStatus, 'status', data); } - this.setState({ selectedPlatform: item.key, source: result }); } + + if (hasStartDate) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + data = data.filter(temp => compareDate(new Date(temp.startTime), selectedStartDate!)); + } + if (hasEndDate) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + data = data.filter(temp => compareDate(new Date(temp.endTime), selectedEndDate!)); + } + + return data; }; - private compareDate(date1: Date, date2: Date): boolean { - if (date1.getFullYear() === date2.getFullYear()) { - if (date1.getMonth() === date2.getMonth()) { - if (date1.getDate() === date2.getDate()) { - return true; - } - } + // status platform startTime endTime + private selectStatus = (_event: React.FormEvent<HTMLDivElement>, item: any): void => { + if (item !== undefined) { + const { searchSource, sortInfo } = this.state; + let result = filterByStatusOrPlatform(item.key, 'status', searchSource); + result = this.commonSelectString(result, 'status'); + this.setState({ selectedStatus: item.key, source: getSortedSource(result, sortInfo) }); } + }; - return false; - } + private selectPlatform = (_event: React.FormEvent<HTMLDivElement>, item: any): void => { + if (item !== undefined) { + const { searchSource, sortInfo } = this.state; + let result = filterByStatusOrPlatform(item.key, 'platform', searchSource); + result = this.commonSelectString(result, 'platform'); + this.setState({ selectedPlatform: item.key, source: getSortedSource(result, sortInfo) }); + } + }; private getSelectedData(type: string, date: Date | null | undefined): void { if (date !== null && date !== undefined) { - const { selectedStatus, selectedPlatform, selectedStartDate, selectedEndDate, filterSource } = this.state; + const { + selectedStatus, + selectedPlatform, + selectedStartDate, + selectedEndDate, + searchSource, + sortInfo + } = this.state; // 只能set item.key const hasStatus = selectedStatus === '' ? false : true; - const hasPlatform = selectedStatus === '' ? false : true; + const hasPlatform = selectedPlatform === '' ? false : true; const hasStartDate = selectedStartDate === undefined ? false : true; const hasEndDate = selectedEndDate === undefined ? false : true; let result; - if (type === 'start') { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - result = filterSource.filter(item => this.compareDate(new Date(item.startTime), date)); - + result = searchSource.filter(item => compareDate(new Date(item.startTime), date)); if (hasStatus) { - result = result.filter(temp => (temp.status === selectedStatus)); + result = result.filter(temp => temp.status === selectedStatus); } - if (hasPlatform) { - result = result.filter(temp => (temp.platform === selectedPlatform)); + result = result.filter(temp => temp.platform === selectedPlatform); } - if (hasEndDate) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - result = result.filter(temp => this.compareDate(new Date(temp.endTime), selectedEndDate!)); + result = result.filter(temp => compareDate(new Date(temp.endTime), selectedEndDate!)); } - - this.setState({ - source: result, + this.setState(() => ({ + source: getSortedSource(result, sortInfo), selectedStartDate: date - }); + })); } else { - result = filterSource.filter(item => this.compareDate(new Date(item.endTime), date)); + result = searchSource.filter(item => compareDate(new Date(item.endTime), date)); if (hasStatus) { - result = result.filter(temp => (temp.status === selectedStatus)); + result = result.filter(temp => temp.status === selectedStatus); } - if (hasPlatform) { - result = result.filter(temp => (temp.platform === selectedPlatform)); + result = result.filter(temp => temp.platform === selectedPlatform); } - if (hasStartDate) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - result = result.filter(temp => this.compareDate(new Date(temp.startTime), selectedEndDate!)); + result = result.filter(temp => compareDate(new Date(temp.startTime), selectedStartDate!)); } - - this.setState({ - source: result, + this.setState(() => ({ + source: getSortedSource(result, sortInfo), selectedEndDate: date - }); + })); } - } } // reset - private setOriginSource(): void { + private setSearchSource(): void { + const { searchSource, sortInfo } = this.state; this.setState(() => ({ - source: this.state.filterSource, + source: getSortedSource(searchSource, sortInfo), selectedStatus: '', selectedPlatform: '', selectedStartDate: undefined, diff --git a/ts/webui/src/components/managementExp/FilterBtns.tsx b/ts/webui/src/components/managementExp/FilterBtns.tsx new file mode 100644 index 0000000000..ef36f03002 --- /dev/null +++ b/ts/webui/src/components/managementExp/FilterBtns.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { DefaultButton, Icon, Dropdown, DatePicker, DayOfWeek } from '@fluentui/react'; +import { EXPERIMENTSTATUS, PLATFORM } from '../../static/const'; +import DayPickerStrings from './experimentConst'; +import { fillOptions } from './expFunction'; + +interface FilterBtnsProps { + selectedStatus: string; + selectedPlatform: string; + selectedStartDate: Date; + selectedEndDate: Date; + selectStatus: (_event: React.FormEvent<HTMLDivElement>, item: any) => void; + selectPlatform: (_event: React.FormEvent<HTMLDivElement>, item: any) => void; + getSelectedData: (type: string, date: Date | null | undefined) => void; + setSearchSource: () => void; +} + +class FilterBtns extends React.Component<FilterBtnsProps, {}> { + constructor(props: FilterBtnsProps) { + super(props); + } + + render(): React.ReactNode { + const { + selectedStatus, + selectedPlatform, + selectedStartDate, + selectedEndDate, + selectStatus, + selectPlatform, + getSelectedData, + setSearchSource + } = this.props; + + return ( + <React.Fragment> + <Dropdown + label='Status' + selectedKey={selectedStatus} + onChange={selectStatus.bind(this)} + placeholder='Select an option' + options={fillOptions(EXPERIMENTSTATUS)} + className='filter-condition-status' + /> + <Dropdown + label='Platform' + selectedKey={selectedPlatform} + onChange={selectPlatform.bind(this)} + placeholder='Select an option' + options={fillOptions(PLATFORM)} + className='filter-condition-platform' + /> + <DatePicker + label='Start time' + firstDayOfWeek={DayOfWeek.Sunday} + strings={DayPickerStrings} + showMonthPickerAsOverlay={true} + placeholder='Select a date...' + ariaLabel='Select a date' + value={selectedStartDate} + onSelectDate={getSelectedData.bind(this, 'start')} + /> + <DatePicker + label='End time' + firstDayOfWeek={DayOfWeek.Sunday} + strings={DayPickerStrings} + showMonthPickerAsOverlay={true} + placeholder='Select a date...' + ariaLabel='Select a date' + value={selectedEndDate} + onSelectDate={getSelectedData.bind(this, 'end')} + /> + <DefaultButton onClick={setSearchSource.bind(this)} className='reset'> + <Icon iconName='Refresh' /> + <span className='margin'>Reset</span> + </DefaultButton> + </React.Fragment> + ); + } +} + +export default FilterBtns; diff --git a/ts/webui/src/components/managementExp/Header.tsx b/ts/webui/src/components/managementExp/Header.tsx new file mode 100644 index 0000000000..d32fc9854c --- /dev/null +++ b/ts/webui/src/components/managementExp/Header.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Stack, StackItem, CommandBarButton } from '@fluentui/react'; +import { RevToggleKey } from '../buttons/Icon'; +import { NNILOGO } from '../stateless-component/NNItabs'; +import { stackTokens, stackStyle } from '../NavConst'; + +export const Hearder = (): any => ( + <div className='header'> + <div className='headerCon'> + <Stack className='nav' horizontal> + <StackItem grow={30} styles={{ root: { minWidth: 300, display: 'flex', verticalAlign: 'center' } }}> + <span className='desktop-logo'>{NNILOGO}</span> + <span className='logoTitle'>Neural Network Intelligence</span> + </StackItem> + <StackItem grow={70} className='navOptions'> + <Stack horizontal horizontalAlign='end' tokens={stackTokens} styles={stackStyle}> + <Link to='/oview' className='experiment'> + <CommandBarButton iconProps={RevToggleKey} text='Back to the experiment' /> + </Link> + </Stack> + </StackItem> + </Stack> + </div> + </div> +); diff --git a/ts/webui/src/components/managementExp/NameColumn.tsx b/ts/webui/src/components/managementExp/NameColumn.tsx new file mode 100644 index 0000000000..63ccb6fefb --- /dev/null +++ b/ts/webui/src/components/managementExp/NameColumn.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; + +interface NameColumnProps { + port: number; + expName: string; +} + +class NameColumn extends React.Component<NameColumnProps, {}> { + constructor(props: NameColumnProps) { + super(props); + } + + render(): React.ReactNode { + const { port, expName } = this.props; + const hostname = window.location.hostname; + const protocol = window.location.protocol; + const webuiPortal = `${protocol}//${hostname}:${port}/oview`; + return ( + <div className='succeed-padding ellipsis'> + <a href={webuiPortal} className='link' target='_blank' rel='noopener noreferrer'> + {expName} + </a> + </div> + ); + } +} + +export default NameColumn; diff --git a/ts/webui/src/components/managementExp/expFunction.ts b/ts/webui/src/components/managementExp/expFunction.ts new file mode 100644 index 0000000000..4d161cbeb2 --- /dev/null +++ b/ts/webui/src/components/managementExp/expFunction.ts @@ -0,0 +1,37 @@ +import { AllExperimentList, SortInfo } from '../../static/interface'; +import { copyAndSort } from '../../static/function'; + +function compareDate(date1: Date, date2: Date): boolean { + if (date1 !== undefined && date2 !== undefined) { + if (date1.getFullYear() === date2.getFullYear()) { + if (date1.getMonth() === date2.getMonth()) { + if (date1.getDate() === date2.getDate()) { + return true; + } + } + } + } + + return false; +} + +const filterByStatusOrPlatform = (val: string, type: string, data: AllExperimentList[]): AllExperimentList[] => { + return data.filter(temp => temp[type] === val); +}; + +function fillOptions(arr: string[]): any { + const list: Array<object> = []; + + arr.map(item => { + list.push({ key: item, text: item }); + }); + + return list; +} + +function getSortedSource(source: AllExperimentList[], sortInfo: SortInfo): AllExperimentList[] { + const keepSortedSource = copyAndSort(source, sortInfo.field, sortInfo.isDescend); + return keepSortedSource; +} + +export { compareDate, filterByStatusOrPlatform, fillOptions, getSortedSource }; diff --git a/ts/webui/src/components/managementExp/experiment.json b/ts/webui/src/components/managementExp/experiment.json deleted file mode 100644 index a79d0713c8..0000000000 --- a/ts/webui/src/components/managementExp/experiment.json +++ /dev/null @@ -1,82 +0,0 @@ -[ - { - "name": "EXample_minst_local_something", - "id": "wk9TNiGF", - "status": "RUNNING", - "port": 8080, - "platform": "local", - "startTime": 1604574871346, - "endTime": 1604651935683, - "tag": ["It is a long tag", "local", "first", "tf-1"] - }, - { - "name": "example_pai", - "id": "kjCpGcBJ", - "status": "SUCCEEDED", - "port": 9999, - "platform": "pai", - "startTime": 1603343773957, - "endTime": 1603343833157, - "tag": ["pai", "first", "tf-2"] - }, - { - "name": "paii", - "id": "LXRthUrH", - "status": "FAILED", - "port": 8080, - "platform": "pai", - "startTime": 1603343773957, - "endTime": 1603343833157, - "tag": ["pai", "good", "tf-2"] - }, - { - "name": "pai_example_2", - "id": "LXRthUrH", - "status": "FAILED", - "port": 3000, - "platform": "pai", - "startTime": 1603343773957, - "endTime": 1603343833157, - "tag": ["pai", "another", "tf-1"] - }, - { - "name": "example_minst33", - "id": "UR5G4", - "status": "FAILED", - "port": 8082, - "platform": "local", - "startTime": 1603344004004, - "endTime": 1605664489998, - "tag": ["local", "failed", "tf-1"] - }, - { - "name": "example_minst", - "id": "QjqqiVBp", - "status": "SUCCEEDED", - "port": 8084, - "platform": "local", - "startTime": 1603344179024, - "endTime": 1603344238656, - "tag": ["local", "first", "tf-1"] - }, - { - "name": "example_minst", - "id": "mFT1RE6A", - "status": "FAILED", - "port": 8086, - "platform": "local", - "startTime": 1603344249043, - "endTime": 1603344348570, - "tag": ["local", "first", "tf-1"] - }, - { - "name": "example_minst", - "id": "I4WoFxzq", - "status": "USER_CANCELED", - "port": 8088, - "platform": "local", - "startTime": 1603344359065, - "endTime": 1603344388521, - "tag": ["local", "first", "tf-1"] - } -] \ No newline at end of file diff --git a/ts/webui/src/components/managementExp/experimentConst.ts b/ts/webui/src/components/managementExp/experimentConst.ts index d9a272f158..c03ec2cabc 100644 --- a/ts/webui/src/components/managementExp/experimentConst.ts +++ b/ts/webui/src/components/managementExp/experimentConst.ts @@ -1,26 +1,26 @@ -import {IDatePickerStrings} from '@fluentui/react'; +import { IDatePickerStrings } from '@fluentui/react'; const DayPickerStrings: IDatePickerStrings = { months: [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' ], - + shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], - + shortDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], - + goToToday: 'Go to today', prevMonthAriaLabel: 'Go to previous month', nextMonthAriaLabel: 'Go to next month', @@ -28,7 +28,7 @@ const DayPickerStrings: IDatePickerStrings = { nextYearAriaLabel: 'Go to next year', closeButtonAriaLabel: 'Close date picker', monthPickerHeaderAriaLabel: '{0}, select to change the year', - yearPickerHeaderAriaLabel: '{0}, select to change the month', - }; + yearPickerHeaderAriaLabel: '{0}, select to change the month' +}; -export default DayPickerStrings; \ No newline at end of file +export default DayPickerStrings; diff --git a/ts/webui/src/components/overview/table/SuccessTable.tsx b/ts/webui/src/components/overview/table/SuccessTable.tsx index db568c6e5a..a762187413 100644 --- a/ts/webui/src/components/overview/table/SuccessTable.tsx +++ b/ts/webui/src/components/overview/table/SuccessTable.tsx @@ -106,7 +106,7 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> </React.Fragment> ); - private columns = [ + private columns: IColumn[] = [ { key: '_expand', name: '', diff --git a/ts/webui/src/static/const.ts b/ts/webui/src/static/const.ts index e1950743a1..ed85a01833 100644 --- a/ts/webui/src/static/const.ts +++ b/ts/webui/src/static/const.ts @@ -2,7 +2,7 @@ const METRIC_GROUP_UPDATE_THRESHOLD = 100; const METRIC_GROUP_UPDATE_SIZE = 20; -const MANAGER_IP = `http://13.77.78.63:8080/api/v1/nni`; +const MANAGER_IP = `/api/v1/nni`; const DOWNLOAD_IP = `/logs`; const WEBUIDOC = 'https://nni.readthedocs.io/en/latest/Tutorial/WebUI.html'; const trialJobStatus = [ @@ -15,7 +15,16 @@ const trialJobStatus = [ 'SYS_CANCELED', 'EARLY_STOPPED' ]; -const EXPERIMENTSTATUS = ['INITIALIZED','RUNNING','ERROR','STOPPING','STOPPED','DONE','NO_MORE_TRIAL','TUNER_NO_MORE_TRIAL']; +const EXPERIMENTSTATUS = [ + 'INITIALIZED', + 'RUNNING', + 'ERROR', + 'STOPPING', + 'STOPPED', + 'DONE', + 'NO_MORE_TRIAL', + 'TUNER_NO_MORE_TRIAL' +]; // 后三个正确吗 const PLATFORM = ['local', 'remote', 'pai', 'kubeflow', 'aml', 'dlts', 'framework']; const CONTROLTYPE = ['MAX_EXEC_DURATION', 'MAX_TRIAL_NUM', 'TRIAL_CONCURRENCY', 'SEARCH_SPACE']; diff --git a/ts/webui/src/static/function.ts b/ts/webui/src/static/function.ts index 701028e8f7..1589e5be34 100644 --- a/ts/webui/src/static/function.ts +++ b/ts/webui/src/static/function.ts @@ -263,7 +263,7 @@ function formatComplexTypeValue(value: any): string | number { } function isManagerExperimentPage(): boolean { - return (location.pathname.indexOf('experiment') === -1) ? false : true; + return location.pathname.indexOf('experiment') === -1 ? false : true; } function caclMonacoEditorHeight(height): number { diff --git a/ts/webui/src/static/interface.ts b/ts/webui/src/static/interface.ts index f7553adc47..c7fb3835be 100644 --- a/ts/webui/src/static/interface.ts +++ b/ts/webui/src/static/interface.ts @@ -218,13 +218,14 @@ interface SortInfo { isDescend?: boolean; } -interface AllExperimentList{ +interface AllExperimentList { + id: string; + experimentName: string; port: number; - startTime: number; - endTime: number; status: string; platform: string; - experimentName: string; + startTime: number; + endTime: number; tag: string[]; pid: number; webuiUrl: string[]; diff --git a/ts/webui/src/static/model/experimentsManager.ts b/ts/webui/src/static/model/experimentsManager.ts index 0aac8ddea7..3f1bea6de5 100644 --- a/ts/webui/src/static/model/experimentsManager.ts +++ b/ts/webui/src/static/model/experimentsManager.ts @@ -3,24 +3,24 @@ import { AllExperimentList } from '../interface'; import { requestAxios } from '../function'; class ExperimentsManager { - private experimentList: AllExperimentList[] = []; public getExperimentList(): AllExperimentList[] { - console.info('333'); // eslint-disable-line - console.info(this.experimentList); // eslint-disable-line return this.experimentList; } public async init(): Promise<void> { - await requestAxios(`${MANAGER_IP}/experiments-info`) - .then(data => { - this.experimentList = data; - }); - // .catch(error => { - // return [] as AllExperimentList[]; - // }); - + await requestAxios(`${MANAGER_IP}/experiments-info`).then(data => { + for (const item of data) { + if (typeof item.port === 'string') { + item.port = JSON.parse(item.port); + } + } + this.experimentList = data; + }) + .catch(error => { + return [] as AllExperimentList[]; + }); } } From a690713d6e8ff245c2c990073a56dbb69f496db9 Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Fri, 27 Nov 2020 01:50:52 +0000 Subject: [PATCH 07/15] fix lint --- .../src/static/model/experimentsManager.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/ts/webui/src/static/model/experimentsManager.ts b/ts/webui/src/static/model/experimentsManager.ts index 3f1bea6de5..d008e8c37f 100644 --- a/ts/webui/src/static/model/experimentsManager.ts +++ b/ts/webui/src/static/model/experimentsManager.ts @@ -10,17 +10,18 @@ class ExperimentsManager { } public async init(): Promise<void> { - await requestAxios(`${MANAGER_IP}/experiments-info`).then(data => { - for (const item of data) { - if (typeof item.port === 'string') { - item.port = JSON.parse(item.port); + await requestAxios(`${MANAGER_IP}/experiments-info`) + .then(data => { + for (const item of data) { + if (typeof item.port === 'string') { + item.port = JSON.parse(item.port); + } } - } - this.experimentList = data; - }) - .catch(error => { - return [] as AllExperimentList[]; - }); + this.experimentList = data; + }) + .catch(_error => { + return [] as AllExperimentList[]; + }); } } From 27538ed00cee0e7109c2fe86b39e435a3b3c81df Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Fri, 27 Nov 2020 03:14:01 +0000 Subject: [PATCH 08/15] fix test met issue and adjust column width --- ts/webui/src/App.scss | 3 ++ .../components/managementExp/Experiment.tsx | 42 ++++++++++++------- .../components/managementExp/FilterBtns.tsx | 2 +- .../managementExp/experimentConst.ts | 5 ++- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/ts/webui/src/App.scss b/ts/webui/src/App.scss index 8eae950096..0dd1b7fea3 100644 --- a/ts/webui/src/App.scss +++ b/ts/webui/src/App.scss @@ -18,6 +18,8 @@ .headerCon { width: 90%; + min-width: 1200px; + max-width: 1490px; margin: 0 auto; } @@ -31,6 +33,7 @@ min-height: calc(100vh - 56); margin: 0 auto; min-width: 1200px; + max-width: 1490px; /* nav bar: 56 + marginTop: 18 */ margin-top: 74px; diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/Experiment.tsx index ea7aede43f..aa4a58e411 100644 --- a/ts/webui/src/components/managementExp/Experiment.tsx +++ b/ts/webui/src/components/managementExp/Experiment.tsx @@ -4,6 +4,7 @@ import { ExperimentsManager } from '../../static/model/experimentsManager'; import { formatTimestamp, copyAndSort } from '../../static/function'; import { AllExperimentList, SortInfo } from '../../static/interface'; import { compareDate, filterByStatusOrPlatform, getSortedSource } from './expFunction'; +import { MAXSCREENCOLUMNWIDHT, MINSCREENCOLUMNWIDHT } from './experimentConst'; import { Hearder } from './Header'; import NameColumn from './NameColumn'; import FilterBtns from './FilterBtns'; @@ -139,8 +140,8 @@ class Experiment extends React.Component<{}, ExpListState> { name: 'Name', key: 'experimentName', fieldName: 'experimentName', // required! - minWidth: 100, - maxWidth: 130, + minWidth: MINSCREENCOLUMNWIDHT, + maxWidth: MAXSCREENCOLUMNWIDHT, isResizable: true, data: 'number', onColumnClick: this.onColumnClick, @@ -150,8 +151,8 @@ class Experiment extends React.Component<{}, ExpListState> { name: 'ID', key: 'id', fieldName: 'id', - minWidth: 100, - maxWidth: 130, + minWidth: MINSCREENCOLUMNWIDHT, + maxWidth: MAXSCREENCOLUMNWIDHT, isResizable: true, className: 'tableHead leftTitle', data: 'string', @@ -162,8 +163,8 @@ class Experiment extends React.Component<{}, ExpListState> { name: 'Status', key: 'status', fieldName: 'status', - minWidth: 100, - maxWidth: 150, + minWidth: MINSCREENCOLUMNWIDHT, + maxWidth: MAXSCREENCOLUMNWIDHT, isResizable: true, onColumnClick: this.onColumnClick, onRender: (item: any): React.ReactNode => ( @@ -174,8 +175,8 @@ class Experiment extends React.Component<{}, ExpListState> { name: 'Port', key: 'port', fieldName: 'port', - minWidth: 60, - maxWidth: 90, + minWidth: MINSCREENCOLUMNWIDHT - 15, + maxWidth: MAXSCREENCOLUMNWIDHT - 30, isResizable: true, data: 'number', onColumnClick: this.onColumnClick, @@ -189,8 +190,8 @@ class Experiment extends React.Component<{}, ExpListState> { name: 'Platform', key: 'platform', fieldName: 'platform', - minWidth: 110, - maxWidth: 130, + minWidth: MINSCREENCOLUMNWIDHT - 15, + maxWidth: MAXSCREENCOLUMNWIDHT - 30, isResizable: true, data: 'string', onColumnClick: this.onColumnClick, @@ -200,8 +201,8 @@ class Experiment extends React.Component<{}, ExpListState> { name: 'Start time', key: 'startTime', fieldName: 'startTime', - minWidth: 140, - maxWidth: 160, + minWidth: MINSCREENCOLUMNWIDHT + 15, + maxWidth: MAXSCREENCOLUMNWIDHT + 30, isResizable: true, data: 'number', onColumnClick: this.onColumnClick, @@ -215,8 +216,8 @@ class Experiment extends React.Component<{}, ExpListState> { name: 'End time', key: 'endTime', fieldName: 'endTime', - minWidth: 120, - maxWidth: 160, + minWidth: MINSCREENCOLUMNWIDHT + 15, + maxWidth: MAXSCREENCOLUMNWIDHT + 30, isResizable: true, data: 'number', onColumnClick: this.onColumnClick, @@ -266,6 +267,9 @@ class Experiment extends React.Component<{}, ExpListState> { searchSource: sortedResult })); } + this.setState(() => ({ + searchInputVal: newValue + })); } } @@ -389,9 +393,15 @@ class Experiment extends React.Component<{}, ExpListState> { // reset private setSearchSource(): void { - const { searchSource, sortInfo } = this.state; + const { sortInfo, searchInputVal, originExperimentList } = this.state; + // hert re-search data for fix this status: filter first -> searchBox search result null -> close filter + const result = originExperimentList.filter( + item => + item.experimentName.toLowerCase().includes(searchInputVal.toLowerCase()) || + item.id.toLowerCase().includes(searchInputVal.toLowerCase()) + ); this.setState(() => ({ - source: getSortedSource(searchSource, sortInfo), + source: getSortedSource(result, sortInfo), selectedStatus: '', selectedPlatform: '', selectedStartDate: undefined, diff --git a/ts/webui/src/components/managementExp/FilterBtns.tsx b/ts/webui/src/components/managementExp/FilterBtns.tsx index ef36f03002..9d05a76279 100644 --- a/ts/webui/src/components/managementExp/FilterBtns.tsx +++ b/ts/webui/src/components/managementExp/FilterBtns.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { DefaultButton, Icon, Dropdown, DatePicker, DayOfWeek } from '@fluentui/react'; import { EXPERIMENTSTATUS, PLATFORM } from '../../static/const'; -import DayPickerStrings from './experimentConst'; +import { DayPickerStrings } from './experimentConst'; import { fillOptions } from './expFunction'; interface FilterBtnsProps { diff --git a/ts/webui/src/components/managementExp/experimentConst.ts b/ts/webui/src/components/managementExp/experimentConst.ts index c03ec2cabc..5ed6244cea 100644 --- a/ts/webui/src/components/managementExp/experimentConst.ts +++ b/ts/webui/src/components/managementExp/experimentConst.ts @@ -31,4 +31,7 @@ const DayPickerStrings: IDatePickerStrings = { yearPickerHeaderAriaLabel: '{0}, select to change the month' }; -export default DayPickerStrings; +const MAXSCREENCOLUMNWIDHT = 180; +const MINSCREENCOLUMNWIDHT = 139; + +export { DayPickerStrings, MAXSCREENCOLUMNWIDHT, MINSCREENCOLUMNWIDHT }; From a3a903449a235cbdea670165ca3db32a58538614 Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Fri, 27 Nov 2020 04:26:52 +0000 Subject: [PATCH 09/15] add /experiments-info error status --- .../components/managementExp/Experiment.tsx | 22 ++++++++++++++++--- .../src/static/model/experimentsManager.ts | 9 ++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/Experiment.tsx index aa4a58e411..313b20acf7 100644 --- a/ts/webui/src/components/managementExp/Experiment.tsx +++ b/ts/webui/src/components/managementExp/Experiment.tsx @@ -3,6 +3,7 @@ import { Stack, DetailsList, DefaultButton, Icon, SearchBox, IColumn } from '@fl import { ExperimentsManager } from '../../static/model/experimentsManager'; import { formatTimestamp, copyAndSort } from '../../static/function'; import { AllExperimentList, SortInfo } from '../../static/interface'; +import MessageInfo from '../modals/MessageInfo'; import { compareDate, filterByStatusOrPlatform, getSortedSource } from './expFunction'; import { MAXSCREENCOLUMNWIDHT, MINSCREENCOLUMNWIDHT } from './experimentConst'; import { Hearder } from './Header'; @@ -16,6 +17,7 @@ import '../../static/style/tableStatus.css'; interface ExpListState { columns: IColumn[]; + errorMessage: string; hideFilter: boolean; searchInputVal: string; selectedStatus: string; @@ -33,6 +35,7 @@ class Experiment extends React.Component<{}, ExpListState> { super(props); this.state = { columns: this.columns, + errorMessage: '', hideFilter: true, searchInputVal: '', selectedStatus: '', @@ -51,15 +54,29 @@ class Experiment extends React.Component<{}, ExpListState> { this.setState(() => ({ source: result, originExperimentList: result, - searchSource: result + searchSource: result, + errorMessage: EXPERIMENTMANAGER.getExpErrorMessage() })); } render(): React.ReactNode { - const { hideFilter, selectedStatus, source, selectedPlatform, selectedStartDate, selectedEndDate } = this.state; + const { + hideFilter, + selectedStatus, + source, + selectedPlatform, + selectedStartDate, + selectedEndDate, + errorMessage + } = this.state; return ( <Stack className='nni' style={{ minHeight: window.innerHeight }}> <Hearder /> + {errorMessage !== undefined ? ( + <div className='warning'> + <MessageInfo info={errorMessage} typeInfo='error' /> + </div> + ) : null} <Stack className='contentBox expBackground'> <Stack className='content'> <Stack className='experimentList'> @@ -347,7 +364,6 @@ class Experiment extends React.Component<{}, ExpListState> { searchSource, sortInfo } = this.state; - // 只能set item.key const hasStatus = selectedStatus === '' ? false : true; const hasPlatform = selectedPlatform === '' ? false : true; const hasStartDate = selectedStartDate === undefined ? false : true; diff --git a/ts/webui/src/static/model/experimentsManager.ts b/ts/webui/src/static/model/experimentsManager.ts index d008e8c37f..8824ecb00e 100644 --- a/ts/webui/src/static/model/experimentsManager.ts +++ b/ts/webui/src/static/model/experimentsManager.ts @@ -4,11 +4,16 @@ import { requestAxios } from '../function'; class ExperimentsManager { private experimentList: AllExperimentList[] = []; + private errorMessage: string = ''; public getExperimentList(): AllExperimentList[] { return this.experimentList; } + public getExpErrorMessage(): string { + return this.errorMessage; + } + public async init(): Promise<void> { await requestAxios(`${MANAGER_IP}/experiments-info`) .then(data => { @@ -19,8 +24,8 @@ class ExperimentsManager { } this.experimentList = data; }) - .catch(_error => { - return [] as AllExperimentList[]; + .catch(error => { + this.errorMessage = error.message; }); } } From beebf9f3ff30ec824b11c10bcb9191ffb4c8816b Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Fri, 27 Nov 2020 10:05:15 +0000 Subject: [PATCH 10/15] no declare platform list, get this from api result filter --- ts/webui/src/components/managementExp/Experiment.tsx | 5 +++++ ts/webui/src/components/managementExp/FilterBtns.tsx | 6 ++++-- ts/webui/src/static/const.ts | 3 --- ts/webui/src/static/model/experimentsManager.ts | 8 ++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/Experiment.tsx index 313b20acf7..e48ad27792 100644 --- a/ts/webui/src/components/managementExp/Experiment.tsx +++ b/ts/webui/src/components/managementExp/Experiment.tsx @@ -17,6 +17,7 @@ import '../../static/style/tableStatus.css'; interface ExpListState { columns: IColumn[]; + platform: string[]; errorMessage: string; hideFilter: boolean; searchInputVal: string; @@ -34,6 +35,7 @@ class Experiment extends React.Component<{}, ExpListState> { constructor(props) { super(props); this.state = { + platform: [], columns: this.columns, errorMessage: '', hideFilter: true, @@ -55,12 +57,14 @@ class Experiment extends React.Component<{}, ExpListState> { source: result, originExperimentList: result, searchSource: result, + platform: EXPERIMENTMANAGER.getPlatformList(), errorMessage: EXPERIMENTMANAGER.getExpErrorMessage() })); } render(): React.ReactNode { const { + platform, hideFilter, selectedStatus, source, @@ -102,6 +106,7 @@ class Experiment extends React.Component<{}, ExpListState> { </Stack> <Stack className={`${hideFilter ? 'hidden' : ''} filter-condition`} horizontal gap={25}> <FilterBtns + platform={platform} selectedStatus={selectedStatus} selectedPlatform={selectedPlatform} // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/ts/webui/src/components/managementExp/FilterBtns.tsx b/ts/webui/src/components/managementExp/FilterBtns.tsx index 9d05a76279..c5cb365bf9 100644 --- a/ts/webui/src/components/managementExp/FilterBtns.tsx +++ b/ts/webui/src/components/managementExp/FilterBtns.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import { DefaultButton, Icon, Dropdown, DatePicker, DayOfWeek } from '@fluentui/react'; -import { EXPERIMENTSTATUS, PLATFORM } from '../../static/const'; +import { EXPERIMENTSTATUS } from '../../static/const'; import { DayPickerStrings } from './experimentConst'; import { fillOptions } from './expFunction'; interface FilterBtnsProps { + platform: string[]; selectedStatus: string; selectedPlatform: string; selectedStartDate: Date; @@ -22,6 +23,7 @@ class FilterBtns extends React.Component<FilterBtnsProps, {}> { render(): React.ReactNode { const { + platform, selectedStatus, selectedPlatform, selectedStartDate, @@ -47,7 +49,7 @@ class FilterBtns extends React.Component<FilterBtnsProps, {}> { selectedKey={selectedPlatform} onChange={selectPlatform.bind(this)} placeholder='Select an option' - options={fillOptions(PLATFORM)} + options={fillOptions(platform)} className='filter-condition-platform' /> <DatePicker diff --git a/ts/webui/src/static/const.ts b/ts/webui/src/static/const.ts index ed85a01833..553e151703 100644 --- a/ts/webui/src/static/const.ts +++ b/ts/webui/src/static/const.ts @@ -25,8 +25,6 @@ const EXPERIMENTSTATUS = [ 'NO_MORE_TRIAL', 'TUNER_NO_MORE_TRIAL' ]; -// 后三个正确吗 -const PLATFORM = ['local', 'remote', 'pai', 'kubeflow', 'aml', 'dlts', 'framework']; const CONTROLTYPE = ['MAX_EXEC_DURATION', 'MAX_TRIAL_NUM', 'TRIAL_CONCURRENCY', 'SEARCH_SPACE']; const MONACO = { readOnly: true, @@ -77,7 +75,6 @@ export { DOWNLOAD_IP, trialJobStatus, EXPERIMENTSTATUS, - PLATFORM, COLUMNPro, WEBUIDOC, CONTROLTYPE, diff --git a/ts/webui/src/static/model/experimentsManager.ts b/ts/webui/src/static/model/experimentsManager.ts index 8824ecb00e..9093c03a51 100644 --- a/ts/webui/src/static/model/experimentsManager.ts +++ b/ts/webui/src/static/model/experimentsManager.ts @@ -4,12 +4,17 @@ import { requestAxios } from '../function'; class ExperimentsManager { private experimentList: AllExperimentList[] = []; + private platform: string[] = []; private errorMessage: string = ''; public getExperimentList(): AllExperimentList[] { return this.experimentList; } + public getPlatformList(): string[] { + return this.platform; + } + public getExpErrorMessage(): string { return this.errorMessage; } @@ -17,12 +22,15 @@ class ExperimentsManager { public async init(): Promise<void> { await requestAxios(`${MANAGER_IP}/experiments-info`) .then(data => { + const platforms: Set<string> = new Set(); for (const item of data) { if (typeof item.port === 'string') { item.port = JSON.parse(item.port); + platforms.add(item.platform); } } this.experimentList = data; + this.platform = Array.from(platforms); }) .catch(error => { this.errorMessage = error.message; From ea2090dcc45da82822d6ed8e84b6b63aa93389d1 Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Tue, 8 Dec 2020 07:54:01 +0000 Subject: [PATCH 11/15] fix compare tooltip issue --- .../components/managementExp/Experiment.tsx | 2 +- ts/webui/src/components/modals/Compare.tsx | 6 +- .../trial-detail/DefaultMetricPoint.tsx | 38 +++--- .../static/style/experiment/experiment.scss | 114 ++++++++---------- ts/webui/yarn.lock | 36 +----- 5 files changed, 81 insertions(+), 115 deletions(-) diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/Experiment.tsx index 3d94654195..364f816efc 100644 --- a/ts/webui/src/components/managementExp/Experiment.tsx +++ b/ts/webui/src/components/managementExp/Experiment.tsx @@ -178,7 +178,7 @@ class Experiment extends React.Component<{}, ExpListState> { className: 'tableHead leftTitle', data: 'string', onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => <div className='succeed-padding id'>{item.id}</div> + onRender: (item: any): React.ReactNode => <div className='succeed-padding'>{item.id}</div> }, { name: 'Status', diff --git a/ts/webui/src/components/modals/Compare.tsx b/ts/webui/src/components/modals/Compare.tsx index 936c2714d3..8039f4ceae 100644 --- a/ts/webui/src/components/modals/Compare.tsx +++ b/ts/webui/src/components/modals/Compare.tsx @@ -58,14 +58,14 @@ class Compare extends React.Component<CompareProps, {}> { super(props); } - private _generateTooltipSummary(row: Item, metricKey: string): string { - return renderToString( + private _generateTooltipSummary = (row: Item, metricKey: string): string => + renderToString( <div className='tooldetailAccuracy'> + <div>Trial No.: {row.sequenceId}</div> <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 diff --git a/ts/webui/src/components/trial-detail/DefaultMetricPoint.tsx b/ts/webui/src/components/trial-detail/DefaultMetricPoint.tsx index 3427c0c28d..79800ca926 100644 --- a/ts/webui/src/components/trial-detail/DefaultMetricPoint.tsx +++ b/ts/webui/src/components/trial-detail/DefaultMetricPoint.tsx @@ -73,18 +73,24 @@ class DefaultPoint extends React.Component<DefaultPointProps, DefaultPointState> data.data[0] < maxSequenceId ? point[0] : point[0] - 300, 80 ], - formatter: (data: TooltipForAccuracy): React.ReactNode => - '<div class="tooldetailAccuracy">' + - '<div>Trial No.: ' + - data.data[0] + - '</div>' + - '<div>Default metric: ' + - data.data[1] + - '</div>' + - '<div>Parameters: <pre>' + - JSON.stringify(data.data[2], null, 4) + - '</pre></div>' + - '</div>' + formatter: (data: TooltipForAccuracy): React.ReactNode => { + return ( + '<div class="tooldetailAccuracy">' + + '<div>Trial No.: ' + + data.data[0] + + '</div>' + + '<div>Trial ID: ' + + data.data[2] + + '</div>' + + '<div>Default metric: ' + + data.data[1] + + '</div>' + + '<div>Parameters: <pre>' + + JSON.stringify(data.data[3], null, 4) + + '</pre></div>' + + '</div>' + ); + } }, dataZoom: [ { @@ -110,7 +116,7 @@ class DefaultPoint extends React.Component<DefaultPointProps, DefaultPointState> } generateScatterSeries(trials: Trial[]): any { - const data = trials.map(trial => [trial.sequenceId, trial.accuracy, trial.description.parameters]); + const data = trials.map(trial => [trial.sequenceId, trial.accuracy, trial.id, trial.description.parameters]); return { symbolSize: 6, type: 'scatter', @@ -120,7 +126,7 @@ class DefaultPoint extends React.Component<DefaultPointProps, DefaultPointState> generateBestCurveSeries(trials: Trial[]): any { let best = trials[0]; - const data = [[best.sequenceId, best.accuracy, best.description.parameters]]; + const data = [[best.sequenceId, best.accuracy, best.id, best.description.parameters]]; for (let i = 1; i < trials.length; i++) { const trial = trials[i]; @@ -128,10 +134,10 @@ class DefaultPoint extends React.Component<DefaultPointProps, DefaultPointState> const delta = trial.accuracy! - best.accuracy!; const better = EXPERIMENT.optimizeMode === 'minimize' ? delta < 0 : delta > 0; if (better) { - data.push([trial.sequenceId, trial.accuracy, trial.description.parameters]); + data.push([trial.sequenceId, trial.accuracy, best.id, trial.description.parameters]); best = trial; } else { - data.push([trial.sequenceId, best.accuracy, trial.description.parameters]); + data.push([trial.sequenceId, best.accuracy, best.id, trial.description.parameters]); } } diff --git a/ts/webui/src/static/style/experiment/experiment.scss b/ts/webui/src/static/style/experiment/experiment.scss index 529d92d0bf..384a0c1055 100644 --- a/ts/webui/src/static/style/experiment/experiment.scss +++ b/ts/webui/src/static/style/experiment/experiment.scss @@ -1,72 +1,64 @@ -.expBackground{ - background: #f2f2f2; - height: 100%; - .content{ - background: #fff; - } +.expBackground { + background: #f2f2f2; + height: 100%; + .content { + background: #fff; + } } -.experimentList{ - padding: 42px; +.experimentList { + padding: 42px; - .box{ - .search{ - width: 90%; - &-input{ - width: 330px; - } - } + .box { + .search { + width: 90%; + &-input { + width: 330px; + } + } - .filter{ - width: 10%; - text-align: right; + .filter { + width: 10%; + text-align: right; - &-button-open{ - background: #F3F2F1; - } - } - } + &-button-open { + background: #f3f2f1; + } + } + } - .filter-condition{ - margin-top: 26px; - .reset{ - width: 80px; - position: relative; - top: 29px; - } + .filter-condition { + margin-top: 26px; + .reset { + width: 80px; + position: relative; + top: 29px; + } - &-status{ - width: 194px; - } - &-platform{ - width: 150px; - } - } + &-status { + width: 194px; + } + &-platform { + width: 150px; + } + } - .hidden{ - display: none; - } + .hidden { + display: none; + } - .margin{ - margin-left: 10px; - } + .margin { + margin-left: 10px; + } - .table{ - .id{ - font-variant: small-caps; - font-size: 15px; - } - } + .tagContainer { + width: 100%; - .tagContainer{ - - width: 100%; - - .tag{ - font-weight: 500; - background: #f2f2f2; - margin: 0 4px; - padding: 0 6px; - } - } -} \ No newline at end of file + .tag { + font-weight: 500; + background: #f2f2f2; + margin: 0 4px; + padding: 0 6px; + } + } +} diff --git a/ts/webui/yarn.lock b/ts/webui/yarn.lock index 343eaa5ac2..ac517c8be4 100644 --- a/ts/webui/yarn.lock +++ b/ts/webui/yarn.lock @@ -4598,7 +4598,7 @@ debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: dependencies: ms "2.1.2" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -6785,7 +6785,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -8347,11 +8347,6 @@ lockfile@^1.0.4: dependencies: signal-exit "^3.0.2" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -8360,33 +8355,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@*, lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= - lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -8417,11 +8390,6 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" From ee9ffa2f9770705ec2d36905d0ccd9e77766b268 Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Tue, 8 Dec 2020 08:29:33 +0000 Subject: [PATCH 12/15] fix tooltip location --- ts/webui/src/components/modals/Compare.tsx | 2 +- .../src/components/trial-detail/Intermediate.tsx | 12 +++++++++--- ts/webui/src/static/interface.ts | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ts/webui/src/components/modals/Compare.tsx b/ts/webui/src/components/modals/Compare.tsx index 8039f4ceae..9609671945 100644 --- a/ts/webui/src/components/modals/Compare.tsx +++ b/ts/webui/src/components/modals/Compare.tsx @@ -84,7 +84,7 @@ class Compare extends React.Component<CompareProps, {}> { trigger: 'item', enterable: true, position: (point: number[], data: TooltipForIntermediate): [number, number] => { - if (data.dataIndex < length / 2) { + if (data.dataIndex < xAxisMax / 2) { return [point[0], 80]; } else { return [point[0] - 300, 80]; diff --git a/ts/webui/src/components/trial-detail/Intermediate.tsx b/ts/webui/src/components/trial-detail/Intermediate.tsx index 72dcc94cee..ee7ea89190 100644 --- a/ts/webui/src/components/trial-detail/Intermediate.tsx +++ b/ts/webui/src/components/trial-detail/Intermediate.tsx @@ -61,6 +61,7 @@ class Intermediate extends React.Component<IntermediateProps, IntermediateState> const temp = source[item]; trialIntermediate.push({ name: temp.id, + trialNum: temp.sequenceId, data: temp.description.intermediate, type: 'line', hyperPara: temp.description.parameters @@ -94,13 +95,18 @@ class Intermediate extends React.Component<IntermediateProps, IntermediateState> }, formatter: function(data: TooltipForIntermediate): React.ReactNode { const trialId = data.seriesName; - let obj = {}; + let parameters = {}; + let trialNum = 0; const temp = trialIntermediate.find(key => key.name === trialId); if (temp !== undefined) { - obj = temp.hyperPara; + parameters = temp.hyperPara; + trialNum = temp.trialNum; } return ( '<div class="tooldetailAccuracy">' + + '<div>Trial No.: ' + + trialNum + + '</div>' + '<div>Trial ID: ' + trialId + '</div>' + @@ -109,7 +115,7 @@ class Intermediate extends React.Component<IntermediateProps, IntermediateState> '</div>' + '<div>Parameters: ' + '<pre>' + - JSON.stringify(obj, null, 4) + + JSON.stringify(parameters, null, 4) + '</pre>' + '</div>' + '</div>' diff --git a/ts/webui/src/static/interface.ts b/ts/webui/src/static/interface.ts index c7fb3835be..99e89237ae 100644 --- a/ts/webui/src/static/interface.ts +++ b/ts/webui/src/static/interface.ts @@ -120,6 +120,7 @@ interface Intermedia { type: string; data: Array<number | object>; // intermediate data hyperPara: object; // each trial hyperpara value + trialNum: number; } interface MetricDataRecord { From 10edd490401705361615e7771cf62fe4eeea906a Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Fri, 11 Dec 2020 10:25:36 +0000 Subject: [PATCH 13/15] fix some comments --- ts/webui/src/App.tsx | 10 ++++----- .../{Experiment.tsx => ExperimentManager.tsx} | 6 +++-- .../components/managementExp/NameColumn.tsx | 13 +++++++---- ts/webui/src/components/modals/Compare.tsx | 22 ++++++++++--------- ts/webui/src/index.tsx | 2 +- .../src/static/model/experimentsManager.ts | 8 ++++--- .../src/static/style/{common => }/common.scss | 0 ts/webui/src/static/style/overview/count.scss | 4 +++- 8 files changed, 39 insertions(+), 26 deletions(-) rename ts/webui/src/components/managementExp/{Experiment.tsx => ExperimentManager.tsx} (98%) rename ts/webui/src/static/style/{common => }/common.scss (100%) diff --git a/ts/webui/src/App.tsx b/ts/webui/src/App.tsx index 3f01ae9af9..10bc23925b 100644 --- a/ts/webui/src/App.tsx +++ b/ts/webui/src/App.tsx @@ -7,7 +7,7 @@ import NavCon from './components/NavCon'; import MessageInfo from './components/modals/MessageInfo'; import { SlideNavBtns } from './components/slideNav/SlideNavBtns'; import './App.scss'; -import './static/style/common/common.scss'; +import './static/style/common.scss'; interface AppState { interval: number; @@ -31,13 +31,13 @@ export const AppContext = React.createContext({ bestTrialEntries: '10', maxDurationUnit: 'm', // eslint-disable-next-line @typescript-eslint/no-empty-function - changeColumn: (_val: string[]) => {}, + changeColumn: (_val: string[]): void => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function - changeMetricGraphMode: (_val: 'max' | 'min') => {}, + changeMetricGraphMode: (_val: 'max' | 'min'): void => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function - changeMaxDurationUnit: (_val: string) => {}, + changeMaxDurationUnit: (_val: string): void => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function - changeEntries: (_val: string) => {}, + changeEntries: (_val: string): void => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function updateOverviewPage: () => {} }); diff --git a/ts/webui/src/components/managementExp/Experiment.tsx b/ts/webui/src/components/managementExp/ExperimentManager.tsx similarity index 98% rename from ts/webui/src/components/managementExp/Experiment.tsx rename to ts/webui/src/components/managementExp/ExperimentManager.tsx index 364f816efc..fc63369a31 100644 --- a/ts/webui/src/components/managementExp/Experiment.tsx +++ b/ts/webui/src/components/managementExp/ExperimentManager.tsx @@ -166,7 +166,9 @@ class Experiment extends React.Component<{}, ExpListState> { isResizable: true, data: 'number', onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => <NameColumn port={item.port} expName={item.experimentName} /> + onRender: (item: any): React.ReactNode => ( + <NameColumn port={item.port} status={item.status} expName={item.experimentName} /> + ) }, { name: 'ID', @@ -203,7 +205,7 @@ class Experiment extends React.Component<{}, ExpListState> { onColumnClick: this.onColumnClick, onRender: (item: any): React.ReactNode => ( <div className='succeed-padding'> - <div>{item.port}</div> + <div>{item.port !== undefined ? item.port : '--'}</div> </div> ) }, diff --git a/ts/webui/src/components/managementExp/NameColumn.tsx b/ts/webui/src/components/managementExp/NameColumn.tsx index 63ccb6fefb..d2e3ad1e17 100644 --- a/ts/webui/src/components/managementExp/NameColumn.tsx +++ b/ts/webui/src/components/managementExp/NameColumn.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; interface NameColumnProps { port: number; expName: string; + status: string; } class NameColumn extends React.Component<NameColumnProps, {}> { @@ -11,15 +12,19 @@ class NameColumn extends React.Component<NameColumnProps, {}> { } render(): React.ReactNode { - const { port, expName } = this.props; + const { port, expName, status } = this.props; const hostname = window.location.hostname; const protocol = window.location.protocol; const webuiPortal = `${protocol}//${hostname}:${port}/oview`; return ( <div className='succeed-padding ellipsis'> - <a href={webuiPortal} className='link' target='_blank' rel='noopener noreferrer'> - {expName} - </a> + {status === 'STOPPED' ? ( + <div>{expName}</div> + ) : ( + <a href={webuiPortal} className='link' target='_blank' rel='noopener noreferrer'> + {expName} + </a> + )} </div> ); } diff --git a/ts/webui/src/components/modals/Compare.tsx b/ts/webui/src/components/modals/Compare.tsx index 9609671945..fc20bd95da 100644 --- a/ts/webui/src/components/modals/Compare.tsx +++ b/ts/webui/src/components/modals/Compare.tsx @@ -58,16 +58,16 @@ class Compare extends React.Component<CompareProps, {}> { super(props); } - private _generateTooltipSummary = (row: Item, metricKey: string): string => + private _generateTooltipSummary = (row: Item, value: string): string => renderToString( <div className='tooldetailAccuracy'> <div>Trial No.: {row.sequenceId}</div> <div>Trial ID: {row.id}</div> - <div>Default metric: {row.metrics.get(metricKey) || 'N/A'}</div> + <div>Intermediate metric: {value}</div> </div> ); - private _intermediates(items: Item[], metricKey: string): React.ReactNode { + private _intermediates(items: Item[]): React.ReactNode { // Precondition: make sure `items` is not empty const xAxisMax = Math.max(...items.map(item => item.intermediates.length)); const xAxis = Array(xAxisMax) @@ -92,7 +92,7 @@ class Compare extends React.Component<CompareProps, {}> { }, formatter: (data: TooltipForIntermediate): string => { const item = items.find(k => k.id === data.seriesName) as Item; - return this._generateTooltipSummary(item, metricKey); + return this._generateTooltipSummary(item, data.data); } }, grid: { @@ -187,9 +187,13 @@ class Compare extends React.Component<CompareProps, {}> { {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)) - )} + {metricKeys !== undefined + ? metricKeys.map(k => + this._renderRow(`metrics_${k}`, `Metric: ${k}`, 'value', items, item => + item.metrics.get(k) + ) + ) + : null} </tbody> </table> ); @@ -209,8 +213,6 @@ class Compare extends React.Component<CompareProps, {}> { 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 @@ -232,7 +234,7 @@ class Compare extends React.Component<CompareProps, {}> { /> </div> <Stack className='compare-modal-intermediate'> - {this._intermediates(items, defaultMetricKey)} + {this._intermediates(items)} <Stack className='compare-yAxis'># Intermediate result</Stack> </Stack> {showDetails && <Stack>{this._columns(items)}</Stack>} diff --git a/ts/webui/src/index.tsx b/ts/webui/src/index.tsx index 986e95b382..1cd2170de6 100644 --- a/ts/webui/src/index.tsx +++ b/ts/webui/src/index.tsx @@ -4,7 +4,7 @@ import App from './App'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; const Overview = lazy(() => import('./components/Overview')); const TrialsDetail = lazy(() => import('./components/TrialsDetail')); -const Experiment = lazy(() => import('./components/managementExp/Experiment')); +const Experiment = lazy(() => import('./components/managementExp/ExperimentManager')); import './index.css'; import './static/style/loading.scss'; import * as serviceWorker from './serviceWorker'; diff --git a/ts/webui/src/static/model/experimentsManager.ts b/ts/webui/src/static/model/experimentsManager.ts index 9093c03a51..e95bc9f2cb 100644 --- a/ts/webui/src/static/model/experimentsManager.ts +++ b/ts/webui/src/static/model/experimentsManager.ts @@ -24,10 +24,12 @@ class ExperimentsManager { .then(data => { const platforms: Set<string> = new Set(); for (const item of data) { - if (typeof item.port === 'string') { - item.port = JSON.parse(item.port); - platforms.add(item.platform); + if (item.port !== undefined) { + if (typeof item.port === 'string') { + item.port = JSON.parse(item.port); + } } + platforms.add(item.platform); } this.experimentList = data; this.platform = Array.from(platforms); diff --git a/ts/webui/src/static/style/common/common.scss b/ts/webui/src/static/style/common.scss similarity index 100% rename from ts/webui/src/static/style/common/common.scss rename to ts/webui/src/static/style/common.scss diff --git a/ts/webui/src/static/style/overview/count.scss b/ts/webui/src/static/style/overview/count.scss index b01205ce7a..5189ff0fbc 100644 --- a/ts/webui/src/static/style/overview/count.scss +++ b/ts/webui/src/static/style/overview/count.scss @@ -58,6 +58,7 @@ $margin: 24px; .concurrency { .editparam { margin-top: 5px; + position: relative; } } .editparam { @@ -124,5 +125,6 @@ $margin: 24px; .info { position: absolute; z-index: 999; - left: 0; + left: -50%; + width: 270%; } From 355816a36cb502de2f6b702014217c2dcf9f69d4 Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Fri, 11 Dec 2020 10:35:24 +0000 Subject: [PATCH 14/15] delete datestring --- .../components/managementExp/FilterBtns.tsx | 3 -- .../managementExp/experimentConst.ts | 35 +------------------ 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/ts/webui/src/components/managementExp/FilterBtns.tsx b/ts/webui/src/components/managementExp/FilterBtns.tsx index c5cb365bf9..8c0563329e 100644 --- a/ts/webui/src/components/managementExp/FilterBtns.tsx +++ b/ts/webui/src/components/managementExp/FilterBtns.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { DefaultButton, Icon, Dropdown, DatePicker, DayOfWeek } from '@fluentui/react'; import { EXPERIMENTSTATUS } from '../../static/const'; -import { DayPickerStrings } from './experimentConst'; import { fillOptions } from './expFunction'; interface FilterBtnsProps { @@ -55,7 +54,6 @@ class FilterBtns extends React.Component<FilterBtnsProps, {}> { <DatePicker label='Start time' firstDayOfWeek={DayOfWeek.Sunday} - strings={DayPickerStrings} showMonthPickerAsOverlay={true} placeholder='Select a date...' ariaLabel='Select a date' @@ -65,7 +63,6 @@ class FilterBtns extends React.Component<FilterBtnsProps, {}> { <DatePicker label='End time' firstDayOfWeek={DayOfWeek.Sunday} - strings={DayPickerStrings} showMonthPickerAsOverlay={true} placeholder='Select a date...' ariaLabel='Select a date' diff --git a/ts/webui/src/components/managementExp/experimentConst.ts b/ts/webui/src/components/managementExp/experimentConst.ts index 5ed6244cea..d71cab4399 100644 --- a/ts/webui/src/components/managementExp/experimentConst.ts +++ b/ts/webui/src/components/managementExp/experimentConst.ts @@ -1,37 +1,4 @@ -import { IDatePickerStrings } from '@fluentui/react'; -const DayPickerStrings: IDatePickerStrings = { - months: [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' - ], - - shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - - days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], - - shortDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], - - goToToday: 'Go to today', - prevMonthAriaLabel: 'Go to previous month', - nextMonthAriaLabel: 'Go to next month', - prevYearAriaLabel: 'Go to previous year', - nextYearAriaLabel: 'Go to next year', - closeButtonAriaLabel: 'Close date picker', - monthPickerHeaderAriaLabel: '{0}, select to change the year', - yearPickerHeaderAriaLabel: '{0}, select to change the month' -}; - const MAXSCREENCOLUMNWIDHT = 180; const MINSCREENCOLUMNWIDHT = 139; -export { DayPickerStrings, MAXSCREENCOLUMNWIDHT, MINSCREENCOLUMNWIDHT }; +export { MAXSCREENCOLUMNWIDHT, MINSCREENCOLUMNWIDHT }; From 0038c753c7e2d9d4a1a6994c3defc46d8054673f Mon Sep 17 00:00:00 2001 From: Lijiao <Lijiaoa@outlook.com> Date: Fri, 11 Dec 2020 12:03:03 +0000 Subject: [PATCH 15/15] fix clickable style and clickable column ID rather than name --- .../managementExp/ExperimentManager.tsx | 9 ++++----- .../{NameColumn.tsx => TrialIdColumn.tsx} | 16 ++++++++-------- ts/webui/src/static/style/common.scss | 7 ++++++- 3 files changed, 18 insertions(+), 14 deletions(-) rename ts/webui/src/components/managementExp/{NameColumn.tsx => TrialIdColumn.tsx} (66%) diff --git a/ts/webui/src/components/managementExp/ExperimentManager.tsx b/ts/webui/src/components/managementExp/ExperimentManager.tsx index fc63369a31..aebda22a61 100644 --- a/ts/webui/src/components/managementExp/ExperimentManager.tsx +++ b/ts/webui/src/components/managementExp/ExperimentManager.tsx @@ -7,11 +7,12 @@ import MessageInfo from '../modals/MessageInfo'; import { compareDate, filterByStatusOrPlatform, getSortedSource } from './expFunction'; import { MAXSCREENCOLUMNWIDHT, MINSCREENCOLUMNWIDHT } from './experimentConst'; import { Hearder } from './Header'; -import NameColumn from './NameColumn'; +import NameColumn from './TrialIdColumn'; import FilterBtns from './FilterBtns'; import '../../App.scss'; import '../../static/style/nav/nav.scss'; import '../../static/style/experiment/experiment.scss'; +import '../../static/style/overview/probar.scss'; import '../../static/style/tableStatus.css'; interface ExpListState { @@ -166,9 +167,7 @@ class Experiment extends React.Component<{}, ExpListState> { isResizable: true, data: 'number', onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => ( - <NameColumn port={item.port} status={item.status} expName={item.experimentName} /> - ) + onRender: (item: any): React.ReactNode => <div className='succeed-padding'>{item.experimentName}</div> }, { name: 'ID', @@ -180,7 +179,7 @@ class Experiment extends React.Component<{}, ExpListState> { className: 'tableHead leftTitle', data: 'string', onColumnClick: this.onColumnClick, - onRender: (item: any): React.ReactNode => <div className='succeed-padding'>{item.id}</div> + onRender: (item: any): React.ReactNode => <NameColumn port={item.port} status={item.status} id={item.id} /> }, { name: 'Status', diff --git a/ts/webui/src/components/managementExp/NameColumn.tsx b/ts/webui/src/components/managementExp/TrialIdColumn.tsx similarity index 66% rename from ts/webui/src/components/managementExp/NameColumn.tsx rename to ts/webui/src/components/managementExp/TrialIdColumn.tsx index d2e3ad1e17..159b95f482 100644 --- a/ts/webui/src/components/managementExp/NameColumn.tsx +++ b/ts/webui/src/components/managementExp/TrialIdColumn.tsx @@ -1,28 +1,28 @@ import * as React from 'react'; -interface NameColumnProps { +interface TrialIdColumnProps { port: number; - expName: string; + id: string; status: string; } -class NameColumn extends React.Component<NameColumnProps, {}> { - constructor(props: NameColumnProps) { +class TrialIdColumn extends React.Component<TrialIdColumnProps, {}> { + constructor(props: TrialIdColumnProps) { super(props); } render(): React.ReactNode { - const { port, expName, status } = this.props; + const { port, id, status } = this.props; const hostname = window.location.hostname; const protocol = window.location.protocol; const webuiPortal = `${protocol}//${hostname}:${port}/oview`; return ( <div className='succeed-padding ellipsis'> {status === 'STOPPED' ? ( - <div>{expName}</div> + <div>{id}</div> ) : ( <a href={webuiPortal} className='link' target='_blank' rel='noopener noreferrer'> - {expName} + {id} </a> )} </div> @@ -30,4 +30,4 @@ class NameColumn extends React.Component<NameColumnProps, {}> { } } -export default NameColumn; +export default TrialIdColumn; diff --git a/ts/webui/src/static/style/common.scss b/ts/webui/src/static/style/common.scss index 311bc5a511..90218499bb 100644 --- a/ts/webui/src/static/style/common.scss +++ b/ts/webui/src/static/style/common.scss @@ -6,8 +6,13 @@ .link { outline: none; - color: #0071bc; + color: #333 !important; + text-decoration: none; + &:hover { + color: #0071bc !important; + text-decoration: underline; + } &:active, &:visited { color: #0071bc;