Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: refactor experiment creation modal and add continue workflow #971

Merged
merged 6 commits into from
Aug 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions webui/react/src/components/CreateExperimentModal.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.error {
margin: var(--theme-sizes-layout-medium) var(--theme-sizes-layout-large);
}
84 changes: 84 additions & 0 deletions webui/react/src/components/CreateExperimentModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Alert, Modal } from 'antd';
import yaml from 'js-yaml';
import React, { useCallback, useState } from 'react';
import MonacoEditor from 'react-monaco-editor';

import { routeAll } from 'routes';
import { forkExperiment } from 'services/api';

import css from './CreateExperimentModal.module.scss';

interface Props {
title: string;
okText: string;
parentId: number;
visible: boolean;
config: string;
onVisibleChange: (visible: boolean) => void;
onConfigChange: (config: string) => void;
}

const CreateExperimentModal: React.FC<Props> = (
{ visible, config, onVisibleChange, onConfigChange, parentId, ...props }: Props,
) => {
const [ configError, setConfigError ] = useState<string>();

const editorOnChange = useCallback((newValue: string) => {
onConfigChange(newValue);
setConfigError(undefined);
}, [ onConfigChange, setConfigError ]);

const handleOk = async (): Promise<void> => {
try {
// Validate the yaml syntax by attempting to load it.
yaml.safeLoad(config);
const configId = await forkExperiment({ experimentConfig: config, parentId });
onVisibleChange(false);
routeAll(`/det/experiments/${configId}`);
} catch (e) {
let errorMessage = 'Failed to config using the provided config.';
if (e.name === 'YAMLException') {
errorMessage = e.message;
} else if (e.response?.data?.message) {
errorMessage = e.response.data.message;
}
setConfigError(errorMessage);
}
};

const handleCancel = (): void => {
onVisibleChange(false);
};
return <Modal
bodyStyle={{
padding: 0,
}}
className={css.configModal}
okText={props.okText}
style={{
minWidth: '60rem',
}}
title={props.title}
visible={visible}
onCancel={handleCancel}
onOk={handleOk}
>
<MonacoEditor
height="40vh"
language="yaml"
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
selectOnLineNumbers: true,
}}
theme="vs-light"
value={config}
onChange={editorOnChange}
/>
{configError &&
<Alert className={css.error} message={configError} type="error" />
}
</Modal>;

};
export default CreateExperimentModal;
18 changes: 10 additions & 8 deletions webui/react/src/components/TrialActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@ import { terminalRunStates } from 'utils/types';

import css from './TrialActions.module.scss';

interface Props {
trial: TrialDetails;
onSettled: () => void; // A callback to trigger after an action is done.
}

