Unverified Commit 1cd7ad5f authored by Lijiaoa's avatar Lijiaoa Committed by GitHub
Browse files

[webui] refactor overview page (#2924)

parent 0a6c234a
import React from 'react';
import { TooltipHost, Stack } from '@fluentui/react';
import { EXPERIMENT } from '../../../static/datamodel';
import '../../../static/style/overview/command.scss';
export const Command = (): any => {
const clusterMetaData = EXPERIMENT.profile.params.clusterMetaData;
const tuner = EXPERIMENT.profile.params.tuner;
const advisor = EXPERIMENT.profile.params.advisor;
const assessor = EXPERIMENT.profile.params.assessor;
let title = '';
let builtinName = '';
let trialCommand = 'unknown';
if (tuner !== undefined) {
title = title.concat('Tuner');
if (tuner.builtinTunerName !== undefined) {
builtinName = builtinName.concat(tuner.builtinTunerName);
}
}
if (advisor !== undefined) {
title = title.concat('/ Assessor');
if (advisor.builtinAdvisorName !== undefined) {
builtinName = builtinName.concat(advisor.builtinAdvisorName);
}
}
if (assessor !== undefined) {
title = title.concat('/ Addvisor');
if (assessor.builtinAssessorName !== undefined) {
builtinName = builtinName.concat(assessor.builtinAssessorName);
}
}
if (clusterMetaData !== undefined) {
for (const item of clusterMetaData) {
if (item.key === 'command') {
trialCommand = item.value;
}
}
}
return (
<div className='command basic'>
<div className='command1'>
<p>Training platform</p>
<div className='nowrap'>{EXPERIMENT.profile.params.trainingServicePlatform}</div>
<p className='lineMargin'>{title}</p>
<div className='nowrap'>{builtinName}</div>
</div>
<Stack className='command2'>
<p>Log directory</p>
<div className='nowrap'>
<TooltipHost content={EXPERIMENT.profile.logDir || 'unknown'} className='nowrap'>
{EXPERIMENT.profile.logDir || 'unknown'}
</TooltipHost>
</div>
<p className='lineMargin'>Trial command</p>
<div className='nowrap'>
<TooltipHost content={trialCommand || 'unknown'} className='nowrap'>
{trialCommand || 'unknown'}
</TooltipHost>
</div>
</Stack>
</div>
);
};
import { FontWeights, mergeStyleSets, getTheme } from '@fluentui/react';
const theme = getTheme();
export const styles = mergeStyleSets({
buttonArea: {
verticalAlign: 'top',
display: 'inline-block',
textAlign: 'center',
// margin: '0 100px',
minWidth: 30,
height: 30
},
callout: {
maxWidth: 300
},
header: {
padding: '18px 24px 12px'
},
title: [
theme.fonts.xLarge,
{
margin: 0,
color: theme.palette.neutralPrimary,
fontWeight: FontWeights.semilight
}
],
inner: {
height: '100%',
padding: '0 24px 20px'
},
actions: {
position: 'relative',
marginTop: 20,
width: '100%',
whiteSpace: 'nowrap'
},
subtext: [
theme.fonts.small,
{
margin: 0,
color: theme.palette.neutralPrimary,
fontWeight: FontWeights.semilight
}
],
link: [
theme.fonts.medium,
{
color: theme.palette.neutralPrimary
}
]
});
const itemStyle1: React.CSSProperties = {
width: '75%'
};
const itemStyleSucceed: React.CSSProperties = {
width: '28%'
};
const itemStyle2: React.CSSProperties = {
height: 38
};
// top trials entries
const entriesOption = [
{ key: '10', text: '10' },
{ key: '20', text: '20' },
{ key: '30', text: '30' },
{ key: '50', text: '40' },
{ key: '100', text: '100' }
];
export { itemStyle1, itemStyleSucceed, itemStyle2, entriesOption };
import * as React from 'react'; import * as React from 'react';
import { DetailsRow, IDetailsRowBaseProps } from '@fluentui/react'; import { DetailsRow, IDetailsRowBaseProps } from '@fluentui/react';
import OpenRow from '../public-child/OpenRow'; import OpenRow from '../../public-child/OpenRow';
interface DetailsProps { interface DetailsProps {
detailsProps: IDetailsRowBaseProps; detailsProps: IDetailsRowBaseProps;
......
import * as React from 'react'; import * as React from 'react';
import { DetailsList, IDetailsListProps, IColumn } from '@fluentui/react'; import { DetailsList, IDetailsListProps, IColumn } from '@fluentui/react';
import DefaultMetric from '../public-child/DefaultMetric'; import DefaultMetric from '../../public-child/DefaultMetric';
import Details from './Details'; import Details from './Details';
import { convertDuration } from '../../static/function'; import { convertDuration } from '../../../static/function';
import { TRIALS } from '../../static/datamodel'; import { TRIALS } from '../../../static/datamodel';
import { DETAILTABS } from '../stateless-component/NNItabs'; import { DETAILTABS } from '../../stateless-component/NNItabs';
import '../../static/style/succTable.scss'; import '../../../static/style/succTable.scss';
import '../../static/style/openRow.scss'; import '../../../static/style/tableStatus.css';
import '../../../static/style/openRow.scss';
interface SuccessTableProps { interface SuccessTableProps {
trialIds: string[]; trialIds: string[];
...@@ -15,12 +16,17 @@ interface SuccessTableProps { ...@@ -15,12 +16,17 @@ interface SuccessTableProps {
interface SuccessTableState { interface SuccessTableState {
columns: IColumn[]; columns: IColumn[];
source: Array<any>; source: Array<any>;
innerWidth: number;
} }
class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> { class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> {
constructor(props: SuccessTableProps) { constructor(props: SuccessTableProps) {
super(props); super(props);
this.state = { columns: this.columns, source: TRIALS.table(this.props.trialIds) }; this.state = {
columns: this.columns,
source: TRIALS.table(this.props.trialIds),
innerWidth: window.innerWidth
};
} }
private onRenderRow: IDetailsListProps['onRenderRow'] = props => { private onRenderRow: IDetailsListProps['onRenderRow'] = props => {
...@@ -70,8 +76,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> ...@@ -70,8 +76,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
name: 'Trial No.', name: 'Trial No.',
key: 'sequenceId', key: 'sequenceId',
fieldName: 'sequenceId', // required! fieldName: 'sequenceId', // required!
minWidth: 60, minWidth: (window.innerWidth * 0.333 - 150) / 5,
maxWidth: 120, maxWidth: (window.innerWidth * 0.333 - 150) / 5,
isResizable: true, isResizable: true,
data: 'number', data: 'number',
onColumnClick: this.onColumnClick onColumnClick: this.onColumnClick
...@@ -80,8 +86,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> ...@@ -80,8 +86,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
name: 'ID', name: 'ID',
key: 'id', key: 'id',
fieldName: 'id', fieldName: 'id',
minWidth: 80, minWidth: (window.innerWidth * 0.333 - 150) / 5,
maxWidth: 100, maxWidth: (window.innerWidth * 0.333 - 150) / 5,
isResizable: true, isResizable: true,
className: 'tableHead leftTitle', className: 'tableHead leftTitle',
data: 'string', data: 'string',
...@@ -90,8 +96,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> ...@@ -90,8 +96,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
{ {
name: 'Duration', name: 'Duration',
key: 'duration', key: 'duration',
minWidth: 100, minWidth: (window.innerWidth * 0.333 - 150) / 5,
maxWidth: 210, maxWidth: (window.innerWidth * 0.333 - 150) / 5,
isResizable: true, isResizable: true,
fieldName: 'duration', fieldName: 'duration',
data: 'number', data: 'number',
...@@ -105,8 +111,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> ...@@ -105,8 +111,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
{ {
name: 'Status', name: 'Status',
key: 'status', key: 'status',
minWidth: 140, minWidth: (window.innerWidth * 0.333 - 150) / 5,
maxWidth: 210, maxWidth: (window.innerWidth * 0.333 - 150) / 5,
isResizable: true, isResizable: true,
fieldName: 'status', fieldName: 'status',
onRender: (item: any): React.ReactNode => { onRender: (item: any): React.ReactNode => {
...@@ -117,8 +123,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> ...@@ -117,8 +123,8 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
name: 'Default metric', name: 'Default metric',
key: 'accuracy', key: 'accuracy',
fieldName: 'accuracy', fieldName: 'accuracy',
minWidth: 120, minWidth: (window.innerWidth * 0.333 - 200) / 5,
maxWidth: 360, // maxWidth: (window.innerWidth * 0.333 - 150) / 5,
isResizable: true, isResizable: true,
data: 'number', data: 'number',
onColumnClick: this.onColumnClick, onColumnClick: this.onColumnClick,
...@@ -128,6 +134,17 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> ...@@ -128,6 +134,17 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
} }
]; ];
setInnerWidth = (): void => {
this.setState(() => ({ innerWidth: window.innerWidth }));
};
componentDidMount(): void {
window.addEventListener('resize', this.setInnerWidth);
}
componentWillUnmount(): void {
window.removeEventListener('resize', this.setInnerWidth);
}
componentDidUpdate(prevProps: SuccessTableProps): void { componentDidUpdate(prevProps: SuccessTableProps): void {
if (this.props.trialIds !== prevProps.trialIds) { if (this.props.trialIds !== prevProps.trialIds) {
const { trialIds } = this.props; const { trialIds } = this.props;
...@@ -138,7 +155,6 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState> ...@@ -138,7 +155,6 @@ class SuccessTable extends React.Component<SuccessTableProps, SuccessTableState>
render(): React.ReactNode { render(): React.ReactNode {
const { columns, source } = this.state; const { columns, source } = this.state;
const isNoneData = source.length === 0 ? true : false; const isNoneData = source.length === 0 ? true : false;
return ( return (
<div id='succTable'> <div id='succTable'>
<DetailsList <DetailsList
......
...@@ -8,7 +8,7 @@ import JSONTree from 'react-json-tree'; ...@@ -8,7 +8,7 @@ import JSONTree from 'react-json-tree';
import PaiTrialLog from '../public-child/PaiTrialLog'; import PaiTrialLog from '../public-child/PaiTrialLog';
import TrialLog from '../public-child/TrialLog'; import TrialLog from '../public-child/TrialLog';
import MessageInfo from '../modals/MessageInfo'; import MessageInfo from '../modals/MessageInfo';
import '../../static/style/overview.scss'; import '../../static/style/overview/overview.scss';
import '../../static/style/copyParameter.scss'; import '../../static/style/copyParameter.scss';
import '../../static/style/openRow.scss'; import '../../static/style/openRow.scss';
......
import React, { useState, useCallback } from 'react';
import { DefaultButton, Stack } from '@fluentui/react';
import TrialConfigPanel from './TrialConfigPanel';
import '../../../static/style/overview/config.scss';
export const TrialConfigButton = (): any => {
const [isShowConfigPanel, setShowConfigPanle] = useState(false);
const [activeTab, setActiveTab] = useState('1');
const hideConfigPanel = useCallback(() => setShowConfigPanle(false), []);
const showTrialConfigpPanel = useCallback(() => {
setShowConfigPanle(true);
setActiveTab('config');
}, []);
const showSearchSpacePanel = useCallback(() => {
setShowConfigPanle(true);
setActiveTab('search space');
}, []);
return (
<React.Fragment>
<Stack className='config'>
<DefaultButton text='Config' onClick={showTrialConfigpPanel} />
<DefaultButton text='Search space' onClick={showSearchSpacePanel} />
</Stack>
{isShowConfigPanel && <TrialConfigPanel hideConfigPanel={hideConfigPanel} activeTab={activeTab} />}
</React.Fragment>
);
};
import * as React from 'react';
import { Stack, Panel, Pivot, PivotItem, PrimaryButton } from '@fluentui/react';
import { EXPERIMENT } from '../../../static/datamodel';
import MonacoEditor from 'react-monaco-editor';
import { MONACO } from '../../../static/const';
import { convertDuration } from '../../../static/function';
import { prettyStringify } from '../../../static/json_util';
import lodash from 'lodash';
import '../../../static/style/logDrawer.scss';
interface LogDrawerProps {
hideConfigPanel: () => void;
activeTab?: string;
}
interface LogDrawerState {
panelInnerHeight: number;
}
class TrialConfigPanel extends React.Component<LogDrawerProps, LogDrawerState> {
constructor(props: LogDrawerProps) {
super(props);
this.state = {
panelInnerHeight: window.innerHeight
};
}
setLogDrawerHeight(): void {
this.setState(() => ({ panelInnerHeight: window.innerHeight }));
}
async componentDidMount(): Promise<void> {
window.addEventListener('resize', this.setLogDrawerHeight);
}
componentWillUnmount(): void {
window.removeEventListener('resize', this.setLogDrawerHeight);
}
render(): React.ReactNode {
const { hideConfigPanel, activeTab } = this.props;
const { panelInnerHeight } = this.state;
// [marginTop 16px] + [Search space 46px] +
// button[height: 32px, marginTop: 45px, marginBottom: 25px] + [padding-bottom: 20px]
const monacoEditorHeight = panelInnerHeight - 184;
const blacklist = [
'id',
'logDir',
'startTime',
'endTime',
'experimentName',
'searchSpace',
'trainingServicePlatform'
];
const filter = (key: string, val: any): any => {
return blacklist.includes(key) ? undefined : val;
};
const profile = lodash.cloneDeep(EXPERIMENT.profile);
profile.execDuration = convertDuration(profile.execDuration);
profile.params.maxExecDuration = convertDuration(profile.params.maxExecDuration);
const showProfile = JSON.stringify(profile, filter, 2);
return (
<Stack>
<Panel
isOpen={true}
hasCloseButton={false}
isFooterAtBottom={true}
isLightDismiss={true}
onLightDismissClick={hideConfigPanel}
>
<div className='log-tab-body'>
<Pivot initialSelectedKey={activeTab} style={{ minHeight: 190, paddingTop: '16px' }}>
<PivotItem headerText='Search space' itemKey='search space'>
<MonacoEditor
height={monacoEditorHeight}
language='json'
theme='vs-light'
value={prettyStringify(EXPERIMENT.searchSpace, 300, 2)}
options={MONACO}
/>
</PivotItem>
<PivotItem headerText='Config' itemKey='config'>
<div className='profile'>
<MonacoEditor
width='100%'
height={monacoEditorHeight}
language='json'
theme='vs-light'
value={showProfile}
options={MONACO}
/>
</div>
</PivotItem>
</Pivot>
</div>
<PrimaryButton text='Close' className='configClose' onClick={hideConfigPanel} />
</Panel>
</Stack>
);
}
}
export default TrialConfigPanel;
...@@ -15,7 +15,7 @@ const DETAILTABS = ( ...@@ -15,7 +15,7 @@ const DETAILTABS = (
const NNILOGO = ( const NNILOGO = (
<NavLink to={'/oview'}> <NavLink to={'/oview'}>
<img src={require('../../static/img/logo2.png')} alt='NNI logo' style={{ height: 40 }} /> <img src={require('../../static/img/logo.png')} alt='NNI logo' style={{ height: 40 }} />
</NavLink> </NavLink>
); );
......
...@@ -23,7 +23,7 @@ import { MANAGER_IP, COLUMNPro } from '../../static/const'; ...@@ -23,7 +23,7 @@ import { MANAGER_IP, COLUMNPro } from '../../static/const';
import { convertDuration, formatTimestamp, intermediateGraphOption, parseMetrics } from '../../static/function'; import { convertDuration, formatTimestamp, intermediateGraphOption, parseMetrics } from '../../static/function';
import { EXPERIMENT, TRIALS } from '../../static/datamodel'; import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import { TableRecord, TrialJobInfo } from '../../static/interface'; import { TableRecord, TrialJobInfo } from '../../static/interface';
const Details = lazy(() => import('../overview/Details')); const Details = lazy(() => import('../overview/table/Details'));
const ChangeColumnComponent = lazy(() => import('../modals/ChangeColumnComponent')); const ChangeColumnComponent = lazy(() => import('../modals/ChangeColumnComponent'));
const Compare = lazy(() => import('../modals/Compare')); const Compare = lazy(() => import('../modals/Compare'));
const KillJob = lazy(() => import('../modals/Killjob')); const KillJob = lazy(() => import('../modals/Killjob'));
......
...@@ -5,13 +5,20 @@ import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-d ...@@ -5,13 +5,20 @@ import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-d
const Overview = lazy(() => import('./components/Overview')); const Overview = lazy(() => import('./components/Overview'));
const TrialsDetail = lazy(() => import('./components/TrialsDetail')); const TrialsDetail = lazy(() => import('./components/TrialsDetail'));
import './index.css'; import './index.css';
import './static/style/loading.scss';
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from './serviceWorker';
ReactDOM.render( ReactDOM.render(
<Router> <Router>
<App> <App>
<Switch> <Switch>
<Suspense fallback={null}> <Suspense
fallback={
<div className='loading'>
<img src={require('./static/img/loading.gif')} />
</div>
}
>
<Route path='/oview' component={Overview} /> <Route path='/oview' component={Overview} />
<Route path='/detail' component={TrialsDetail} /> <Route path='/detail' component={TrialsDetail} />
<Route path='/' render={(): React.ReactNode => <Redirect to={{ pathname: '/oview' }} />} /> <Route path='/' render={(): React.ReactNode => <Redirect to={{ pathname: '/oview' }} />} />
......
...@@ -15,7 +15,7 @@ const trialJobStatus = [ ...@@ -15,7 +15,7 @@ const trialJobStatus = [
'SYS_CANCELED', 'SYS_CANCELED',
'EARLY_STOPPED' 'EARLY_STOPPED'
]; ];
const CONTROLTYPE = ['SEARCH_SPACE', 'TRIAL_CONCURRENCY', 'MAX_EXEC_DURATION']; const CONTROLTYPE = ['MAX_EXEC_DURATION', 'MAX_TRIAL_NUM', 'TRIAL_CONCURRENCY', 'SEARCH_SPACE'];
const MONACO = { const MONACO = {
readOnly: true, readOnly: true,
automaticLayout: true, automaticLayout: true,
......
...@@ -29,25 +29,53 @@ const convertTime = (num: number): string => { ...@@ -29,25 +29,53 @@ const convertTime = (num: number): string => {
} }
}; };
const convertTimeToSecond = (str: string): number => {
let seconds = 0;
let d, h, m;
if (str.includes('d')) {
[d, str] = str.split('d');
seconds += parseInt(d) * 24 * 3600;
}
if (str.includes('h')) {
[h, str] = str.split('h');
seconds += parseInt(h) * 3600;
}
if (str.includes('m')) {
[m, str] = str.split('m');
seconds += parseInt(m) * 60;
}
if (str) {
seconds += parseInt(str.split('s')[0]);
}
return seconds;
};
// trial's duration, accurate to seconds for example 10min 30s // trial's duration, accurate to seconds for example 10min 30s
const convertDuration = (num: number): string => { const convertDuration = (seconds: number): string => {
if (num < 1) { let str = '';
return '0s';
const d = Math.floor(seconds / (24 * 3600));
if (d > 0) {
str += `${d}d `;
} }
const hour = Math.floor(num / 3600); seconds -= d * 24 * 3600;
const minute = Math.floor((num / 60) % 60);
const second = Math.floor(num % 60); const h = Math.floor(seconds / 3600);
const result: string[] = []; if (h > 0) {
if (hour > 0) { str += `${h}h `;
result.push(`${hour}h`);
} }
if (minute > 0) { seconds -= h * 3600;
result.push(`${minute}min`);
const m = Math.floor(seconds / 60);
if (m > 0) {
str += `${m}m `;
} }
if (second > 0) { seconds -= m * 60;
result.push(`${second}s`);
if (seconds > 0) {
str += `${Math.floor(seconds)}s`;
} }
return result.join(' '); return str ? str : '0s';
}; };
function parseMetrics(metricData: string): any { function parseMetrics(metricData: string): any {
...@@ -246,6 +274,7 @@ function formatComplexTypeValue(value: any): string | number { ...@@ -246,6 +274,7 @@ function formatComplexTypeValue(value: any): string | number {
export { export {
convertTime, convertTime,
convertDuration, convertDuration,
convertTimeToSecond,
getFinalResult, getFinalResult,
getFinal, getFinal,
downFile, downFile,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment