From b350776503e2ee8e1af8723070004d1cc0bb9a02 Mon Sep 17 00:00:00 2001 From: Caleb Kang Date: Thu, 30 Jul 2020 08:43:45 -0600 Subject: [PATCH] style: adjust layout for the experiment detail page --- .../src/pages/ExperimentDetails.module.scss | 6 +- webui/react/src/pages/ExperimentDetails.tsx | 47 ++++--- .../ExperimentActions.module.scss | 5 - .../ExperimentDetails/ExperimentActions.tsx | 48 ++++--- .../ExperimentChart.module.scss | 6 + .../ExperimentDetails/ExperimentChart.tsx | 13 +- .../ExperimentInfoBox.module.scss | 28 +++-- .../ExperimentDetails/ExperimentInfoBox.tsx | 118 ++++++++++-------- webui/react/src/styles/antd.scss | 6 +- 9 files changed, 154 insertions(+), 123 deletions(-) delete mode 100644 webui/react/src/pages/ExperimentDetails/ExperimentActions.module.scss create mode 100644 webui/react/src/pages/ExperimentDetails/ExperimentChart.module.scss diff --git a/webui/react/src/pages/ExperimentDetails.module.scss b/webui/react/src/pages/ExperimentDetails.module.scss index bb59a289ca34..ace2258d9f31 100644 --- a/webui/react/src/pages/ExperimentDetails.module.scss +++ b/webui/react/src/pages/ExperimentDetails.module.scss @@ -1,9 +1,5 @@ .topRow { - display: flex; - - section { - flex-grow: 1; - } + height: 40rem; } .error { margin: var(--theme-sizes-layout-medium) var(--theme-sizes-layout-large); diff --git a/webui/react/src/pages/ExperimentDetails.tsx b/webui/react/src/pages/ExperimentDetails.tsx index 86e7a6253f4d..7b6a63418406 100644 --- a/webui/react/src/pages/ExperimentDetails.tsx +++ b/webui/react/src/pages/ExperimentDetails.tsx @@ -1,13 +1,11 @@ -import { Alert, Button, Modal, Table, Tooltip } from 'antd'; +import { Alert, Button, Col, Modal, Row, Space, Table, Tooltip } from 'antd'; import { ColumnType } from 'antd/lib/table'; -import ExperimentActions from 'components/ExperimentActions'; -import ExperimentChart from 'components/ExperimentChart'; -import ExperimentInfoBox from 'components/ExperimentInfoBox'; import yaml from 'js-yaml'; import React, { useCallback, useEffect, useState } from 'react'; import MonacoEditor from 'react-monaco-editor'; import { useParams } from 'react-router'; +import Badge, { BadgeType } from 'components/Badge'; import CheckpointModal from 'components/CheckpointModal'; import Icon from 'components/Icon'; import { makeClickHandler } from 'components/Link'; @@ -223,9 +221,33 @@ const ExperimentDetailsComp: React.FC = () => { experiment={experiment} onClick={{ Fork: showForkModal }} onSettled={pollExperimentDetails} />} - subTitle={experiment?.config.description} + subTitle={ + {experiment?.config.description} + + } title={`Experiment ${experimentId}`}> - + + + + + + + + +
+ + + + { onChange={editorOnChange} /> {forkError && } - -
-
- {activeCheckpoint && = ({ }; const actionButtons: ConditionalButton[] = [ - { button: }, + { + button: , + showIf: (exp): boolean => exp.state === RunState.Paused, + }, + { + button: , + showIf: (exp): boolean => exp.state === RunState.Active, + }, + { button: }, { button: , + onClick={handleArchive(true)}>Archive, showIf: (exp): boolean => terminalRunStates.has(exp.state) && !exp.archived, }, { button: , + onClick={handleArchive(false)}>Unarchive, showIf: (exp): boolean => terminalRunStates.has(exp.state) && exp.archived, }, { - button: , - showIf: (exp): boolean => exp.state === RunState.Active, - }, - { - button: , - showIf: (exp): boolean => exp.state === RunState.Paused, + button: , + showIf: (exp): boolean => !experimentWillNeverHaveData(exp), }, { button: - + , showIf: (exp): boolean => cancellableRunStates.includes(exp.state), }, @@ -137,24 +141,16 @@ const ExperimentActions: React.FC = ({ , showIf: (exp): boolean => killableRunStates.includes(exp.state), }, - { - button: , - showIf: (exp): boolean => !experimentWillNeverHaveData(exp), - }, ]; return ( -
    + {actionButtons .filter(ab => !ab.showIf || ab.showIf(experiment as ExperimentDetails)) .map(ab => ab.button) } -
+ ); - }; export default ExperimentActions; diff --git a/webui/react/src/pages/ExperimentDetails/ExperimentChart.module.scss b/webui/react/src/pages/ExperimentDetails/ExperimentChart.module.scss new file mode 100644 index 000000000000..9055a50f1b84 --- /dev/null +++ b/webui/react/src/pages/ExperimentDetails/ExperimentChart.module.scss @@ -0,0 +1,6 @@ +.base { + border: solid var(--theme-sizes-border-width) var(--theme-colors-monochrome-12); + border-radius: var(--theme-sizes-border-radius); + height: 40rem; + padding: var(--theme-sizes-layout-big); +} diff --git a/webui/react/src/pages/ExperimentDetails/ExperimentChart.tsx b/webui/react/src/pages/ExperimentDetails/ExperimentChart.tsx index d0c89afb51fc..9375eacaaa96 100644 --- a/webui/react/src/pages/ExperimentDetails/ExperimentChart.tsx +++ b/webui/react/src/pages/ExperimentDetails/ExperimentChart.tsx @@ -3,12 +3,13 @@ import { SelectValue } from 'antd/es/select'; import Plotly, { PlotData, PlotlyHTMLElement, PlotRelayoutEvent } from 'plotly.js/lib/core'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import Section from 'components/Section'; +import SelectFilter from 'components/SelectFilter'; import { ValidationHistory } from 'types'; import { clone } from 'utils/data'; import { capitalize, generateAlphaNumeric } from 'utils/string'; -import Section from './Section'; -import SelectFilter from './SelectFilter'; +import css from './ExperimentChart.module.scss'; const { Option } = Select; @@ -37,7 +38,7 @@ type PlotArguments = [ ]; const defaultLayout: Partial = { - height: 400, + height: 368, margin: { b: 50, l: 50, pad: 6, r: 10, t: 10 }, xaxis: { hoverformat: '', @@ -159,8 +160,10 @@ const ExperimentChart: React.FC = ({ validationMetric, ...props }: Props) ); return ( -
-
+
+
+
+
); }; diff --git a/webui/react/src/pages/ExperimentDetails/ExperimentInfoBox.module.scss b/webui/react/src/pages/ExperimentDetails/ExperimentInfoBox.module.scss index 5fbe09fe6573..f218b389cad1 100644 --- a/webui/react/src/pages/ExperimentDetails/ExperimentInfoBox.module.scss +++ b/webui/react/src/pages/ExperimentDetails/ExperimentInfoBox.module.scss @@ -1,14 +1,26 @@ .base { + border: solid var(--theme-sizes-border-width) var(--theme-colors-monochrome-12); + border-radius: var(--theme-sizes-border-radius); color: var(--theme-colors-monochrome-9); font-size: var(--theme-sizes-layout-big); - margin-right: var(--theme-sizes-layout-medium); + height: 100%; + padding: var(--theme-sizes-layout-big); - table { - border-collapse: separate; - border-spacing: 1rem 1rem; - } - .label { - color: black; - font-weight: 700; + .info { + align-items: center; + display: flex; + padding: var(--theme-sizes-layout-medium) 0; + + .label { + color: var(--theme-colors-monochrome-9); + flex-basis: 34%; + font-size: var(--theme-sizes-font-small); + max-width: 16rem; + padding-right: var(--theme-sizes-layout-medium); + } + .content { + color: var(--theme-colors-monochrome-6); + font-size: var(--theme-sizes-font-medium); + } } } diff --git a/webui/react/src/pages/ExperimentDetails/ExperimentInfoBox.tsx b/webui/react/src/pages/ExperimentDetails/ExperimentInfoBox.tsx index b46deb282afe..59178bf607b1 100644 --- a/webui/react/src/pages/ExperimentDetails/ExperimentInfoBox.tsx +++ b/webui/react/src/pages/ExperimentDetails/ExperimentInfoBox.tsx @@ -1,16 +1,17 @@ -import { Button } from 'antd'; +import { Button, Tooltip } from 'antd'; import Modal from 'antd/lib/modal/Modal'; import yaml from 'js-yaml'; import React, { useCallback, useMemo, useState } from 'react'; import MonacoEditor from 'react-monaco-editor'; +import TimeAgo from 'timeago-react'; -import Badge, { BadgeType } from 'components/Badge'; import CheckpointModal from 'components/CheckpointModal'; import Link from 'components/Link'; import ProgressBar from 'components/ProgressBar'; +import Section from 'components/Section'; import { CheckpointDetail, CheckpointState, ExperimentDetails } from 'types'; -import { formatDatetime } from 'utils/date'; import { floatToPercent, humanReadableFloat } from 'utils/string'; +import { getDuration, shortEnglishHumannizer } from 'utils/time'; import css from './ExperimentInfoBox.module.scss'; @@ -18,35 +19,32 @@ interface Props { experiment: ExperimentDetails; } -const renderRow = (label: string, value: React.ReactNode): React.ReactNode => { - if (value === undefined) return <>; +const renderInfo = (label: string, content: React.ReactNode): React.ReactNode => { + if (!content) return null; return ( -
- - - +
+
{label}
+
{content}
+
); }; -const InfoBox: React.FC = ({ experiment: exp }: Props) => { +const InfoBox: React.FC = ({ experiment }: Props) => { + const config = experiment.config; const [ showConfig, setShowConfig ] = useState(false); const [ showBestCheckpoint, setShowBestCheckpoint ] = useState(false); - const orderFactor = exp.config.searcher.smallerIsBetter ? 1 : -1; + const orderFactor = experiment.config.searcher.smallerIsBetter ? 1 : -1; const bestValidation = useMemo(() => { - const sortedValidations = exp.validationHistory + const sortedValidations = experiment.validationHistory .filter(a => a.validationError !== undefined) .sort((a, b) => (a.validationError as number - (b.validationError as number)) * orderFactor); return sortedValidations[0]?.validationError; - }, [ exp.validationHistory, orderFactor ]); + }, [ experiment.validationHistory, orderFactor ]); - const bestCheckpoint: CheckpointDetail = useMemo(() => { - const sortedCheckpoints: CheckpointDetail[] = exp.trials + const bestCheckpoint: CheckpointDetail | undefined = useMemo(() => { + const sortedCheckpoints: CheckpointDetail[] = experiment.trials .filter(trial => trial.bestAvailableCheckpoint && trial.bestAvailableCheckpoint.validationMetric && trial.bestAvailableCheckpoint.state === CheckpointState.Completed) @@ -60,7 +58,7 @@ const InfoBox: React.FC = ({ experiment: exp }: Props) => { return (a.validationMetric as number - (b.validationMetric as number)) * orderFactor; }); return sortedCheckpoints[0]; - }, [ exp.trials, orderFactor ]); + }, [ experiment.trials, orderFactor ]); const handleShowBestCheckpoint = useCallback(() => setShowBestCheckpoint(true), []); const handleHideBestCheckpoint = useCallback(() => setShowBestCheckpoint(false), []); @@ -68,43 +66,57 @@ const InfoBox: React.FC = ({ experiment: exp }: Props) => { const handleHideConfig = useCallback(() => setShowConfig(false), []); return ( -
-
{label} - {[ 'string', 'number' ].includes(typeof value) ? - {value} : value - } -
- - {renderRow('State', )} - {renderRow('Progress', exp.progress && )} - {renderRow('Start Time', formatDatetime(exp.startTime))} - {renderRow('End Time', exp.endTime && formatDatetime(exp.endTime))} - {renderRow('Max Slot', exp.config.resources.maxSlots || 'Unlimited')} - {bestValidation && renderRow( - 'Best Validation', - `${humanReadableFloat(bestValidation)} (${exp.config.searcher.metric})`, - )} - {renderRow('Best Checkpoint', bestCheckpoint && (<> +
+
+ {renderInfo( + 'Progress', + experiment.progress && , + )} + {renderInfo( + 'Best Validation', + bestValidation && `${humanReadableFloat(bestValidation)} (${config.searcher.metric})`, + )} + {renderInfo( + 'Configuration', + , + )} + {renderInfo( + 'Best Checkpoint', + bestCheckpoint && - - ))} - {renderRow('Configuration',)} - {renderRow('Model Definition', )} -
-
+ , + )} + {renderInfo('Max Slot', config.resources.maxSlots || 'Unlimited')} + {renderInfo( + 'Start Time', + + + , + )} + {renderInfo( + 'Duration', + experiment.endTime != null && shortEnglishHumannizer(getDuration(experiment)), + )} + {renderInfo( + 'Model Definition', + Download Model, + )} + + {bestCheckpoint && } @@ -119,9 +131,9 @@ const InfoBox: React.FC = ({ experiment: exp }: Props) => { selectOnLineNumbers: true, }} theme="vs-light" - value={yaml.safeDump(exp.configRaw)} /> + value={yaml.safeDump(experiment.configRaw)} /> - +
); }; diff --git a/webui/react/src/styles/antd.scss b/webui/react/src/styles/antd.scss index 16fb79c1d879..7d17387d7134 100644 --- a/webui/react/src/styles/antd.scss +++ b/webui/react/src/styles/antd.scss @@ -79,14 +79,16 @@ html { /* PageHeader */ .ant-page-header { - padding: 0 0 var(--theme-sizes-layout-medium) 0; + border-bottom: solid var(--theme-sizes-border-width) var(--theme-colors-monochrome-12); + margin-bottom: var(--theme-sizes-layout-big); + padding: 0 0 var(--theme-sizes-layout-big) 0; width: 100%; } .ant-page-header.has-breadcrumb { padding-top: 0; } .ant-page-header-heading, .ant-page-header-heading + .ant-breadcrumb { margin-top: 0; } .ant-page-header-heading-left, - .ant-page-header-heading-right { margin: 0; } + .ant-page-header-heading-extra { margin: 0; } .ant-page-header-heading-title { font-size: var(--theme-sizes-font-jumbo); font-weight: normal;