enum Action {
export enum Action {
Continue = 'Continue',
Logs = 'Logs',
Tensorboard = 'Tensorboard',
}

interface Props {
trial: TrialDetails;
onSettled: () => void; // A callback to trigger after an action is done.
onClick: (action: Action) => (() => void);
}

type ButtonLoadingStates = Record<Action, boolean>;

const TrialActions: React.FC<Props> = ({ trial, onSettled: updateFn }: Props) => {
const TrialActions: React.FC<Props> = ({ trial, onClick, onSettled: updateFn }: Props) => {

const [ buttonStates, setButtonStates ] = useState<ButtonLoadingStates>({
Continue: false,
Expand All @@ -48,7 +49,8 @@ const TrialActions: React.FC<Props> = ({ trial, onSettled: updateFn }: Props) =>
};

const actionButtons: ConditionalButton<TrialDetails>[] = [
{ button: <Button disabled key={Action.Continue} type="primary">Continue Trial</Button> },
{ button: <Button key={Action.Continue} type="primary"
onClick={onClick(Action.Continue)}>Continue Trial</Button> },
{ button: <Button key={Action.Logs} type="primary">
<Link path={`/det/trials/${trial.id}/logs`} popout>Logs</Link>
</Button> },
Expand Down
6 changes: 2 additions & 4 deletions webui/react/src/ioTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,10 @@ export const ioStep = io.type({
export const ioTrialDetails = io.type({
end_time: io.union([ io.string, io.null ]),
experiment_id: io.number,

hparams: io.record(io.string, io.any),
id: io.number,

seed: io.number,
start_time: io.string,

state: runStatesIoType,
steps: io.array(ioStep),
warm_start_checkpoint_id: io.union([ io.number, io.null ]),
Expand All @@ -187,7 +185,7 @@ export const ioTrial = io.type({
best_validation_metric: io.union([ io.number, io.null ]),
end_time: io.union([ io.string, io.null ]),
experiment_id: io.number,
hparams: io.any,
hparams: io.record(io.string, io.any),
id: io.number,
latest_validation_metrics: io.union([ ioLatestValidatonMetrics, io.null ]),
num_batches: io.union([ io.number, io.null ]),
Expand Down
3 changes: 0 additions & 3 deletions webui/react/src/pages/ExperimentDetails.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,3 @@
flex-grow: 1;
}
}
.error {
margin: var(--theme-sizes-layout-medium) var(--theme-sizes-layout-large);
}
82 changes: 19 additions & 63 deletions webui/react/src/pages/ExperimentDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Alert, Breadcrumb, Button, Modal, Space, Table, Tooltip } from 'antd';
import { Breadcrumb, Button, Space, Table, Tooltip } from 'antd';
import { ColumnType } from 'antd/lib/table';
import yaml from 'js-yaml';
import React, { useCallback, useEffect, useState } from 'react';
import MonacoEditor from 'react-monaco-editor';
import { useParams } from 'react-router';

import CheckpointModal from 'components/CheckpointModal';
import CreateExperimentModal from 'components/CreateExperimentModal';
import ExperimentActions from 'components/ExperimentActions';
import ExperimentChart from 'components/ExperimentChart';
import ExperimentInfoBox from 'components/ExperimentInfoBox';
Expand All @@ -20,17 +20,14 @@ import { durationRenderer, relativeTimeRenderer, stateRenderer } from 'component
import handleError, { ErrorType } from 'ErrorHandler';
import usePolling from 'hooks/usePolling';
import useRestApi from 'hooks/useRestApi';
import { routeAll } from 'routes';
import { forkExperiment, getExperimentDetails, isNotFound } from 'services/api';
import { getExperimentDetails, isNotFound } from 'services/api';
import { ExperimentDetailsParams } from 'services/types';
import { CheckpointDetail, ExperimentDetails, TrialItem } from 'types';
import { clone } from 'utils/data';
import { alphanumericSorter, numericSorter, runStateSorter, stringTimeSorter } from 'utils/data';
import { humanReadableFloat } from 'utils/string';
import { getDuration } from 'utils/time';

import css from './ExperimentDetails.module.scss';

interface Params {
experimentId: string;
}
Expand All @@ -40,9 +37,6 @@ const ExperimentDetailsComp: React.FC = () => {
const id = parseInt(experimentId);
const [ activeCheckpoint, setActiveCheckpoint ] = useState<CheckpointDetail>();
const [ showCheckpoint, setShowCheckpoint ] = useState(false);
const [ forkValue, setForkValue ] = useState<string>('Loading');
const [ forkModalState, setForkModalState ] = useState({ visible: false });
const [ forkError, setForkError ] = useState<string>();
const [ experimentResponse, triggerExperimentRequest ] =
useRestApi<ExperimentDetailsParams, ExperimentDetails>(getExperimentDetails, { id });
const experiment = experimentResponse.data;
Expand All @@ -54,33 +48,30 @@ const ExperimentDetailsComp: React.FC = () => {
}, [ id, triggerExperimentRequest ]);

usePolling(pollExperimentDetails);
const [ forkModalVisible, setForkModalVisible ] = useState(false);
const [ forkModalConfig, setForkModalConfig ] = useState('Loading');

useEffect(() => {
if (experiment && experiment.config) {
try {
const prefix = 'Fork of ';
const rawConfig = clone(experiment.configRaw);
rawConfig.description = prefix + rawConfig.description;
setForkValue(yaml.safeDump(rawConfig));
setForkModalConfig(yaml.safeDump(rawConfig));
} catch (e) {
handleError({
error: e,
message: 'failed to load experiment config',
type: ErrorType.ApiBadResponse,
});
setForkValue('failed to load experiment config');
setForkModalConfig('failed to load experiment config');
}
}
}, [ experiment ]);

const showForkModal = useCallback((): void => {
setForkModalState(state => ({ ...state, visible: true }));
}, [ setForkModalState ]);

const editorOnChange = useCallback((newValue) => {
setForkValue(newValue);
setForkError(undefined);
}, []);
setForkModalVisible(true);
}, [ setForkModalVisible ]);

const handleTableRow = useCallback((record: TrialItem) => ({
onClick: makeClickHandler(record.url as string),
Expand All @@ -103,28 +94,6 @@ const ExperimentDetailsComp: React.FC = () => {
return <Spinner fillContainer />;
}

const handleOk = async (): Promise<void> => {
try {
// Validate the yaml syntax by attempting to load it.
yaml.safeLoad(forkValue);
const forkId = await forkExperiment({ experimentConfig: forkValue, parentId: id });
setForkModalState(state => ({ ...state, visible: false }));
routeAll(`/det/experiments/${forkId}`);
} catch (e) {
let errorMessage = 'Failed to fork using the provided config.';
if (e.name === 'YAMLException') {
errorMessage = e.message;
} else if (e.response?.data?.message) {
errorMessage = e.response.data.message;
}
setForkError(errorMessage);
}
};

const handleCancel = (): void => {
setForkModalState(state => ({ ...state, visible: false }));
};

const handleCheckpointShow = (event: React.MouseEvent, checkpoint: CheckpointDetail) => {
event.stopPropagation();
setActiveCheckpoint(checkpoint);
Expand Down Expand Up @@ -227,30 +196,8 @@ const ExperimentDetailsComp: React.FC = () => {
experiment={experiment}
onClick={{ Fork: showForkModal }}
onSettled={pollExperimentDetails} />
<div className={css.topRow}>
<div>
<ExperimentInfoBox experiment={experiment} />
<Modal
bodyStyle={{ padding: 0 }}
className={css.forkModal}
okText="Fork"
title={`Fork Experiment ${experimentId}`}
visible={forkModalState.visible}
width={768}
onCancel={handleCancel}
onOk={handleOk}>
<MonacoEditor
height="80vh"
language="yaml"
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
selectOnLineNumbers: true,
}}
theme="vs-light"
value={forkValue}
onChange={editorOnChange} />
{forkError && <Alert className={css.error} message={forkError} type="error" />}
</Modal>
<ExperimentChart
startTime={experiment.startTime}
validationHistory={experiment.validationHistory}
Expand All @@ -271,6 +218,15 @@ const ExperimentDetailsComp: React.FC = () => {
show={showCheckpoint}
title={`Best Checkpoint for Trial ${activeCheckpoint.trialId}`}
onHide={handleCheckpointDismiss} />}
<CreateExperimentModal
config={forkModalConfig}
okText="Fork"
parentId={id}
title={`Fork Experiment ${id}`}
visible={forkModalVisible}
onConfigChange={setForkModalConfig}
onVisibleChange={setForkModalVisible}
/>
</Page>
);
};
Expand Down
Loading