Commit 593a275c authored by Yuge Zhang's avatar Yuge Zhang
Browse files

Merge branch 'master' of https://github.com/microsoft/nni into dev-retiarii

parents b3cdee85 683c458a
......@@ -65,7 +65,6 @@ dependencies = [
'ruamel.yaml',
'requests',
'responses',
'scipy',
'schema',
'PythonWebHDFS',
'colorama',
......@@ -77,7 +76,9 @@ dependencies = [
'dataclasses ; python_version < "3.7"',
'numpy < 1.19.4 ; sys_platform == "win32"',
'numpy < 1.20 ; sys_platform != "win32" and python_version < "3.7"',
'numpy ; sys.platform != "win32" and python_version >= "3.7"'
'numpy ; sys.platform != "win32" and python_version >= "3.7"',
'scipy < 1.6 ; python_version < "3.7"',
'scipy ; python_version >= "3.7"',
]
release = os.environ.get('NNI_RELEASE')
......@@ -110,6 +111,14 @@ def _setup():
python_requires = '>=3.6',
install_requires = dependencies,
extras_require = {
'SMAC': [
'ConfigSpaceNNI @ git+https://github.com/QuanluZhang/ConfigSpace.git',
'smac @ git+https://github.com/QuanluZhang/SMAC3.git'
],
'BOHB': ['ConfigSpace==0.4.7', 'statsmodels==0.10.0'],
'PPOTuner': ['enum34', 'gym']
},
setup_requires = ['requests'],
entry_points = {
......@@ -157,6 +166,19 @@ def _find_node_files():
def _using_conda_or_virtual_environment():
return sys.prefix != sys.base_prefix or os.path.isdir(os.path.join(sys.prefix, 'conda-meta'))
def _copy_data_files():
# after installation, nni needs to find this location in nni.tools.package_utils.get_registered_algo_config_path
# since we can not import nni here, we need to ensure get_registered_algo_config_path use the same
# logic here to retrieve registered_algorithms.yml
if _using_conda_or_virtual_environment():
nni_config_dir = os.path.join(sys.prefix, 'nni')
elif sys.platform == 'win32':
nni_config_dir = os.path.join(os.getenv('APPDATA'), 'nni')
else:
nni_config_dir = os.path.expanduser('~/.config/nni')
if not os.path.exists(nni_config_dir):
os.makedirs(nni_config_dir)
shutil.copyfile('./deployment/registered_algorithms.yml', os.path.join(nni_config_dir, 'registered_algorithms.yml'))
class BuildTs(Command):
description = 'build TypeScript modules'
......@@ -178,6 +200,7 @@ class Build(build):
sys.exit('Please set environment variable "NNI_RELEASE=<release_version>"')
if os.path.islink('nni_node/main.js'):
sys.exit('A development build already exists. Please uninstall NNI and run "python3 setup.py clean --all".')
_copy_data_files()
super().run()
class Develop(develop):
......@@ -203,6 +226,7 @@ class Develop(develop):
def run(self):
if not self.skip_ts:
setup_ts.build(release=None)
_copy_data_files()
super().run()
class Clean(clean):
......
......@@ -101,6 +101,7 @@ export namespace ValidationSchemas {
name: joi.string().min(1).required()
}),
// ############## adl ###############
namespace: joi.string(),
adaptive: joi.boolean(),
checkpoint: joi.object({
storageClass: joi.string().min(1).required(),
......
......@@ -13,14 +13,17 @@ class AdlClientV1 extends KubernetesCRDClient {
/**
* constructor, to initialize adl CRD definition
*/
public constructor() {
protected readonly namespace: string;
public constructor(namespace: string) {
super();
this.namespace = namespace;
this.crdSchema = JSON.parse(fs.readFileSync('./config/adl/adaptdl-crd-v1.json', 'utf8'));
this.client.addCustomResourceDefinition(this.crdSchema);
}
protected get operator(): any {
return this.client.apis['adaptdl.petuum.com'].v1.namespaces('default').adaptdljobs;
return this.client.apis['adaptdl.petuum.com'].v1.namespaces(this.namespace).adaptdljobs;
}
public get containerName(): string {
......@@ -29,7 +32,7 @@ class AdlClientV1 extends KubernetesCRDClient {
public async getKubernetesPods(jobName: string): Promise<any> {
let result: Promise<any>;
const response = await this.client.api.v1.namespaces('default').pods
const response = await this.client.api.v1.namespaces(this.namespace).pods
.get({ qs: { labelSelector: `adaptdl/job=${jobName}` } });
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(response.body);
......@@ -47,8 +50,8 @@ class AdlClientFactory {
/**
* Factory method to generate operator client
*/
public static createClient(): KubernetesCRDClient {
return new AdlClientV1();
public static createClient(namespace: string): KubernetesCRDClient {
return new AdlClientV1(namespace);
}
}
......
......@@ -58,6 +58,8 @@ export class AdlTrialConfig extends KubernetesTrialConfig {
public readonly image: string;
public readonly namespace?: string;
public readonly imagePullSecrets?: ImagePullSecretConfig[];
public readonly nfs?: NFSConfig;
......@@ -72,7 +74,8 @@ export class AdlTrialConfig extends KubernetesTrialConfig {
constructor(codeDir: string,
command: string, gpuNum: number,
image: string, imagePullSecrets?: ImagePullSecretConfig[],
image: string, namespace?: string,
imagePullSecrets?: ImagePullSecretConfig[],
nfs?: NFSConfig, checkpoint?: CheckpointConfig,
cpuNum?: number, memorySize?: string,
adaptive?: boolean
......@@ -81,6 +84,7 @@ export class AdlTrialConfig extends KubernetesTrialConfig {
this.command = command;
this.gpuNum = gpuNum;
this.image = image;
this.namespace = namespace;
this.imagePullSecrets = imagePullSecrets;
this.nfs = nfs;
this.checkpoint = checkpoint;
......
......@@ -16,21 +16,21 @@ export class AdlJobInfoCollector extends KubernetesJobInfoCollector {
super(jobMap);
}
protected async retrieveSingleTrialJobInfo(kubernetesCRDClient: AdlClientV1 | undefined,
protected async retrieveSingleTrialJobInfo(adlClient: AdlClientV1 | undefined,
kubernetesTrialJob: KubernetesTrialJobDetail): Promise<void> {
if (!this.statusesNeedToCheck.includes(kubernetesTrialJob.status)) {
return Promise.resolve();
}
if (kubernetesCRDClient === undefined) {
return Promise.reject('kubernetesCRDClient is undefined');
if (adlClient === undefined) {
return Promise.reject('AdlClient is undefined');
}
let kubernetesJobInfo: any;
let kubernetesPodsInfo: any;
try {
kubernetesJobInfo = await kubernetesCRDClient.getKubernetesJob(kubernetesTrialJob.kubernetesJobName);
kubernetesPodsInfo = await kubernetesCRDClient.getKubernetesPods(kubernetesTrialJob.kubernetesJobName);
kubernetesJobInfo = await adlClient.getKubernetesJob(kubernetesTrialJob.kubernetesJobName);
kubernetesPodsInfo = await adlClient.getKubernetesPods(kubernetesTrialJob.kubernetesJobName);
} catch (error) {
// Notice: it maynot be a 'real' error since cancel trial job can also cause getKubernetesJob failed.
this.log.error(`Get job ${kubernetesTrialJob.kubernetesJobName} info failed, error is ${error}`);
......
......@@ -39,7 +39,6 @@ class AdlTrainingService extends KubernetesTrainingService implements Kubernetes
super();
this.adlJobInfoCollector = new AdlJobInfoCollector(this.trialJobsMap);
this.experimentId = getExperimentId();
this.kubernetesCRDClient = AdlClientFactory.createClient();
this.configmapTemplateStr = fs.readFileSync(
'./config/adl/adaptdl-nni-configmap-template.json', 'utf8');
this.jobTemplateStr = fs.readFileSync('./config/adl/adaptdljob-template.json', 'utf8');
......@@ -294,15 +293,34 @@ python3 -m nni.tools.trial_tool.trial_keeper --trial_command '{8}' \
return Promise.resolve(runScript);
}
public async cleanUp(): Promise<void> {
super.cleanUp();
// Delete Tensorboard deployment
try {
await this.genericK8sClient.deleteDeployment("adaptdl-tensorboard-" + this.experimentId.toLowerCase());
this.log.info('tensorboard deployment deleted');
} catch (error) {
this.log.error(`tensorboard deployment deletion failed: ${error.message}`);
}
}
public async setClusterMetadata(key: string, value: string): Promise<void> {
this.log.info('SetCluster ' + key + ', ' +value);
switch (key) {
case TrialConfigMetadataKey.NNI_MANAGER_IP:
this.nniManagerIpConfig = <NNIManagerIpConfig>JSON.parse(value);
break;
case TrialConfigMetadataKey.TRIAL_CONFIG:
case TrialConfigMetadataKey.TRIAL_CONFIG: {
this.adlTrialConfig = <AdlTrialConfig>JSON.parse(value);
let namespace: string = 'default';
if (this.adlTrialConfig.namespace !== undefined) {
namespace = this.adlTrialConfig.namespace;
}
this.genericK8sClient.setNamespace = namespace;
this.kubernetesCRDClient = AdlClientFactory.createClient(namespace);
break;
}
case TrialConfigMetadataKey.VERSION_CHECK:
this.versionCheck = (value === 'true' || value === 'True');
break;
......
......@@ -8,17 +8,22 @@ import { Client1_10, config } from 'kubernetes-client';
import { getLogger, Logger } from '../../common/log';
/**
* Generict Kubernetes client, target version >= 1.9
* Generic Kubernetes client, target version >= 1.9
*/
class GeneralK8sClient {
protected readonly client: any;
protected readonly log: Logger = getLogger();
protected namespace: string = 'default';
constructor() {
this.client = new Client1_10({ config: config.fromKubeconfig(), version: '1.9'});
this.client.loadSpec();
}
public set setNamespace(namespace: string) {
this.namespace = namespace;
}
private matchStorageClass(response: any): string {
const adlSupportedProvisioners: RegExp[] = [
new RegExp("microk8s.io/hostpath"),
......@@ -60,7 +65,8 @@ class GeneralK8sClient {
public async createDeployment(deploymentManifest: any): Promise<string> {
let result: Promise<string>;
const response: any = await this.client.apis.apps.v1.namespaces('default').deployments.post({ body: deploymentManifest })
const response: any = await this.client.apis.apps.v1.namespaces(this.namespace)
.deployments.post({ body: deploymentManifest })
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(response.body.metadata.uid);
} else {
......@@ -72,7 +78,7 @@ class GeneralK8sClient {
public async deleteDeployment(deploymentName: string): Promise<boolean> {
let result: Promise<boolean>;
// TODO: change this hard coded deployment name after demo
const response: any = await this.client.apis.apps.v1.namespaces('default')
const response: any = await this.client.apis.apps.v1.namespaces(this.namespace)
.deployment(deploymentName).delete();
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(true);
......@@ -84,7 +90,7 @@ class GeneralK8sClient {
public async createConfigMap(configMapManifest: any): Promise<boolean> {
let result: Promise<boolean>;
const response: any = await this.client.api.v1.namespaces('default')
const response: any = await this.client.api.v1.namespaces(this.namespace)
.configmaps.post({body: configMapManifest});
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(true);
......@@ -97,7 +103,7 @@ class GeneralK8sClient {
public async createPersistentVolumeClaim(pvcManifest: any): Promise<boolean> {
let result: Promise<boolean>;
const response: any = await this.client.api.v1.namespaces('default')
const response: any = await this.client.api.v1.namespaces(this.namespace)
.persistentvolumeclaims.post({body: pvcManifest});
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(true);
......@@ -109,8 +115,8 @@ class GeneralK8sClient {
public async createSecret(secretManifest: any): Promise<boolean> {
let result: Promise<boolean>;
const response: any = await this.client.api.v1.namespaces('default').secrets
.post({body: secretManifest});
const response: any = await this.client.api.v1.namespaces(this.namespace)
.secrets.post({body: secretManifest});
if (response.statusCode && (response.statusCode >= 200 && response.statusCode <= 299)) {
result = Promise.resolve(true);
} else {
......
......@@ -209,13 +209,6 @@ abstract class KubernetesTrainingService {
return Promise.reject(error);
}
try {
await this.genericK8sClient.deleteDeployment("adaptdl-tensorboard-" + getExperimentId().toLowerCase())
this.log.info('tensorboard deployment deleted')
} catch (error) {
this.log.error(`tensorboard deployment deletion failed: ${error.message}`)
}
return Promise.resolve();
}
......
......@@ -58,12 +58,12 @@ class GPUScheduler {
return [];
}
public getSystemGpuCount(): number {
public getSystemGpuCount(): number | undefined{
if (this.gpuSummary !== undefined) {
return this.gpuSummary.gpuCount;
}
return 0;
return undefined;
}
public async stop(): Promise<void> {
......
......@@ -435,8 +435,8 @@ class LocalTrainingService implements TrainingService {
}
private checkSpecifiedGpuIndices(): void {
const gpuCount: number = this.gpuScheduler.getSystemGpuCount();
if (this.designatedGpuIndices !== undefined) {
const gpuCount: number | undefined = this.gpuScheduler.getSystemGpuCount();
if (this.designatedGpuIndices !== undefined && gpuCount !== undefined) {
for (const index of this.designatedGpuIndices) {
if (index >= gpuCount) {
throw new Error(`Specified GPU index not found: ${index}`);
......
......@@ -18,6 +18,7 @@
.headerCon {
width: 90%;
min-width: 1200px;
max-width: 1490px;
margin: 0 auto;
}
......@@ -28,6 +29,7 @@
.content {
width: 87%;
min-height: calc(100vh - 56);
margin: 0 auto;
min-width: 1200px;
max-width: 1490px;
......
......@@ -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 { SlideNavBtns } from './components/slideNav/SlideNavBtns';
......@@ -29,15 +30,15 @@ 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
// eslint-disable-next-line @typescript-eslint/no-empty-function
changeColumn: (_val: string[]): void => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
changeMetricGraphMode: (_val: 'max' | 'min'): void => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
changeMaxDurationUnit: (_val: string): void => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
changeEntries: (_val: string): void => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
updateOverviewPage: () => {}
});
......@@ -139,69 +140,57 @@ 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 */}
<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
}}
>
<SlideNavBtns />
</AppContext.Provider>
{/* 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>
)}
<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>
</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>
</Stack>
</Stack>
)}
</React.Fragment>
);
}
......
import * as React from 'react';
import axios from 'axios';
import { WEBUIDOC, MANAGER_IP } from '../static/const';
import {
Stack,
initializeIcons,
StackItem,
CommandBarButton,
IContextualMenuProps,
IStackTokens,
IStackStyles
} 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 { infoIconAbout, timeIcon, disableUpdates, requency, closeTimer } from './buttons/Icon';
import { OVERVIEWTABS, DETAILTABS, NNILOGO } from './stateless-component/NNItabs';
import { EXPERIMENT } from '../static/datamodel';
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;
......@@ -133,42 +114,50 @@ class NavCon extends React.Component<NavProps, NavState> {
};
return (
<Stack horizontal className='nav'>
<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'>
<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={refreshFrequency === '' ? disableUpdates : timeIcon}
text={refreshText}
menuProps={this.refreshProps}
iconProps={{ iconName: 'ShowResults' }}
text='Experiment summary'
onClick={this.showExpcontent}
/>
<div className='nav-refresh-num'>{refreshFrequency}</div>
</div>
<CommandBarButton
iconProps={{ iconName: 'ShowResults' }}
text='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>
</Link>
</Stack>
</StackItem>
{isvisibleExperimentDrawer && (
<ExperimentSummaryPanel
closeExpDrawer={this.closeExpDrawer}
experimentProfile={EXPERIMENT.profile}
/>
<CommandBarButton iconProps={infoIconAbout} text='About' menuProps={aboutProps} />
</Stack>
</StackItem>
{isvisibleExperimentDrawer && (
<ExperimentSummaryPanel
closeExpDrawer={this.closeExpDrawer}
experimentProfile={EXPERIMENT.profile}
/>
)}
)}
</React.Fragment>
</Stack>
);
}
......
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 };
......@@ -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> {
<Title />
</TitleContext.Provider>
<BestMetricContext.Provider value={{ bestAccuracy: bestAccuracy }}>
<ReBasicInfo />
<BasicInfo />
</BestMetricContext.Provider>
</div>
{/* duration & trial numbers */}
......
......@@ -19,6 +19,9 @@ const LineChart = <Icon iconName='LineChart' />;
const Edit = <Icon iconName='Edit' />;
const CheckMark = <Icon iconName='CheckMark' />;
const Cancel = <Icon iconName='Cancel' />;
const ReplyAll = { iconName: 'ReplyAll' };
const RevToggleKey = { iconName: 'RevToggleKey' };
const ChevronRightMed = <Icon iconName='ChevronRightMed' />;
export {
infoIcon,
......@@ -37,5 +40,8 @@ export {
LineChart,
Edit,
CheckMark,
Cancel
Cancel,
ReplyAll,
RevToggleKey,
ChevronRightMed
};
import * as React from 'react';
import { Stack, DetailsList, DefaultButton, Icon, SearchBox, IColumn } from '@fluentui/react';
import { ExperimentsManager } from '../../static/model/experimentsManager';
import { expformatTimestamp, 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';
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 {
columns: IColumn[];
platform: string[];
errorMessage: string;
hideFilter: boolean;
searchInputVal: string;
selectedStatus: string;
selectedPlatform: string;
selectedStartDate?: Date;
selectedEndDate?: Date;
sortInfo: SortInfo;
source: AllExperimentList[];
originExperimentList: AllExperimentList[];
searchSource: AllExperimentList[];
}
class Experiment extends React.Component<{}, ExpListState> {
constructor(props) {
super(props);
this.state = {
platform: [],
columns: this.columns,
errorMessage: '',
hideFilter: true,
searchInputVal: '',
selectedStatus: '',
selectedPlatform: '',
source: [], // data in table
originExperimentList: [], // api /experiments-info
searchSource: [], // search box search result
sortInfo: { field: '', isDescend: false }
};
}
async componentDidMount(): Promise<void> {
const EXPERIMENTMANAGER = new ExperimentsManager();
await EXPERIMENTMANAGER.init();
const result = EXPERIMENTMANAGER.getExperimentList();
this.setState(() => ({
source: result,
originExperimentList: result,
searchSource: result,
platform: EXPERIMENTMANAGER.getPlatformList(),
errorMessage: EXPERIMENTMANAGER.getExpErrorMessage()
}));
}
render(): React.ReactNode {
const {
platform,
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'>
<Stack className='box' horizontal>
<div className='search'>
<SearchBox
className='search-input'
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={`${!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}>
<FilterBtns
platform={platform}
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)}
/>
</Stack>
<DetailsList
columns={this.columns}
items={source}
setKey='set'
compact={true}
selectionMode={0} // close selector function
className='table'
/>
</Stack>
</Stack>
</Stack>
</Stack>
);
}
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 }
}));
};
private columns: IColumn[] = [
{
name: 'Name',
key: 'experimentName',
fieldName: 'experimentName', // required!
minWidth: MINSCREENCOLUMNWIDHT,
maxWidth: MAXSCREENCOLUMNWIDHT,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
onRender: (item: any): React.ReactNode => <div className='succeed-padding'>{item.experimentName}</div>
},
{
name: 'ID',
key: 'id',
fieldName: 'id',
minWidth: MINSCREENCOLUMNWIDHT,
maxWidth: MAXSCREENCOLUMNWIDHT,
isResizable: true,
className: 'tableHead leftTitle',
data: 'string',
onColumnClick: this.onColumnClick,
onRender: (item: any): React.ReactNode => <NameColumn port={item.port} status={item.status} id={item.id} />
},
{
name: 'Status',
key: 'status',
fieldName: 'status',
minWidth: MINSCREENCOLUMNWIDHT,
maxWidth: MAXSCREENCOLUMNWIDHT,
isResizable: true,
onColumnClick: this.onColumnClick,
onRender: (item: any): React.ReactNode => (
<div className={`${item.status} commonStyle succeed-padding`}>{item.status}</div>
)
},
{
name: 'Port',
key: 'port',
fieldName: 'port',
minWidth: MINSCREENCOLUMNWIDHT - 15,
maxWidth: MAXSCREENCOLUMNWIDHT - 30,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
onRender: (item: any): React.ReactNode => (
<div className='succeed-padding'>
<div>{item.port !== undefined ? item.port : '--'}</div>
</div>
)
},
{
name: 'Platform',
key: 'platform',
fieldName: 'platform',
minWidth: MINSCREENCOLUMNWIDHT - 15,
maxWidth: MAXSCREENCOLUMNWIDHT - 30,
isResizable: true,
data: 'string',
onColumnClick: this.onColumnClick,
onRender: (item: any): React.ReactNode => <div className='commonStyle succeed-padding'>{item.platform}</div>
},
{
name: 'Start time',
key: 'startTime',
fieldName: 'startTime',
minWidth: MINSCREENCOLUMNWIDHT + 15,
maxWidth: MAXSCREENCOLUMNWIDHT + 30,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
onRender: (item: any): React.ReactNode => (
<div className='succeed-padding'>
<div>{expformatTimestamp(item.startTime)}</div>
</div>
)
},
{
name: 'End time',
key: 'endTime',
fieldName: 'endTime',
minWidth: MINSCREENCOLUMNWIDHT + 15,
maxWidth: MAXSCREENCOLUMNWIDHT + 30,
isResizable: true,
data: 'number',
onColumnClick: this.onColumnClick,
onRender: (item: any): React.ReactNode => (
<div className='succeed-padding'>
<div>{expformatTimestamp(item.endTime)}</div>
</div>
)
}
];
private clickFilter(_e: any): void {
const { hideFilter } = this.state;
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 { originExperimentList, sortInfo } = this.state;
if (newValue !== undefined) {
if (newValue === '') {
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
}));
}
this.setState(() => ({
searchInputVal: newValue
}));
}
}
/***
* 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) {
data = filterByStatusOrPlatform(selectedPlatform, 'platform', data);
}
}
if (field === 'platform') {
if (hasStatus) {
data = filterByStatusOrPlatform(selectedStatus, 'status', data);
}
}
if (field === '') {
if (hasPlatform) {
data = filterByStatusOrPlatform(selectedPlatform, 'platform', data);
}
if (hasStatus) {
data = filterByStatusOrPlatform(selectedStatus, 'status', data);
}
}
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;
};
// 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) });
}
};
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,
searchSource,
sortInfo
} = this.state;
const hasStatus = 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 = searchSource.filter(item => 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 => compareDate(new Date(temp.endTime), selectedEndDate!));
}
this.setState(() => ({
source: getSortedSource(result, sortInfo),
selectedStartDate: date
}));
} else {
result = searchSource.filter(item => 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 => compareDate(new Date(temp.startTime), selectedStartDate!));
}
this.setState(() => ({
source: getSortedSource(result, sortInfo),
selectedEndDate: date
}));
}
}
}
// reset
private setSearchSource(): void {
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(result, sortInfo),
selectedStatus: '',
selectedPlatform: '',
selectedStartDate: undefined,
selectedEndDate: undefined
}));
}
}
export default Experiment;
import * as React from 'react';
import { DefaultButton, Icon, Dropdown, DatePicker, DayOfWeek } from '@fluentui/react';
import { EXPERIMENTSTATUS } from '../../static/const';
import { fillOptions } from './expFunction';
interface FilterBtnsProps {
platform: string[];
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 {
platform,
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}
showMonthPickerAsOverlay={true}
placeholder='Select a date...'
ariaLabel='Select a date'
value={selectedStartDate}
onSelectDate={getSelectedData.bind(this, 'start')}
/>
<DatePicker
label='End time'
firstDayOfWeek={DayOfWeek.Sunday}
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;
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>
);
import * as React from 'react';
interface TrialIdColumnProps {
port: number;
id: string;
status: string;
}
class TrialIdColumn extends React.Component<TrialIdColumnProps, {}> {
constructor(props: TrialIdColumnProps) {
super(props);
}
render(): React.ReactNode {
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>{id}</div>
) : (
<a href={webuiPortal} className='link' target='_blank' rel='noopener noreferrer'>
{id}
</a>
)}
</div>
);
}
}
export default TrialIdColumn;
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