Skip to content

Commit

Permalink
feat: add connect modal for notebook and shell tasks [MD-404] (#9545)
Browse files Browse the repository at this point in the history
add "Connect" button and modal to display info for shell and notebook tasks
  • Loading branch information
azhou-determined committed Jun 20, 2024
1 parent b9ea173 commit 83b9a8b
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 20 deletions.
71 changes: 53 additions & 18 deletions webui/react/src/components/TaskActionDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import Button from 'hew/Button';
import Dropdown, { MenuItem } from 'hew/Dropdown';
import Icon from 'hew/Icon';
import { useModal } from 'hew/Modal';
import useConfirm from 'hew/useConfirm';
import React from 'react';
import React, { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';

import css from 'components/ActionDropdown/ActionDropdown.module.scss';
import TaskConnectModalComponent, { TaskConnectField } from 'components/TaskConnectModal';
import usePermissions from 'hooks/usePermissions';
import { paths } from 'routes/utils';
import { paths, serverAddress } from 'routes/utils';
import { killTask } from 'services/api';
import { ExperimentAction as Action, AnyTask, CommandTask, DetailedUser } from 'types';
import { TaskAction as Action, CommandState, CommandTask, CommandType, DetailedUser } from 'types';
import handleError, { ErrorLevel, ErrorType } from 'utils/error';
import { capitalize } from 'utils/string';
import { isTaskKillable } from 'utils/task';
Expand All @@ -19,39 +21,72 @@ interface Props {
curUser?: DetailedUser;
onComplete?: (action?: Action) => void;
onVisibleChange?: (visible: boolean) => void;
task: AnyTask;
task: CommandTask;
}

const TaskActionDropdown: React.FC<Props> = ({ task, onComplete, children }: Props) => {
const { canModifyWorkspaceNSC } = usePermissions();
const isKillable = isTaskKillable(
task,
canModifyWorkspaceNSC({ workspace: { id: task.workspaceId } }),
);
const TaskConnectModal = useModal(TaskConnectModalComponent);

const isConnectable = (task: CommandTask): boolean => {
const connectableTaskTypes: CommandType[] = [CommandType.JupyterLab, CommandType.Shell];
return connectableTaskTypes.includes(task.type) && task.state === CommandState.Running;
};

const confirm = useConfirm();

const menuItems: MenuItem[] = [
{
key: Action.ViewLogs,
label: 'View Logs',
},
];
const taskConnectFields: TaskConnectField[] = useMemo(() => {
switch (task.type) {
case CommandType.JupyterLab:
return [
{
label: 'Connect to notebook in VSCode using the remote Jupyter server address:',
value: `${serverAddress()}${task.serviceAddress}`,
},
];
case CommandType.Shell:
return [
{
label: 'Start an interactive SSH session in the terminal:',
value: `det shell open ${task.id}`,
},
];
default:
return [];
}
}, [task]);

if (isKillable) menuItems.unshift({ key: Action.Kill, label: 'Kill' });
const menuItems: MenuItem[] = useMemo(() => {
const items: MenuItem[] = [
{
key: Action.ViewLogs,
label: 'View Logs',
},
];
if (isTaskKillable(task, canModifyWorkspaceNSC({ workspace: { id: task.workspaceId } }))) {
items.push({ key: Action.Kill, label: 'Kill' });
}
if (isConnectable(task)) {
items.push({ key: Action.Connect, label: 'Connect' });
}
return items;
}, [task, canModifyWorkspaceNSC]);

const navigate = useNavigate();

const handleDropdown = (key: string) => {
try {
switch (key) {
case Action.Connect:
TaskConnectModal.open();
break;
case Action.Kill:
confirm({
content: 'Are you sure you want to kill this task?',
danger: true,
okText: 'Kill',
onConfirm: async () => {
await killTask(task as CommandTask);
await killTask(task);
onComplete?.(key);
},
onError: handleError,
Expand All @@ -60,7 +95,7 @@ const TaskActionDropdown: React.FC<Props> = ({ task, onComplete, children }: Pro
break;
case Action.ViewLogs:
onComplete?.(key);
navigate(paths.taskLogs(task as CommandTask));
navigate(paths.taskLogs(task));
break;
}
} catch (e) {
Expand All @@ -74,7 +109,6 @@ const TaskActionDropdown: React.FC<Props> = ({ task, onComplete, children }: Pro
}
// TODO show loading indicator when we have a button component that supports it.
};

return children ? (
<Dropdown isContextMenu menu={menuItems} onClick={handleDropdown}>
{children}
Expand All @@ -87,6 +121,7 @@ const TaskActionDropdown: React.FC<Props> = ({ task, onComplete, children }: Pro
type="text"
/>
</Dropdown>
<TaskConnectModal.Component fields={taskConnectFields} title={`Connect to ${task.name}`} />
</div>
);
};
Expand Down
32 changes: 32 additions & 0 deletions webui/react/src/components/TaskConnectModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import CodeSample from 'hew/CodeSample';
import { Modal } from 'hew/Modal';
import { Label } from 'hew/Typography';
import React from 'react';

export interface Props {
title?: string;
fields: TaskConnectField[];
}

export interface TaskConnectField {
label: string;
value: string;
}

const TaskConnectModalComponent: React.FC<Props> = ({ fields, title }: Props) => {
return (
<Modal size="medium" title={title ?? 'Connect to Task'}>
<>
{fields.map((lv) => {
return (
<>
<Label>{lv.label}</Label>
<CodeSample text={lv.value} />
</>
);
})}
</>
</Modal>
);
};
export default TaskConnectModalComponent;
3 changes: 1 addition & 2 deletions webui/react/src/components/TaskList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import userStore from 'stores/users';
import workspaceStore from 'stores/workspaces';
import {
ExperimentAction as Action,
AnyTask,
CommandState,
CommandTask,
CommandType,
Expand Down Expand Up @@ -596,7 +595,7 @@ const TaskList: React.FC<Props> = ({ workspace }: Props) => {
}: {
children: React.ReactNode;
onVisibleChange?: (visible: boolean) => void;
record: AnyTask;
record: CommandTask;
}) => (
<TaskActionDropdown
curUser={currentUser}
Expand Down
8 changes: 8 additions & 0 deletions webui/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,14 @@ export interface CommandTask extends Task {
workspaceId: number;
}

export const TaskAction = {
Connect: 'Connect',
Kill: 'Kill',
ViewLogs: 'View Logs',
} as const;

export type TaskAction = ValueOf<typeof TaskAction>;

export type RecentEvent = {
lastEvent: {
date: string;
Expand Down

0 comments on commit 83b9a8b

Please sign in to comment.