From f6edb73e0930e681e0b7734920e5624c8a8639ef Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Wed, 3 Jun 2020 16:20:48 -0700 Subject: [PATCH 01/19] feat: add tasklist page --- webui/react/src/components/Link.tsx | 31 +++--- .../src/components/TaskActionDropdown.tsx | 4 +- .../react/src/components/TaskCard.stories.tsx | 1 + webui/react/src/components/TaskCard.tsx | 3 +- .../src/components/TasksTable.module.scss | 6 ++ .../src/components/TasksTable.stories.tsx | 19 ++++ webui/react/src/components/TasksTable.tsx | 99 +++++++++++++++++++ webui/react/src/pages/TaskList.tsx | 28 +++++- webui/react/src/services/api.ts | 10 +- webui/react/src/types.ts | 29 +++++- webui/react/src/utils/data.ts | 44 +++++++++ webui/react/src/utils/task.ts | 10 +- webui/react/src/utils/types.ts | 7 +- 13 files changed, 258 insertions(+), 33 deletions(-) create mode 100644 webui/react/src/components/TasksTable.module.scss create mode 100644 webui/react/src/components/TasksTable.stories.tsx create mode 100644 webui/react/src/components/TasksTable.tsx diff --git a/webui/react/src/components/Link.tsx b/webui/react/src/components/Link.tsx index b997ae259c1..31409d59730 100644 --- a/webui/react/src/components/Link.tsx +++ b/webui/react/src/components/Link.tsx @@ -4,26 +4,20 @@ import { routeAll, setupUrlForDev } from 'routes'; import css from './Link.module.scss'; +type OnClick = (event: React.MouseEvent) => void + interface Props { disabled?: boolean; inherit?: boolean; path: string; popout?: boolean; - onClick?: (event: React.MouseEvent) => void; + onClick?: OnClick; } const windowFeatures = [ 'noopener', 'noreferrer' ]; -const Link: React.FC = ({ - disabled, inherit, path, popout, onClick, children, -}: PropsWithChildren) => { - const classes = [ css.base ]; - const rel = windowFeatures.join(' '); - - if (!disabled) classes.push(css.link); - if (inherit) classes.push(css.inherit); - - const handleClick = useCallback((event: React.MouseEvent): void => { +export const handleClick = (path: string, onClick?: OnClick, popout?: boolean): OnClick => { + return (event: React.MouseEvent): void => { const url = setupUrlForDev(path); event.persist(); @@ -36,10 +30,21 @@ const Link: React.FC = ({ } else { routeAll(url); } - }, [ onClick, path, popout ]); + }; +}; + +const Link: React.FC = ({ + disabled, inherit, path, popout, onClick, children, +}: PropsWithChildren) => { + const classes = [ css.base ]; + const rel = windowFeatures.join(' '); + + if (!disabled) classes.push(css.link); + if (inherit) classes.push(css.inherit); return ( - + {children} ); diff --git a/webui/react/src/components/TaskActionDropdown.tsx b/webui/react/src/components/TaskActionDropdown.tsx index ba8e33c1383..d79cc8832d4 100644 --- a/webui/react/src/components/TaskActionDropdown.tsx +++ b/webui/react/src/components/TaskActionDropdown.tsx @@ -6,14 +6,14 @@ import Icon from 'components/Icon'; import Experiments from 'contexts/Experiments'; import handleError, { ErrorLevel, ErrorType } from 'ErrorHandler'; import { archiveExperiment, killTask, setExperimentState } from 'services/api'; -import { Experiment, RecentTask, RunState, TaskType } from 'types'; +import { Experiment, RunState, Task, TaskType } from 'types'; import { capitalize } from 'utils/string'; import { cancellableRunStates, isTaskKillable, terminalRunStates } from 'utils/types'; import css from './TaskActionDropdown.module.scss'; interface Props { - task: RecentTask; + task: Task; } const stopPropagation = (e: React.MouseEvent): void => e.stopPropagation(); diff --git a/webui/react/src/components/TaskCard.stories.tsx b/webui/react/src/components/TaskCard.stories.tsx index 49b9332d404..d8f3dc652c4 100644 --- a/webui/react/src/components/TaskCard.stories.tsx +++ b/webui/react/src/components/TaskCard.stories.tsx @@ -23,6 +23,7 @@ const baseProps: RecentTask = { }, ownerId: 5, progress: 0.34, + startTime: (new Date()).toString(), state: RunState.Active, title: 'I\'m a task', type: TaskType.Experiment, diff --git a/webui/react/src/components/TaskCard.tsx b/webui/react/src/components/TaskCard.tsx index b8cd6880c68..b46820d30af 100644 --- a/webui/react/src/components/TaskCard.tsx +++ b/webui/react/src/components/TaskCard.tsx @@ -8,6 +8,7 @@ import ProgressBar from 'components/ProgressBar'; import TaskActionDropdown from 'components/TaskActionDropdown'; import { RecentTask } from 'types'; import { percent } from 'utils/number'; +import { canBeOpened } from 'utils/task'; import css from './TaskCard.module.scss'; @@ -20,7 +21,7 @@ const TaskCard: React.FC = (props: RecentTask) => { return (
- + {hasProgress &&
} diff --git a/webui/react/src/components/TasksTable.module.scss b/webui/react/src/components/TasksTable.module.scss new file mode 100644 index 00000000000..a16ca1dbd49 --- /dev/null +++ b/webui/react/src/components/TasksTable.module.scss @@ -0,0 +1,6 @@ + +.base { + tr:hover { + text-decoration: none; + } +} diff --git a/webui/react/src/components/TasksTable.stories.tsx b/webui/react/src/components/TasksTable.stories.tsx new file mode 100644 index 00000000000..751033641a4 --- /dev/null +++ b/webui/react/src/components/TasksTable.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import RouterDecorator from 'storybook/RouterDecorator'; +import { Task } from 'types'; +import { generateTasks } from 'utils/task'; + +import TasksTable from './TasksTable'; + +export default { + component: TasksTable, + decorators: [ RouterDecorator ], + title: 'TasksTable', +}; + +const tasks: Task[] = generateTasks(); + +export const Default = (): React.ReactNode => { + return ; +}; diff --git a/webui/react/src/components/TasksTable.tsx b/webui/react/src/components/TasksTable.tsx new file mode 100644 index 00000000000..cd00eedc5a0 --- /dev/null +++ b/webui/react/src/components/TasksTable.tsx @@ -0,0 +1,99 @@ +import { Table } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import React from 'react'; +import TimeAgo from 'timeago-react'; + +import Avatar from 'components/Avatar'; +import Badge from 'components/Badge'; +import linkCss from 'components/Link.module.scss'; +import { CommonProps, Task } from 'types'; +import { alphanumericSorter, stateSorter, stringTimeSorter } from 'utils/data'; +import { canBeOpened } from 'utils/task'; + +import { BadgeType } from './Badge'; +import Icon from './Icon'; +import { handleClick } from './Link'; +import TaskActionDropdown from './TaskActionDropdown'; +import css from './TasksTable.module.scss'; + +interface Props extends CommonProps { + tasks: Task[]; +} + + type Renderer = (text: string, record: T, index: number) => React.ReactNode + +const typeRenderer: Renderer = (_, record) => + (); +const startTimeRenderer: Renderer = (_, record) => + ( + + + + ); +const stateRenderer: Renderer = (_, record) => ( + +); +const actionsRenderer: Renderer = (_, record) => (); +const userRenderer: Renderer = (_, record) => (); + +const columns: ColumnsType = [ + { + dataIndex: 'id', + sorter: (a, b): number => alphanumericSorter(a.id, b.id), + title: 'ID', + }, + { + render: typeRenderer, + sorter: (a, b): number => alphanumericSorter(a.type, b.type), + title: 'Type', + }, + { + dataIndex: 'title', + sorter: (a, b): number => alphanumericSorter(a.title, b.title), + title: 'Description', + }, + { + defaultSortOrder: 'descend', + render: startTimeRenderer, + sorter: (a, b): number => stringTimeSorter(a.startTime, b.startTime), + title: 'Start Time', + }, + { + title: 'Duration', + }, + { + render: stateRenderer, + sorter: (a, b): number => stateSorter(a.state, b.state), + title: 'State', + }, + { + dataIndex: 'ownerId', + render: userRenderer, + title: 'User ID', + }, + { + render: actionsRenderer, + title: '', + }, +]; + +const TasksTable: React.FC = ({ tasks }: Props) => { + return ( + canBeOpened(record) ? linkCss.base : ''} + rowKey="id" + onRow={(record) => { + return { + // can't use an actual link element on the whole row since anchor tag is not a valid + // direct tr child https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr + onClick: canBeOpened(record) ? handleClick(record.url as string) : undefined, + }; + }} /> + + ); +}; + +export default TasksTable; diff --git a/webui/react/src/pages/TaskList.tsx b/webui/react/src/pages/TaskList.tsx index 3bc3d87eb97..71bfd0ef15e 100644 --- a/webui/react/src/pages/TaskList.tsx +++ b/webui/react/src/pages/TaskList.tsx @@ -1,10 +1,36 @@ import React from 'react'; import Page from 'components/Page'; +import TaskTable from 'components/TasksTable'; +import { Commands, Notebooks, Shells, Tensorboards } from 'contexts/Commands'; +import Experiments from 'contexts/Experiments'; +import { commandToTask, experimentToTask } from 'utils/types'; const TaskList: React.FC = () => { + const commands = Commands.useStateContext(); + const notebooks = Notebooks.useStateContext(); + const shells = Shells.useStateContext(); + const tensorboards = Tensorboards.useStateContext(); + const experiments = Experiments.useStateContext(); + + const genericCommands = [ + ...(commands.data || []), + ...(notebooks.data || []), + ...(shells.data || []), + ...(tensorboards.data || []), + ]; + + const loadedTasks = [ + ...(experiments.data || []).map(experimentToTask), + ...genericCommands.map(commandToTask), + ]; + + // TODO select and batch operation: + // https://ant.design/components/table/#components-table-demo-row-selection-and-operation return ( - + + + ); }; diff --git a/webui/react/src/services/api.ts b/webui/react/src/services/api.ts index 872d355ceaf..3f20e3415af 100644 --- a/webui/react/src/services/api.ts +++ b/webui/react/src/services/api.ts @@ -2,11 +2,9 @@ import { CancelToken } from 'axios'; import { generateApi } from 'services/apiBuilder'; import * as Config from 'services/apiConfig'; -import { ExperimentsParams, KillCommandParams, KillExpParams, LaunchTensorboardParams, - PatchExperimentParams, PatchExperimentState } from 'services/types'; -import { - CommandType, Credentials, DeterminedInfo, Experiment, RecentTask, TaskType, User, -} from 'types'; +import { ExperimentsParams, KillCommandParams, KillExpParams, + LaunchTensorboardParams, PatchExperimentParams, PatchExperimentState } from 'services/types'; +import { CommandType, Credentials, DeterminedInfo, Experiment, Task, TaskType, User } from 'types'; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export const isAuthFailure = (e: any): boolean => { @@ -36,7 +34,7 @@ export const launchTensorboard = generateApi(Config.launchTensorboard); export const killTask = - async (task: RecentTask, cancelToken?: CancelToken): Promise => { + async (task: Task, cancelToken?: CancelToken): Promise => { if (task.type === TaskType.Experiment) { return killExperiment({ cancelToken, experimentId: parseInt(task.id) }); } diff --git a/webui/react/src/types.ts b/webui/react/src/types.ts index b420fdfa775..9c6a5dd5027 100644 --- a/webui/react/src/types.ts +++ b/webui/react/src/types.ts @@ -98,6 +98,8 @@ export enum CommandState { Terminated = 'TERMINATED', } +export type State = CommandState | RunState + export interface CommandAddress { containerIp: string; containerPort: number; @@ -179,21 +181,38 @@ export enum TaskType { Tensorboard = 'TENSORBOARD', } -export interface RecentTask { +export interface Task { archived?: boolean; + // source?: Record; + misc?: CommandMisc; title: string; type: TaskType; - lastEvent: { - name: string; - date: string; - }; id: string; ownerId: number; progress?: number; url?: string; + startTime: string; state: RunState | CommandState; } +// food for thought for future work +// remove exlusive fields from Task and: +// export interface ExperimentTask extends Task { +// progress?: number; +// archived?: boolean; +// } + +// export interface CommandTask extends Task { +// misc?: CommandMisc; +// } + +export interface RecentTask extends Task { + lastEvent: { + name: string; + date: string; + }; +} + export type PropsWithClassName = T & {className?: string}; export enum TBSourceType { diff --git a/webui/react/src/utils/data.ts b/webui/react/src/utils/data.ts index b4a94fb810e..f41c7776ee5 100644 --- a/webui/react/src/utils/data.ts +++ b/webui/react/src/utils/data.ts @@ -1,3 +1,5 @@ +import { CommandState, RunState, State } from 'types'; + export const isMap = (data: T): boolean => data instanceof Map; export const isNumber = (data: T): boolean => typeof data === 'number'; export const isObject = (data: T): boolean => typeof data === 'object' && data !== null; @@ -33,3 +35,45 @@ export const categorize = (array: T[], keyFn: ((arg0: T) => string)): Record< }); return d; }; + +export const stringTimeSorter = (a: string, b: string): number => { + const aTime = new Date(a).getTime(); + const bTime = new Date(b).getTime(); + return aTime - bTime; +}; + +export const alphanumericSorter = (a: string, b: string): number => { + return a.localeCompare(b, 'en', { numeric: true }); +}; + +const runStateSortValues: Record = { + [RunState.Active]: 0, + [RunState.Paused]: 1, + [RunState.StoppingError]: 2, + [RunState.Errored]: 3, + [RunState.StoppingCompleted]: 4, + [RunState.Completed]: 5, + [RunState.StoppingCanceled]: 6, + [RunState.Canceled]: 7, + [RunState.Deleted]: 7, +}; + +const commandStateSortValues: Record = { + [CommandState.Pending]: 0, + [CommandState.Assigned]: 1, + [CommandState.Pulling]: 2, + [CommandState.Starting]: 3, + [CommandState.Running]: 4, + [CommandState.Terminating]: 5, + [CommandState.Terminated]: 6, +}; + +export const stateSorter = (a: State, b: State): number => { + // FIXME this is O(n) we can do it in constant time. + // What is the right typescript way of doing it? + const aValue = Object.values(RunState).includes(a as RunState) ? + runStateSortValues[a as RunState] : commandStateSortValues[a as CommandState]; + const bValue = Object.values(RunState).includes(b as RunState) ? + runStateSortValues[b as RunState] : commandStateSortValues[b as CommandState]; + return aValue - bValue; +}; diff --git a/webui/react/src/utils/task.ts b/webui/react/src/utils/task.ts index feda4f25a69..a77e071b5c1 100644 --- a/webui/react/src/utils/task.ts +++ b/webui/react/src/utils/task.ts @@ -1,4 +1,4 @@ -import { CommandState, RecentTask, RunState, TaskType } from 'types'; +import { CommandState, RecentTask, RunState, Task, TaskType } from 'types'; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export function getRandomElementOfEnum(e: any): any { @@ -11,17 +11,18 @@ export function generateTasks(): RecentTask[] { const runStates = new Array(10).fill(0).map(() => getRandomElementOfEnum(RunState)); const cmdStates = new Array(10).fill(0).map(() => getRandomElementOfEnum(CommandState)); const states = [ ...runStates, ...cmdStates ]; - + const startTime = (new Date()).toString(); return states.map((state, idx) => { const progress = Math.random(); const props = { id: `${idx}`, lastEvent: { - date: (new Date()).toString(), + date: startTime, name: 'opened', }, ownerId: Math.floor(Math.random() * 100), progress, + startTime, state: state as RunState | CommandState, title: `${idx}`, type: getRandomElementOfEnum(TaskType) as TaskType, @@ -30,3 +31,6 @@ export function generateTasks(): RecentTask[] { return props; }); } + +// FIXME should check for type specific conditions eg tensorboard is terminated +export const canBeOpened = (task: Task): boolean => !!task.url; diff --git a/webui/react/src/utils/types.ts b/webui/react/src/utils/types.ts index 54112b831fd..724b5459fad 100644 --- a/webui/react/src/utils/types.ts +++ b/webui/react/src/utils/types.ts @@ -1,6 +1,6 @@ import { Command, CommandState, CommandType, Experiment, - RecentTask, RunState, TaskType, + RecentTask, RunState, Task, TaskType, } from 'types'; /* Conversions to Tasks */ @@ -29,7 +29,9 @@ export const commandToTask = (command: Command): RecentTask => { date: command.registeredTime, name: 'requested', }, + misc: command.misc, ownerId: command.owner.id, + startTime: command.registeredTime, state: command.state as CommandState, title, type: command.kind as unknown as TaskType, @@ -48,6 +50,7 @@ export const experimentToTask = (experiment: Experiment): RecentTask => { lastEvent, ownerId: experiment.ownerId, progress: typeof experiment.progress === 'number' ? experiment.progress : undefined, + startTime: experiment.startTime, state: experiment.state as RunState, title: experiment.config.description, type: TaskType.Experiment, @@ -95,7 +98,7 @@ export const commandStateToLabel: {[key in CommandState]: string} = { [CommandState.Terminated]: 'Terminated', }; -export const isTaskKillable = (task: RecentTask): boolean => { +export const isTaskKillable = (task: Task): boolean => { return killableRunStates.includes(task.state as RunState) || killableCmdStates.includes(task.state as CommandState); }; From b2eafd514827db83c9594e01793a2bf17352eaee Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Tue, 9 Jun 2020 17:22:42 -0700 Subject: [PATCH 02/19] remove experiments from task list --- webui/react/src/pages/TaskList.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/webui/react/src/pages/TaskList.tsx b/webui/react/src/pages/TaskList.tsx index 71bfd0ef15e..f7029cc58cc 100644 --- a/webui/react/src/pages/TaskList.tsx +++ b/webui/react/src/pages/TaskList.tsx @@ -3,15 +3,13 @@ import React from 'react'; import Page from 'components/Page'; import TaskTable from 'components/TasksTable'; import { Commands, Notebooks, Shells, Tensorboards } from 'contexts/Commands'; -import Experiments from 'contexts/Experiments'; -import { commandToTask, experimentToTask } from 'utils/types'; +import { commandToTask } from 'utils/types'; const TaskList: React.FC = () => { const commands = Commands.useStateContext(); const notebooks = Notebooks.useStateContext(); const shells = Shells.useStateContext(); const tensorboards = Tensorboards.useStateContext(); - const experiments = Experiments.useStateContext(); const genericCommands = [ ...(commands.data || []), @@ -21,7 +19,6 @@ const TaskList: React.FC = () => { ]; const loadedTasks = [ - ...(experiments.data || []).map(experimentToTask), ...genericCommands.map(commandToTask), ]; From 34e4737cc749177b3d8487fa8ac90e60a5e7cd07 Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Tue, 9 Jun 2020 17:26:58 -0700 Subject: [PATCH 03/19] opt to render username first --- webui/react/src/components/TasksTable.tsx | 7 ++++--- webui/react/src/types.ts | 1 + webui/react/src/utils/types.ts | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/webui/react/src/components/TasksTable.tsx b/webui/react/src/components/TasksTable.tsx index cd00eedc5a0..6e4b4e93e80 100644 --- a/webui/react/src/components/TasksTable.tsx +++ b/webui/react/src/components/TasksTable.tsx @@ -34,7 +34,8 @@ const stateRenderer: Renderer = (_, record) => ( ); const actionsRenderer: Renderer = (_, record) => (); -const userRenderer: Renderer = (_, record) => (); +const userRenderer: Renderer = (_, record) => + (); const columns: ColumnsType = [ { @@ -67,9 +68,9 @@ const columns: ColumnsType = [ title: 'State', }, { - dataIndex: 'ownerId', render: userRenderer, - title: 'User ID', + sorter: (a, b): number => alphanumericSorter(a.username || a.id, a.username || b.id), + title: 'User', }, { render: actionsRenderer, diff --git a/webui/react/src/types.ts b/webui/react/src/types.ts index 9c6a5dd5027..51fed4ae12b 100644 --- a/webui/react/src/types.ts +++ b/webui/react/src/types.ts @@ -191,6 +191,7 @@ export interface Task { ownerId: number; progress?: number; url?: string; + username?: string; startTime: string; state: RunState | CommandState; } diff --git a/webui/react/src/utils/types.ts b/webui/react/src/utils/types.ts index 724b5459fad..9a1b940ff77 100644 --- a/webui/react/src/utils/types.ts +++ b/webui/react/src/utils/types.ts @@ -36,6 +36,7 @@ export const commandToTask = (command: Command): RecentTask => { title, type: command.kind as unknown as TaskType, url: waitPageUrl(command), + username: command.owner.username, }; return task; }; From fe0d01abab3699e21bb98dd652f9a3ade3128811 Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Mon, 15 Jun 2020 16:12:49 -0700 Subject: [PATCH 04/19] add experiments decorator to tasktable story --- webui/react/src/components/TasksTable.stories.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webui/react/src/components/TasksTable.stories.tsx b/webui/react/src/components/TasksTable.stories.tsx index 751033641a4..57dffe9b8b5 100644 --- a/webui/react/src/components/TasksTable.stories.tsx +++ b/webui/react/src/components/TasksTable.stories.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import { ExperimentsDecorator } from 'storybook/ConetextDecorators'; import RouterDecorator from 'storybook/RouterDecorator'; import { Task } from 'types'; import { generateTasks } from 'utils/task'; @@ -8,7 +9,7 @@ import TasksTable from './TasksTable'; export default { component: TasksTable, - decorators: [ RouterDecorator ], + decorators: [ RouterDecorator, ExperimentsDecorator ], title: 'TasksTable', }; From cbcd0d413e2a5e52d34b75985cbe70ac08cc0228 Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Mon, 15 Jun 2020 16:21:59 -0700 Subject: [PATCH 05/19] style feedback p1 --- webui/react/src/components/Link.tsx | 8 +++++--- webui/react/src/components/TaskCard.stories.tsx | 4 ++-- webui/react/src/utils/task.ts | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/webui/react/src/components/Link.tsx b/webui/react/src/components/Link.tsx index 31409d59730..0524755a09e 100644 --- a/webui/react/src/components/Link.tsx +++ b/webui/react/src/components/Link.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, useCallback } from 'react'; +import React, { PropsWithChildren, useMemo } from 'react'; import { routeAll, setupUrlForDev } from 'routes'; @@ -16,7 +16,7 @@ interface Props { const windowFeatures = [ 'noopener', 'noreferrer' ]; -export const handleClick = (path: string, onClick?: OnClick, popout?: boolean): OnClick => { +export const handleClick = (path: string, onClick?: OnClick, popout?: boolean): OnClick => { return (event: React.MouseEvent): void => { const url = setupUrlForDev(path); @@ -38,13 +38,15 @@ const Link: React.FC = ({ }: PropsWithChildren) => { const classes = [ css.base ]; const rel = windowFeatures.join(' '); + const handleLinkClick = useMemo(() => + handleClick(path, onClick, popout), [ path, onClick, popout ]); if (!disabled) classes.push(css.link); if (inherit) classes.push(css.inherit); return ( + onClick={handleLinkClick}> {children} ); diff --git a/webui/react/src/components/TaskCard.stories.tsx b/webui/react/src/components/TaskCard.stories.tsx index d8f3dc652c4..ac48bbe0061 100644 --- a/webui/react/src/components/TaskCard.stories.tsx +++ b/webui/react/src/components/TaskCard.stories.tsx @@ -18,12 +18,12 @@ export default { const baseProps: RecentTask = { id: '1a2f', lastEvent: { - date: (new Date()).toString(), + date: (Date.now()).toString(), name: 'opened', }, ownerId: 5, progress: 0.34, - startTime: (new Date()).toString(), + startTime: (Date.now()).toString(), state: RunState.Active, title: 'I\'m a task', type: TaskType.Experiment, diff --git a/webui/react/src/utils/task.ts b/webui/react/src/utils/task.ts index a77e071b5c1..e9805188c87 100644 --- a/webui/react/src/utils/task.ts +++ b/webui/react/src/utils/task.ts @@ -11,7 +11,7 @@ export function generateTasks(): RecentTask[] { const runStates = new Array(10).fill(0).map(() => getRandomElementOfEnum(RunState)); const cmdStates = new Array(10).fill(0).map(() => getRandomElementOfEnum(CommandState)); const states = [ ...runStates, ...cmdStates ]; - const startTime = (new Date()).toString(); + const startTime = (Date.now()).toString(); return states.map((state, idx) => { const progress = Math.random(); const props = { From 57e013ef5e08254f6948e66c281a247a7be7b06a Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Mon, 15 Jun 2020 16:25:10 -0700 Subject: [PATCH 06/19] taskstable to tasktable --- .../{TasksTable.module.scss => TaskTable.module.scss} | 0 .../{TasksTable.stories.tsx => TaskTable.stories.tsx} | 8 ++++---- .../src/components/{TasksTable.tsx => TaskTable.tsx} | 6 +++--- webui/react/src/pages/TaskList.tsx | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename webui/react/src/components/{TasksTable.module.scss => TaskTable.module.scss} (100%) rename webui/react/src/components/{TasksTable.stories.tsx => TaskTable.stories.tsx} (76%) rename webui/react/src/components/{TasksTable.tsx => TaskTable.tsx} (95%) diff --git a/webui/react/src/components/TasksTable.module.scss b/webui/react/src/components/TaskTable.module.scss similarity index 100% rename from webui/react/src/components/TasksTable.module.scss rename to webui/react/src/components/TaskTable.module.scss diff --git a/webui/react/src/components/TasksTable.stories.tsx b/webui/react/src/components/TaskTable.stories.tsx similarity index 76% rename from webui/react/src/components/TasksTable.stories.tsx rename to webui/react/src/components/TaskTable.stories.tsx index 57dffe9b8b5..dc189c58330 100644 --- a/webui/react/src/components/TasksTable.stories.tsx +++ b/webui/react/src/components/TaskTable.stories.tsx @@ -5,16 +5,16 @@ import RouterDecorator from 'storybook/RouterDecorator'; import { Task } from 'types'; import { generateTasks } from 'utils/task'; -import TasksTable from './TasksTable'; +import TaskTable from './TaskTable'; export default { - component: TasksTable, + component: TaskTable, decorators: [ RouterDecorator, ExperimentsDecorator ], - title: 'TasksTable', + title: 'TaskTable', }; const tasks: Task[] = generateTasks(); export const Default = (): React.ReactNode => { - return ; + return ; }; diff --git a/webui/react/src/components/TasksTable.tsx b/webui/react/src/components/TaskTable.tsx similarity index 95% rename from webui/react/src/components/TasksTable.tsx rename to webui/react/src/components/TaskTable.tsx index 6e4b4e93e80..1c4211f5175 100644 --- a/webui/react/src/components/TasksTable.tsx +++ b/webui/react/src/components/TaskTable.tsx @@ -14,7 +14,7 @@ import { BadgeType } from './Badge'; import Icon from './Icon'; import { handleClick } from './Link'; import TaskActionDropdown from './TaskActionDropdown'; -import css from './TasksTable.module.scss'; +import css from './TaskTable.module.scss'; interface Props extends CommonProps { tasks: Task[]; @@ -78,7 +78,7 @@ const columns: ColumnsType = [ }, ]; -const TasksTable: React.FC = ({ tasks }: Props) => { +const TaskTable: React.FC = ({ tasks }: Props) => { return (
= ({ tasks }: Props) => { ); }; -export default TasksTable; +export default TaskTable; diff --git a/webui/react/src/pages/TaskList.tsx b/webui/react/src/pages/TaskList.tsx index f7029cc58cc..4a96430cfb7 100644 --- a/webui/react/src/pages/TaskList.tsx +++ b/webui/react/src/pages/TaskList.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Page from 'components/Page'; -import TaskTable from 'components/TasksTable'; +import TaskTable from 'components/TaskTable'; import { Commands, Notebooks, Shells, Tensorboards } from 'contexts/Commands'; import { commandToTask } from 'utils/types'; From b46681afd3a4c43e227edc119c52cd4a17dc403d Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Mon, 15 Jun 2020 16:29:51 -0700 Subject: [PATCH 07/19] add and use commandStateSorter --- webui/react/src/components/TaskTable.tsx | 6 +++--- webui/react/src/utils/data.ts | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/webui/react/src/components/TaskTable.tsx b/webui/react/src/components/TaskTable.tsx index 1c4211f5175..950bac0b02e 100644 --- a/webui/react/src/components/TaskTable.tsx +++ b/webui/react/src/components/TaskTable.tsx @@ -6,8 +6,8 @@ import TimeAgo from 'timeago-react'; import Avatar from 'components/Avatar'; import Badge from 'components/Badge'; import linkCss from 'components/Link.module.scss'; -import { CommonProps, Task } from 'types'; -import { alphanumericSorter, stateSorter, stringTimeSorter } from 'utils/data'; +import { CommandState, CommonProps, Task } from 'types'; +import { alphanumericSorter, commandStateSorter, stringTimeSorter } from 'utils/data'; import { canBeOpened } from 'utils/task'; import { BadgeType } from './Badge'; @@ -64,7 +64,7 @@ const columns: ColumnsType = [ }, { render: stateRenderer, - sorter: (a, b): number => stateSorter(a.state, b.state), + sorter: (a, b): number => commandStateSorter(a.state as CommandState, b.state as CommandState), title: 'State', }, { diff --git a/webui/react/src/utils/data.ts b/webui/react/src/utils/data.ts index f41c7776ee5..c8677802246 100644 --- a/webui/react/src/utils/data.ts +++ b/webui/react/src/utils/data.ts @@ -68,6 +68,10 @@ const commandStateSortValues: Record = { [CommandState.Terminated]: 6, }; +export const commandStateSorter = (a: CommandState, b: CommandState): number => { + return commandStateSortValues[a] - commandStateSortValues[b]; +}; + export const stateSorter = (a: State, b: State): number => { // FIXME this is O(n) we can do it in constant time. // What is the right typescript way of doing it? From 0871cbd9e18d2f57be2b6310ab61720369f5caa7 Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Mon, 15 Jun 2020 16:30:50 -0700 Subject: [PATCH 08/19] use relative imports --- webui/react/src/components/TaskTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webui/react/src/components/TaskTable.tsx b/webui/react/src/components/TaskTable.tsx index 950bac0b02e..b7818469741 100644 --- a/webui/react/src/components/TaskTable.tsx +++ b/webui/react/src/components/TaskTable.tsx @@ -3,16 +3,16 @@ import { ColumnsType } from 'antd/lib/table'; import React from 'react'; import TimeAgo from 'timeago-react'; -import Avatar from 'components/Avatar'; -import Badge from 'components/Badge'; -import linkCss from 'components/Link.module.scss'; import { CommandState, CommonProps, Task } from 'types'; import { alphanumericSorter, commandStateSorter, stringTimeSorter } from 'utils/data'; import { canBeOpened } from 'utils/task'; +import Avatar from './Avatar'; +import Badge from './Badge'; import { BadgeType } from './Badge'; import Icon from './Icon'; import { handleClick } from './Link'; +import linkCss from './Link.module.scss'; import TaskActionDropdown from './TaskActionDropdown'; import css from './TaskTable.module.scss'; From 9f0e879e1b49a06cc1aed2cd69789b67279ce17b Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Mon, 15 Jun 2020 16:43:54 -0700 Subject: [PATCH 09/19] added loading state to tasktable --- .../react/src/components/TaskTable.stories.tsx | 4 ++++ webui/react/src/components/TaskTable.tsx | 14 +++++++------- webui/react/src/pages/TaskList.tsx | 18 +++++++----------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/webui/react/src/components/TaskTable.stories.tsx b/webui/react/src/components/TaskTable.stories.tsx index dc189c58330..8837bdbffed 100644 --- a/webui/react/src/components/TaskTable.stories.tsx +++ b/webui/react/src/components/TaskTable.stories.tsx @@ -18,3 +18,7 @@ const tasks: Task[] = generateTasks(); export const Default = (): React.ReactNode => { return ; }; + +export const Loading = (): React.ReactNode => { + return ; +}; diff --git a/webui/react/src/components/TaskTable.tsx b/webui/react/src/components/TaskTable.tsx index b7818469741..2fdc96ac567 100644 --- a/webui/react/src/components/TaskTable.tsx +++ b/webui/react/src/components/TaskTable.tsx @@ -17,19 +17,18 @@ import TaskActionDropdown from './TaskActionDropdown'; import css from './TaskTable.module.scss'; interface Props extends CommonProps { - tasks: Task[]; + tasks?: Task[]; } type Renderer = (text: string, record: T, index: number) => React.ReactNode const typeRenderer: Renderer = (_, record) => (); -const startTimeRenderer: Renderer = (_, record) => - ( - - - - ); +const startTimeRenderer: Renderer = (_, record) => ( + + + +); const stateRenderer: Renderer = (_, record) => ( ); @@ -84,6 +83,7 @@ const TaskTable: React.FC = ({ tasks }: Props) => { className={css.base} columns={columns} dataSource={tasks} + loading={tasks === undefined} rowClassName={(record) => canBeOpened(record) ? linkCss.base : ''} rowKey="id" onRow={(record) => { diff --git a/webui/react/src/pages/TaskList.tsx b/webui/react/src/pages/TaskList.tsx index 4a96430cfb7..df0e17eef1a 100644 --- a/webui/react/src/pages/TaskList.tsx +++ b/webui/react/src/pages/TaskList.tsx @@ -11,22 +11,18 @@ const TaskList: React.FC = () => { const shells = Shells.useStateContext(); const tensorboards = Tensorboards.useStateContext(); - const genericCommands = [ - ...(commands.data || []), - ...(notebooks.data || []), - ...(shells.data || []), - ...(tensorboards.data || []), - ]; - - const loadedTasks = [ - ...genericCommands.map(commandToTask), - ]; + const genericCommands = []; + if (commands.data) genericCommands.push(...commands.data); + if (notebooks.data) genericCommands.push(...notebooks.data); + if (shells.data) genericCommands.push(...shells.data); + if (tensorboards.data) genericCommands.push(...tensorboards.data); + const loadedTasks = genericCommands.map(commandToTask); // TODO select and batch operation: // https://ant.design/components/table/#components-table-demo-row-selection-and-operation return ( - + ); }; From 9d3652243f9a8837908165a5892da8c7e8e6d97b Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Mon, 15 Jun 2020 16:51:35 -0700 Subject: [PATCH 10/19] add render titles --- webui/react/src/components/Icon.tsx | 8 +++++--- webui/react/src/components/TaskTable.tsx | 4 ++-- webui/react/src/types.ts | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/webui/react/src/components/Icon.tsx b/webui/react/src/components/Icon.tsx index d26cbad640d..1f87cbbcb66 100644 --- a/webui/react/src/components/Icon.tsx +++ b/webui/react/src/components/Icon.tsx @@ -1,8 +1,10 @@ import React from 'react'; +import { CommonProps } from 'types'; + import css from './Icon.module.scss'; -interface Props { +interface Props extends CommonProps { name?: string; size?: 'tiny' | 'small' | 'medium' | 'large'; } @@ -12,13 +14,13 @@ const defaultProps: Props = { size: 'medium', }; -const Icon: React.FC = ({ name, size }: Props) => { +const Icon: React.FC = ({ name, size, ...rest }: Props) => { const classes = [ css.base ]; if (name) classes.push(`icon-${name}`); if (size) classes.push(css[size]); - return ; + return ; }; Icon.defaultProps = defaultProps; diff --git a/webui/react/src/components/TaskTable.tsx b/webui/react/src/components/TaskTable.tsx index 2fdc96ac567..b6d80b4ebd6 100644 --- a/webui/react/src/components/TaskTable.tsx +++ b/webui/react/src/components/TaskTable.tsx @@ -23,9 +23,9 @@ interface Props extends CommonProps { type Renderer = (text: string, record: T, index: number) => React.ReactNode const typeRenderer: Renderer = (_, record) => - (); + (); const startTimeRenderer: Renderer = (_, record) => ( - + ); diff --git a/webui/react/src/types.ts b/webui/react/src/types.ts index 51fed4ae12b..1e99c340b41 100644 --- a/webui/react/src/types.ts +++ b/webui/react/src/types.ts @@ -224,4 +224,5 @@ export enum TBSourceType { export type CommonProps = { className?: string; children?: React.ReactNode; + title?: string; } From 65bff7ed5c7814bb63204ed8713c0c596927c11f Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Mon, 15 Jun 2020 16:54:32 -0700 Subject: [PATCH 11/19] add title to avatar --- webui/react/src/components/Avatar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webui/react/src/components/Avatar.tsx b/webui/react/src/components/Avatar.tsx index f5f505b20cf..1a965b8ca19 100644 --- a/webui/react/src/components/Avatar.tsx +++ b/webui/react/src/components/Avatar.tsx @@ -28,7 +28,9 @@ const getColor = (name: string): string => { const Avatar: React.FC = ({ name }: Props) => { const style = { backgroundColor: getColor(name) }; - return
{getInitials(name)}
; + return
+ {getInitials(name)} +
; }; export default Avatar; From 381c38f4be8fdc2f300bbf96e643bce571063a9b Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Mon, 15 Jun 2020 16:55:34 -0700 Subject: [PATCH 12/19] add a TODO --- webui/react/src/components/TaskTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webui/react/src/components/TaskTable.tsx b/webui/react/src/components/TaskTable.tsx index b6d80b4ebd6..2aaf8927a10 100644 --- a/webui/react/src/components/TaskTable.tsx +++ b/webui/react/src/components/TaskTable.tsx @@ -68,6 +68,7 @@ const columns: ColumnsType = [ }, { render: userRenderer, + // TODO get user from users context sorter: (a, b): number => alphanumericSorter(a.username || a.id, a.username || b.id), title: 'User', }, From 54c1dbf07be34f1a4b10ab0aedd904c47308fdca Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Tue, 16 Jun 2020 10:26:19 -0700 Subject: [PATCH 13/19] fixed user renderer --- webui/react/src/components/TaskTable.tsx | 4 ++-- webui/react/src/utils/task.ts | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/webui/react/src/components/TaskTable.tsx b/webui/react/src/components/TaskTable.tsx index 2aaf8927a10..98b45e56e5e 100644 --- a/webui/react/src/components/TaskTable.tsx +++ b/webui/react/src/components/TaskTable.tsx @@ -68,8 +68,8 @@ const columns: ColumnsType = [ }, { render: userRenderer, - // TODO get user from users context - sorter: (a, b): number => alphanumericSorter(a.username || a.id, a.username || b.id), + sorter: (a, b): number => + alphanumericSorter(a.username || a.ownerId.toString(), b.username || b.ownerId.toString()), title: 'User', }, { diff --git a/webui/react/src/utils/task.ts b/webui/react/src/utils/task.ts index e9805188c87..4b09642316c 100644 --- a/webui/react/src/utils/task.ts +++ b/webui/react/src/utils/task.ts @@ -7,6 +7,21 @@ export function getRandomElementOfEnum(e: any): any { return e[keys[index]]; } +const sampleUsers = [ + { + id: 0, + username: 'admin', + }, + { + id: 1, + username: 'determined', + }, + { + id: 2, + username: 'hamid', + }, +]; + export function generateTasks(): RecentTask[] { const runStates = new Array(10).fill(0).map(() => getRandomElementOfEnum(RunState)); const cmdStates = new Array(10).fill(0).map(() => getRandomElementOfEnum(CommandState)); @@ -14,19 +29,21 @@ export function generateTasks(): RecentTask[] { const startTime = (Date.now()).toString(); return states.map((state, idx) => { const progress = Math.random(); + const user = sampleUsers[Math.floor(Math.random() * sampleUsers.length)]; const props = { id: `${idx}`, lastEvent: { date: startTime, name: 'opened', }, - ownerId: Math.floor(Math.random() * 100), + ownerId: user.id, progress, startTime, state: state as RunState | CommandState, title: `${idx}`, type: getRandomElementOfEnum(TaskType) as TaskType, url: '#', + username: user.username, }; return props; }); From 3b25812facb3c701d3203e19839cad8296f9fe1f Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Tue, 16 Jun 2020 10:56:19 -0700 Subject: [PATCH 14/19] dedicated label for command types --- webui/react/src/components/TaskTable.tsx | 9 ++++----- webui/react/src/utils/types.ts | 7 +++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/webui/react/src/components/TaskTable.tsx b/webui/react/src/components/TaskTable.tsx index 98b45e56e5e..04fb5be9e85 100644 --- a/webui/react/src/components/TaskTable.tsx +++ b/webui/react/src/components/TaskTable.tsx @@ -3,9 +3,10 @@ import { ColumnsType } from 'antd/lib/table'; import React from 'react'; import TimeAgo from 'timeago-react'; -import { CommandState, CommonProps, Task } from 'types'; +import { CommandState, CommandType, CommonProps, Task } from 'types'; import { alphanumericSorter, commandStateSorter, stringTimeSorter } from 'utils/data'; import { canBeOpened } from 'utils/task'; +import { commandTypeToLabel } from 'utils/types'; import Avatar from './Avatar'; import Badge from './Badge'; @@ -23,7 +24,8 @@ interface Props extends CommonProps { type Renderer = (text: string, record: T, index: number) => React.ReactNode const typeRenderer: Renderer = (_, record) => - (); + (); const startTimeRenderer: Renderer = (_, record) => ( @@ -58,9 +60,6 @@ const columns: ColumnsType = [ sorter: (a, b): number => stringTimeSorter(a.startTime, b.startTime), title: 'Start Time', }, - { - title: 'Duration', - }, { render: stateRenderer, sorter: (a, b): number => commandStateSorter(a.state as CommandState, b.state as CommandState), diff --git a/webui/react/src/utils/types.ts b/webui/react/src/utils/types.ts index 9a1b940ff77..af855d6f1d6 100644 --- a/webui/react/src/utils/types.ts +++ b/webui/react/src/utils/types.ts @@ -108,6 +108,13 @@ export function stateToLabel(state: RunState | CommandState): string { return runStateToLabel[state as RunState] || commandStateToLabel[state as CommandState]; } +export const commandTypeToLabel: {[key in CommandType]: string} = { + [CommandType.Command]: 'Command', + [CommandType.Notebook]: 'Notebook', + [CommandType.Shell]: 'Shell', + [CommandType.Tensorboard]: 'Tensorboard', +}; + /* * `keyof any` is short for "string | number | symbol" * since an object key can be any of those types, our key can too From e647ca660a57f443026d652dbd989f0107c18405 Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Tue, 16 Jun 2020 11:09:56 -0700 Subject: [PATCH 15/19] imporve canBeOpened --- webui/react/src/types.ts | 9 +++++++++ webui/react/src/utils/task.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/webui/react/src/types.ts b/webui/react/src/types.ts index 1e99c340b41..a70063462fb 100644 --- a/webui/react/src/types.ts +++ b/webui/react/src/types.ts @@ -226,3 +226,12 @@ export type CommonProps = { children?: React.ReactNode; title?: string; } + +export const terminalCommandStates: Set = new Set([ + CommandState.Terminated, +]); + +export const terminalRunStates: Set = new Set([ + RunState.Errored, + RunState.Canceled, +]); diff --git a/webui/react/src/utils/task.ts b/webui/react/src/utils/task.ts index 4b09642316c..9e775441a9b 100644 --- a/webui/react/src/utils/task.ts +++ b/webui/react/src/utils/task.ts @@ -1,4 +1,4 @@ -import { CommandState, RecentTask, RunState, Task, TaskType } from 'types'; +import { CommandState, RecentTask, RunState, Task, TaskType, terminalCommandStates } from 'types'; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export function getRandomElementOfEnum(e: any): any { @@ -49,5 +49,9 @@ export function generateTasks(): RecentTask[] { }); } -// FIXME should check for type specific conditions eg tensorboard is terminated -export const canBeOpened = (task: Task): boolean => !!task.url; +export const canBeOpened = (task: Task): boolean => { + if (task.type !== TaskType.Experiment && task.state in terminalCommandStates) { + return false; + } + return !!task.url; +}; From c79667f03a439ea2da1a807358b808599f212645 Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Tue, 16 Jun 2020 11:12:39 -0700 Subject: [PATCH 16/19] only use non-experiment tasks in tasktable story --- webui/react/src/components/TaskTable.stories.tsx | 4 ++-- webui/react/src/utils/task.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/webui/react/src/components/TaskTable.stories.tsx b/webui/react/src/components/TaskTable.stories.tsx index 8837bdbffed..7234082dc47 100644 --- a/webui/react/src/components/TaskTable.stories.tsx +++ b/webui/react/src/components/TaskTable.stories.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { ExperimentsDecorator } from 'storybook/ConetextDecorators'; import RouterDecorator from 'storybook/RouterDecorator'; -import { Task } from 'types'; +import { Task, TaskType } from 'types'; import { generateTasks } from 'utils/task'; import TaskTable from './TaskTable'; @@ -13,7 +13,7 @@ export default { title: 'TaskTable', }; -const tasks: Task[] = generateTasks(); +const tasks: Task[] = generateTasks(20).filter(task => task.type !== TaskType.Experiment); export const Default = (): React.ReactNode => { return ; diff --git a/webui/react/src/utils/task.ts b/webui/react/src/utils/task.ts index 9e775441a9b..bbabd38a20a 100644 --- a/webui/react/src/utils/task.ts +++ b/webui/react/src/utils/task.ts @@ -22,9 +22,11 @@ const sampleUsers = [ }, ]; -export function generateTasks(): RecentTask[] { - const runStates = new Array(10).fill(0).map(() => getRandomElementOfEnum(RunState)); - const cmdStates = new Array(10).fill(0).map(() => getRandomElementOfEnum(CommandState)); +export function generateTasks(count = 10): RecentTask[] { + const runStates = new Array(Math.floor(count)).fill(0) + .map(() => getRandomElementOfEnum(RunState)); + const cmdStates = new Array(Math.ceil(count)).fill(0) + .map(() => getRandomElementOfEnum(CommandState)); const states = [ ...runStates, ...cmdStates ]; const startTime = (Date.now()).toString(); return states.map((state, idx) => { From 34b1e1ace773c68ccb5110af1e78d05801957e1d Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Tue, 16 Jun 2020 11:33:53 -0700 Subject: [PATCH 17/19] improve loaded state --- .../src/components/TaskTable.stories.tsx | 4 ++++ webui/react/src/pages/TaskList.tsx | 22 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/webui/react/src/components/TaskTable.stories.tsx b/webui/react/src/components/TaskTable.stories.tsx index 7234082dc47..5f8d2f5021e 100644 --- a/webui/react/src/components/TaskTable.stories.tsx +++ b/webui/react/src/components/TaskTable.stories.tsx @@ -22,3 +22,7 @@ export const Default = (): React.ReactNode => { export const Loading = (): React.ReactNode => { return ; }; + +export const LoadedNoRows = (): React.ReactNode => { + return ; +}; diff --git a/webui/react/src/pages/TaskList.tsx b/webui/react/src/pages/TaskList.tsx index df0e17eef1a..1af49a5a84c 100644 --- a/webui/react/src/pages/TaskList.tsx +++ b/webui/react/src/pages/TaskList.tsx @@ -11,18 +11,26 @@ const TaskList: React.FC = () => { const shells = Shells.useStateContext(); const tensorboards = Tensorboards.useStateContext(); - const genericCommands = []; - if (commands.data) genericCommands.push(...commands.data); - if (notebooks.data) genericCommands.push(...notebooks.data); - if (shells.data) genericCommands.push(...shells.data); - if (tensorboards.data) genericCommands.push(...tensorboards.data); - const loadedTasks = genericCommands.map(commandToTask); + const sources = [ + commands, + notebooks, + shells, + tensorboards, + ]; + + const loadedTasks = sources + .filter(src => src.data !== undefined) + .map(src => src.data || []) + .reduce((acc, cur) => [ ...acc, ...cur ], []) + .map(commandToTask); + + const hasLoaded = sources.find(src => src.hasLoaded); // TODO select and batch operation: // https://ant.design/components/table/#components-table-demo-row-selection-and-operation return ( - + ); }; From 4a2532b5be7767a243510be0f55948a78a886347 Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Tue, 16 Jun 2020 16:01:03 -0700 Subject: [PATCH 18/19] use useCallback instead of useMemo --- webui/react/src/components/Link.tsx | 18 +++++++++--------- webui/react/src/components/TaskTable.tsx | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/webui/react/src/components/Link.tsx b/webui/react/src/components/Link.tsx index 0524755a09e..c9200de281f 100644 --- a/webui/react/src/components/Link.tsx +++ b/webui/react/src/components/Link.tsx @@ -1,23 +1,22 @@ -import React, { PropsWithChildren, useMemo } from 'react'; +import React, { MouseEventHandler, PropsWithChildren, useMemo as useCallback } from 'react'; import { routeAll, setupUrlForDev } from 'routes'; import css from './Link.module.scss'; -type OnClick = (event: React.MouseEvent) => void - interface Props { disabled?: boolean; inherit?: boolean; path: string; popout?: boolean; - onClick?: OnClick; + onClick?: MouseEventHandler; } const windowFeatures = [ 'noopener', 'noreferrer' ]; -export const handleClick = (path: string, onClick?: OnClick, popout?: boolean): OnClick => { - return (event: React.MouseEvent): void => { +export const makeClickHandler = (path: string, onClick?: MouseEventHandler, + popout?: boolean): MouseEventHandler => { + const handler: MouseEventHandler = (event) => { const url = setupUrlForDev(path); event.persist(); @@ -31,6 +30,7 @@ export const handleClick = (path: string, onClick?: OnClick, popout?: boolean): routeAll(url); } }; + return handler; }; const Link: React.FC = ({ @@ -38,15 +38,15 @@ const Link: React.FC = ({ }: PropsWithChildren) => { const classes = [ css.base ]; const rel = windowFeatures.join(' '); - const handleLinkClick = useMemo(() => - handleClick(path, onClick, popout), [ path, onClick, popout ]); + const handleClick = + useCallback(() => makeClickHandler(path, onClick, popout), [ path, onClick, popout ]); if (!disabled) classes.push(css.link); if (inherit) classes.push(css.inherit); return ( + onClick={handleClick}> {children} ); diff --git a/webui/react/src/components/TaskTable.tsx b/webui/react/src/components/TaskTable.tsx index 04fb5be9e85..bab206c5528 100644 --- a/webui/react/src/components/TaskTable.tsx +++ b/webui/react/src/components/TaskTable.tsx @@ -12,7 +12,7 @@ import Avatar from './Avatar'; import Badge from './Badge'; import { BadgeType } from './Badge'; import Icon from './Icon'; -import { handleClick } from './Link'; +import { makeClickHandler } from './Link'; import linkCss from './Link.module.scss'; import TaskActionDropdown from './TaskActionDropdown'; import css from './TaskTable.module.scss'; @@ -90,7 +90,7 @@ const TaskTable: React.FC = ({ tasks }: Props) => { return { // can't use an actual link element on the whole row since anchor tag is not a valid // direct tr child https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr - onClick: canBeOpened(record) ? handleClick(record.url as string) : undefined, + onClick: canBeOpened(record) ? makeClickHandler(record.url as string) : undefined, }; }} /> From 546ce11edec1722251375e9884934e729f695efe Mon Sep 17 00:00:00 2001 From: Hamid Zare Date: Tue, 16 Jun 2020 16:07:22 -0700 Subject: [PATCH 19/19] fix useCallback import --- webui/react/src/components/Link.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webui/react/src/components/Link.tsx b/webui/react/src/components/Link.tsx index c9200de281f..e5abfc808f6 100644 --- a/webui/react/src/components/Link.tsx +++ b/webui/react/src/components/Link.tsx @@ -1,4 +1,4 @@ -import React, { MouseEventHandler, PropsWithChildren, useMemo as useCallback } from 'react'; +import React, { MouseEventHandler, PropsWithChildren, useCallback } from 'react'; import { routeAll, setupUrlForDev } from 'routes'; @@ -39,7 +39,7 @@ const Link: React.FC = ({ const classes = [ css.base ]; const rel = windowFeatures.join(' '); const handleClick = - useCallback(() => makeClickHandler(path, onClick, popout), [ path, onClick, popout ]); + useCallback(makeClickHandler(path, onClick, popout), [ path, onClick, popout ]); if (!disabled) classes.push(css.link); if (inherit) classes.push(css.inherit);