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

feat: add connect modal for notebook and shell tasks [MD-404] #9545

Merged
merged 7 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
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 the running Jupyter Server in VSCode:',
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;
keita-determined marked this conversation as resolved.
Show resolved Hide resolved
}) => (
<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
Loading