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

[WebUI] support tensorboard (#3361)

parent 817ec68b
import * as React from 'react';
import { Stack, DetailsList, DefaultButton, Icon, SearchBox, IColumn } from '@fluentui/react';
import { Stack, DetailsList, DefaultButton, Icon, SearchBox, IColumn, IStackTokens } from '@fluentui/react';
import { ExperimentsManager } from '../../static/model/experimentsManager';
import { expformatTimestamp, copyAndSort } from '../../static/function';
import { AllExperimentList, SortInfo } from '../../static/interface';
......@@ -18,6 +18,10 @@ import '../../static/style/experiment/experiment.scss';
import '../../static/style/overview/probar.scss';
import '../../static/style/tableStatus.css';
const expTokens: IStackTokens = {
childrenGap: 25
};
interface ExpListState {
columns: IColumn[];
platform: string[];
......@@ -111,7 +115,11 @@ class Experiment extends React.Component<{}, ExpListState> {
</DefaultButton>
</div>
</Stack>
<Stack className={`${hideFilter ? 'hidden' : ''} filter-condition`} horizontal gap={25}>
<Stack
className={`${hideFilter ? 'hidden' : ''} filter-condition`}
horizontal
tokens={expTokens}
>
<FilterBtns
platform={platform}
selectedStatus={selectedStatus}
......
......@@ -4,9 +4,9 @@ import { Stack, Modal, IconButton, IDragOptions, ContextualMenu } from '@fluentu
import ReactEcharts from 'echarts-for-react';
import { TooltipForIntermediate, TableObj, SingleAxis } from '../../static/interface';
import { contentStyles, iconButtonStyles } from '../buttons/ModalTheme';
import '../../static/style/compare.scss';
import { convertDuration, parseMetrics } from '../../static/function';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import '../../static/style/compare.scss';
function _getWebUIWidth(): number {
return window.innerWidth;
......
import React from 'react';
import PropTypes from 'prop-types';
import { PrimaryButton, Dialog, DialogType, DialogFooter } from '@fluentui/react';
function TensorboardDialog(props): any {
const { isReaptedStartTensorboard, onHideDialog, item, isShowTensorboardDetail, errorMessage } = props;
const dialogContentProps = {
type: DialogType.normal,
title: `${isShowTensorboardDetail ? item.id : 'TensorBoard'}`
};
function gotoTensorboard(): void {
const hostname = window.location.hostname;
const protocol = window.location.protocol;
window.open(`${protocol}//${hostname}:${item.port}`);
onHideDialog();
}
const startTensorboard = isReaptedStartTensorboard ? (
<div>
You had started this tensorBoard with these trials:
<span className='bold'>{item.trialJobIdList.join(', ')}</span>.
<div className='line-height'>
Its tensorBoard id: <span className='bold'>{item.id}</span>
</div>
</div>
) : (
<div>
You are starting a new TensorBoard with trials:
<span className='bold'>{item.trialJobIdList.join(', ')}</span>.
<div className='line-height'>
TensorBoard id: <span className='bold'>{item.id}</span>
</div>
</div>
);
return (
<Dialog hidden={false} dialogContentProps={dialogContentProps} modalProps={{ className: 'dialog' }}>
{errorMessage.error ? (
<div>
<span>Failed to start tensorBoard! Error message: {errorMessage.message}</span>.
</div>
) : isShowTensorboardDetail ? (
<div>
This tensorBoard with trials: <span className='bold'>{item.trialJobIdList.join(', ')}</span>.
</div>
) : (
startTensorboard
)}
{errorMessage.error ? (
<DialogFooter>
<PrimaryButton onClick={onHideDialog} text='Close' />
</DialogFooter>
) : (
<DialogFooter>
<PrimaryButton
onClick={gotoTensorboard}
text={`${isShowTensorboardDetail ? 'See tensorBoard' : 'Ok'}`}
/>
</DialogFooter>
)}
</Dialog>
);
}
TensorboardDialog.propTypes = {
isReaptedStartTensorboard: PropTypes.bool,
isShowTensorboardDetail: PropTypes.bool,
onHideDialog: PropTypes.func,
item: PropTypes.object,
errorMessage: PropTypes.object
};
export default TensorboardDialog;
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
import { DefaultButton, IContextualMenuProps } from '@fluentui/react';
import { MANAGER_IP } from '../../../static/const';
import { disableTensorboard, getTensorboardMenu } from '../../../static/function';
import { Tensorboard } from '../../../static/interface';
import TensorboardDialog from './TensorboardDialog';
function TensorboardUI(props): any {
let refreshTensorboard = 0;
const { selectedRowIds } = props;
const [queryTensorboardList, setQueryTensorboardList] = useState([]);
const [isReaptedStartTensorboard, setReaptedTensorboard] = useState(false);
const [tensorboardPanelVisible, setTensorboardPanelVisible] = useState(false);
const [isShowTensorboardDetail, setIsShowTensorboardDetail] = useState(false);
const [selectedTensorboard, setSelectedTensorboard] = useState({});
const [errorMessage, setErrorMessage] = useState({});
const [timerList, setTimerList] = useState([0]);
function startTrialTensorboard(): void {
const { selectedRowIds } = props;
if (selectedRowIds.length > 0) {
setIsShowTensorboardDetail(false);
const result = queryTensorboardList.filter(
(item: Tensorboard) => item.trialJobIdList.join(',') === selectedRowIds.join(',')
);
if (result.length > 0) {
setReaptedTensorboard(true);
setSelectedTensorboard(result[0]);
setTensorboardPanelVisible(true);
} else {
const startTensorboard = axios.post(`${MANAGER_IP}/tensorboard`, { trials: selectedRowIds.join(',') });
startTensorboard
.then(res => {
if (res.status === 200) {
setSelectedTensorboard(res.data);
closeTimer();
queryAllTensorboard();
setErrorMessage({ error: false, message: '' });
setTensorboardPanelVisible(true);
}
})
.catch(error => {
setErrorMessage({
error: true,
message: error.message || 'Tensorboard start failed'
});
setTensorboardPanelVisible(true);
});
setReaptedTensorboard(false);
}
} else {
alert('Please select trials first!');
}
}
function queryAllTensorboard(): void {
const queryTensorboard = axios.get(`${MANAGER_IP}/tensorboard-tasks`);
queryTensorboard.then(res => {
if (res.status === 200) {
setQueryTensorboardList(res.data);
if (res.data.length !== 0) {
refreshTensorboard = window.setTimeout(queryAllTensorboard, 10000);
const storeTimerList = timerList;
storeTimerList.push(refreshTensorboard);
setTimerList(storeTimerList);
}
}
});
}
function closeTimer(): void {
timerList.forEach(item => {
window.clearTimeout(item);
});
}
function stopAllTensorboard(): void {
const delTensorboard = axios.delete(`${MANAGER_IP}/tensorboard-tasks`);
delTensorboard.then(res => {
if (res.status === 200) {
setQueryTensorboardList([]);
closeTimer();
}
});
}
function seeTensorboardWebportal(item: Tensorboard): void {
setSelectedTensorboard(item);
setIsShowTensorboardDetail(true);
setTensorboardPanelVisible(true);
}
const isDisableTensorboardBtn = disableTensorboard(selectedRowIds, queryTensorboardList);
const tensorboardMenu: IContextualMenuProps = getTensorboardMenu(
queryTensorboardList,
stopAllTensorboard,
seeTensorboardWebportal
);
useEffect(() => {
queryAllTensorboard();
// clear timer when component is unmounted
return function closeTimer(): void {
timerList.forEach(item => {
window.clearTimeout(item);
});
};
}, []);
return (
<React.Fragment>
<DefaultButton
text='TensorBoard'
className='elementMarginLeft'
split
splitButtonAriaLabel='See 2 options'
aria-roledescription='split button'
menuProps={tensorboardMenu}
onClick={(): void => startTrialTensorboard()}
disabled={isDisableTensorboardBtn}
/>
{queryTensorboardList.length !== 0 ? <span className='circle'>{queryTensorboardList.length}</span> : null}
{tensorboardPanelVisible && (
<TensorboardDialog
isReaptedStartTensorboard={isReaptedStartTensorboard}
isShowTensorboardDetail={isShowTensorboardDetail}
errorMessage={errorMessage}
item={selectedTensorboard}
onHideDialog={(): void => {
setTensorboardPanelVisible(false);
}}
/>
)}
</React.Fragment>
);
}
TensorboardUI.propTypes = {
selectedRowIds: PropTypes.array
};
export default TensorboardUI;
import * as React from 'react';
import { Stack, TooltipHost, ProgressIndicator, DirectionalHint } from '@fluentui/react';
import { Stack, TooltipHost, ProgressIndicator, DirectionalHint, IStackTokens } from '@fluentui/react';
import { EXPERIMENT, TRIALS } from '../../../static/datamodel';
import { CONTROLTYPE, TOOLTIP_BACKGROUND_COLOR, MAX_TRIAL_NUMBERS } from '../../../static/const';
import { EditExperimentParam } from './EditExperimentParam';
......@@ -7,6 +7,13 @@ import { EditExpeParamContext } from './context';
import { ExpDurationContext } from './ExpDurationContext';
import { leftProgress, rightEidtParam, progressHeight } from './commonStyle';
const line1Tokens: IStackTokens = {
childrenGap: 60
};
const line2Tokens: IStackTokens = {
childrenGap: 80
};
export const TrialCount = (): any => {
const count = TRIALS.countStatus();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
......@@ -52,7 +59,7 @@ export const TrialCount = (): any => {
</Stack>
<Stack horizontal className='marginTop'>
<div style={leftProgress}>
<Stack horizontal className='status-count' gap={60}>
<Stack horizontal className='status-count' tokens={line1Tokens}>
<div>
<span>Running</span>
<p>{count.get('RUNNING')}</p>
......@@ -66,7 +73,7 @@ export const TrialCount = (): any => {
<p>{stoppedCount}</p>
</div>
</Stack>
<Stack horizontal className='status-count marginTop' gap={80}>
<Stack horizontal className='status-count marginTop' tokens={line2Tokens}>
<div>
<span>Failed</span>
<p>{count.get('FAILED')}</p>
......
import React from 'react';
import {
DefaultButton,
Dropdown,
......@@ -12,28 +13,29 @@ import {
TooltipHost,
DirectionalHint
} from '@fluentui/react';
import React from 'react';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import { TOOLTIP_BACKGROUND_COLOR } from '../../static/const';
import { convertDuration, formatTimestamp, copyAndSort } from '../../static/function';
import { TableObj, SortInfo } from '../../static/interface';
import '../../static/style/search.scss';
import '../../static/style/tableStatus.css';
import '../../static/style/logPath.scss';
import '../../static/style/table.scss';
import '../../static/style/button.scss';
import '../../static/style/openRow.scss';
import '../../static/style/pagination.scss';
import '../../static/style/overview/overviewTitle.scss';
import { blocked, copy, LineChart, tableListIcon } from '../buttons/Icon';
import ChangeColumnComponent from '../modals/ChangeColumnComponent';
import Compare from '../modals/Compare';
import Customize from '../modals/CustomizedTrial';
import TensorboardUI from '../modals/tensorboard/TensorboardUI';
import KillJob from '../modals/Killjob';
import ExpandableDetails from '../public-child/ExpandableDetails';
import PaginationTable from '../public-child/PaginationTable';
import CopyButton from '../public-child/CopyButton';
import { Trial } from '../../static/model/trial';
import '../../static/style/button.scss';
import '../../static/style/logPath.scss';
import '../../static/style/openRow.scss';
import '../../static/style/pagination.scss';
import '../../static/style/search.scss';
import '../../static/style/table.scss';
import '../../static/style/tableStatus.css';
import '../../static/style/tensorboard.scss';
import '../../static/style/overview/overviewTitle.scss';
require('echarts/lib/chart/line');
require('echarts/lib/component/tooltip');
......@@ -491,6 +493,7 @@ class TableList extends React.Component<TableListProps, TableListState> {
}}
disabled={selectedRowIds.length === 0}
/>
<TensorboardUI selectedRowIds={selectedRowIds} />
</StackItem>
<StackItem grow={50}>
<Stack horizontal horizontalAlign='end' className='allList'>
......
import * as JSON5 from 'json5';
import axios from 'axios';
import { IContextualMenuProps } from '@fluentui/react';
import { MANAGER_IP } from './const';
import { MetricDataRecord, FinalType, TableObj } from './interface';
import { MetricDataRecord, FinalType, TableObj, Tensorboard } from './interface';
async function requestAxios(url: string): Promise<any> {
const response = await axios.get(url);
......@@ -305,6 +306,45 @@ function copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: bool
return (isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1;
});
}
function disableTensorboard(selectedRowIds: string[], queryTensorboardList: Tensorboard[]): boolean {
let flag = true;
if (selectedRowIds.length !== 0) {
flag = false;
}
if (selectedRowIds.length === 0 && queryTensorboardList.length !== 0) {
flag = false;
}
return flag;
}
function getTensorboardMenu(queryTensorboardList: Tensorboard[], stopFunc, seeDetailFunc): IContextualMenuProps {
const result: Array<object> = [];
if (queryTensorboardList.length !== 0) {
result.push({
key: 'delete',
text: 'Stop all tensorBoard',
className: 'clearAll',
onClick: stopFunc
});
queryTensorboardList.forEach(item => {
result.push({
key: item.id,
text: `${item.id}`,
className: `CommandBarButton-${item.status}`,
onClick: (): void => seeDetailFunc(item)
});
});
}
const tensorboardMenu: IContextualMenuProps = {
items: result.reverse() as any
};
return tensorboardMenu;
}
export {
convertTime,
convertDuration,
......@@ -327,5 +367,7 @@ export {
formatComplexTypeValue,
isManagerExperimentPage,
caclMonacoEditorHeight,
copyAndSort
copyAndSort,
disableTensorboard,
getTensorboardMenu
};
......@@ -193,6 +193,16 @@ interface AllExperimentList {
webuiUrl: string[];
logDir: string[];
}
interface Tensorboard {
id: string;
status: string;
trialJobIdList: string[];
trialLogDirectoryList: string[];
pid: number;
port: string;
}
export {
TableObj,
TableRecord,
......@@ -215,5 +225,6 @@ export {
SingleAxis,
MultipleAxes,
SortInfo,
AllExperimentList
AllExperimentList,
Tensorboard
};
$themeBlue: #0071bc;
.ellipsis {
overflow: hidden;
white-space: nowrap;
......@@ -48,3 +50,26 @@
padding: 0 !important;
border: none !important;
}
.elementMarginLeft {
margin-left: 10px;
}
.operationBtn {
i {
color: $themeBlue;
}
}
/* for table row border */
.ms-List-cell {
border-bottom: 1px solid #eeecea;
}
.line-height {
line-height: 30px;
}
.bold {
font-weight: bold;
}
$cicleBg: #009ee7;
$size: 24px;
.circle {
display: inline-block;
width: $size;
height: $size;
border-radius: 50%;
color: #fff;
line-height: $size;
text-align: center;
background: $cicleBg;
margin-left: 4px;
position: relative;
top: -16px;
left: -20px;
}
.CommandBarButton {
&-RUNNING {
.ms-ContextualMenu-itemText::after {
content: 'RUNNING';
color: #0071bc;
margin-left: 15px;
}
}
&-STOPPED {
.ms-ContextualMenu-itemText::after {
content: 'STOPPED';
color: green;
margin-left: 15px;
}
}
&-STOPPING {
.ms-ContextualMenu-itemText::after {
content: 'STOPPING';
color: green;
margin-left: 15px;
}
}
&-DOWNLOADING_DATA {
.ms-ContextualMenu-itemText::after {
content: 'DOWNLOADING_DATA';
color: green;
margin-left: 15px;
}
}
&-ERROR {
.ms-ContextualMenu-itemText::after {
content: 'ERROR';
color: red;
margin-left: 15px;
}
}
&-FAIL_DOWNLOAD_DATA {
.ms-ContextualMenu-itemText::after {
content: 'FAIL_DOWNLOAD_DATA';
color: red;
margin-left: 15px;
}
}
}
.clearAll {
border-top: 1px solid #ccc;
.ms-ContextualMenu-itemText {
text-align: center;
}
}
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