diff --git a/client/src/app/components/task-manager/TaskManagerDrawer.tsx b/client/src/app/components/task-manager/TaskManagerDrawer.tsx index 63ccc967c..56867b803 100644 --- a/client/src/app/components/task-manager/TaskManagerDrawer.tsx +++ b/client/src/app/components/task-manager/TaskManagerDrawer.tsx @@ -2,6 +2,9 @@ import React, { forwardRef, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import dayjs from "dayjs"; import { + Dropdown, + DropdownItem, + DropdownList, EmptyState, EmptyStateActions, EmptyStateBody, @@ -9,6 +12,8 @@ import { EmptyStateHeader, EmptyStateIcon, EmptyStateVariant, + MenuToggle, + MenuToggleElement, NotificationDrawer, NotificationDrawerBody, NotificationDrawerHeader, @@ -18,15 +23,16 @@ import { NotificationDrawerListItemHeader, Tooltip, } from "@patternfly/react-core"; -import { CubesIcon } from "@patternfly/react-icons"; +import { CubesIcon, EllipsisVIcon } from "@patternfly/react-icons"; import { css } from "@patternfly/react-styles"; -import { TaskState } from "@app/api/models"; +import { Task, TaskState } from "@app/api/models"; import { useTaskManagerContext } from "./TaskManagerContext"; import { useServerTasks } from "@app/queries/tasks"; import "./TaskManagerDrawer.css"; import { TaskStateIcon } from "../Icons"; +import { useTaskActions } from "@app/pages/tasks/useTaskActions"; /** A version of `Task` specific for the task manager drawer components */ interface TaskManagerTask { @@ -47,6 +53,9 @@ interface TaskManagerTask { applicationId: number; applicationName: string; preemptEnabled: boolean; + + // full object to be used with library functions + _: Task; } const PAGE_SIZE = 20; @@ -61,6 +70,9 @@ export const TaskManagerDrawer: React.FC = forwardRef( const { tasks } = useTaskManagerData(); const [expandedItems, setExpandedItems] = useState([]); + const [taskWithExpandedActions, setTaskWithExpandedAction] = useState< + number | boolean + >(false); const closeDrawer = () => { setIsExpanded(!isExpanded); @@ -109,6 +121,10 @@ export const TaskManagerDrawer: React.FC = forwardRef( : expandedItems.filter((i) => i !== task.id) ); }} + actionsExpanded={task.id === taskWithExpandedActions} + onActionsExpandToggle={(flag: boolean) => + setTaskWithExpandedAction(flag && task.id) + } /> ))} @@ -130,13 +146,22 @@ const TaskItem: React.FC<{ task: TaskManagerTask; expanded: boolean; onExpandToggle: (expand: boolean) => void; -}> = ({ task, expanded, onExpandToggle }) => { + actionsExpanded: boolean; + onActionsExpandToggle: (expand: boolean) => void; +}> = ({ + task, + expanded, + onExpandToggle, + actionsExpanded, + onActionsExpandToggle, +}) => { const starttime = dayjs(task.started ?? task.createTime); const title = expanded ? `${task.id} (${task.addon})` : `${task.id} (${task.addon}) - ${task.applicationName} - ${ task.priority ?? 0 }`; + const taskActionItems = useTaskActions(task._); return ( } > - {/* Put the item's action menu here */} + onActionsExpandToggle(false)} + isOpen={actionsExpanded} + onOpenChange={() => onActionsExpandToggle(false)} + popperProps={{ position: "right" }} + toggle={(toggleRef: React.Ref) => ( + isDisabled)} + onClick={() => onActionsExpandToggle(!actionsExpanded)} + variant="plain" + aria-label={`Actions for task ${task.name}`} + > + + )} + > + + {taskActionItems.map(({ title, onClick, isDisabled }) => ( + + {title} + + ))} + + {expanded ? ( { applicationName: task.application.name, preemptEnabled: task?.policy?.preemptEnabled ?? false, + _: task, + // TODO: Add any checks that could be needed later... // - isCancelable (does the current user own the task? other things to check?) // - isPreemptionToggleAllowed diff --git a/client/src/app/pages/tasks/TaskActionColumn.tsx b/client/src/app/pages/tasks/TaskActionColumn.tsx new file mode 100644 index 000000000..0f99d035e --- /dev/null +++ b/client/src/app/pages/tasks/TaskActionColumn.tsx @@ -0,0 +1,10 @@ +import React, { FC } from "react"; + +import { Task } from "@app/api/models"; +import { ActionsColumn } from "@patternfly/react-table"; +import { useTaskActions } from "./useTaskActions"; + +export const TaskActionColumn: FC<{ task: Task }> = ({ task }) => { + const actions = useTaskActions(task); + return ; +}; diff --git a/client/src/app/pages/tasks/tasks-page.tsx b/client/src/app/pages/tasks/tasks-page.tsx index 4b0f2ff13..75a45c7ac 100644 --- a/client/src/app/pages/tasks/tasks-page.tsx +++ b/client/src/app/pages/tasks/tasks-page.tsx @@ -13,15 +13,7 @@ import { ToolbarContent, ToolbarItem, } from "@patternfly/react-core"; -import { - Table, - Tbody, - Th, - Thead, - Tr, - Td, - ActionsColumn, -} from "@patternfly/react-table"; +import { Table, Tbody, Th, Thead, Tr, Td } from "@patternfly/react-table"; import { CubesIcon } from "@patternfly/react-icons"; import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; @@ -46,9 +38,9 @@ import { Task } from "@app/api/models"; import { IconWithLabel, TaskStateIcon } from "@app/components/Icons"; import { ManageColumnsToolbar } from "../applications/applications-table/components/manage-columns-toolbar"; import dayjs from "dayjs"; -import { useTaskActions } from "./useTaskActions"; import { formatPath } from "@app/utils/utils"; import { Paths } from "@app/Paths"; +import { TaskActionColumn } from "./TaskActionColumn"; export const TasksPage: React.FC = () => { const { t } = useTranslation(); @@ -205,9 +197,6 @@ export const TasksPage: React.FC = () => { filterToolbarProps.setFilterValues({}); }; - const { cancelTask, togglePreemption, canCancel, canTogglePreemption } = - useTaskActions(); - const toCells = ({ id, application, @@ -335,31 +324,7 @@ export const TasksPage: React.FC = () => { isActionCell id={`row-actions-${task.id}`} > - cancelTask(task.id), - }, - { - title: task.policy?.preemptEnabled - ? t("actions.disablePreemption") - : t("actions.enablePreemption"), - isDisabled: !canTogglePreemption(task.state), - onClick: () => togglePreemption(task), - }, - { - title: t("actions.taskDetails"), - onClick: () => - history.push( - formatPath(Paths.taskDetails, { - taskId: task.id, - }) - ), - }, - ]} - /> + diff --git a/client/src/app/pages/tasks/useTaskActions.tsx b/client/src/app/pages/tasks/useTaskActions.tsx index 33b60e34a..bfcd70c28 100644 --- a/client/src/app/pages/tasks/useTaskActions.tsx +++ b/client/src/app/pages/tasks/useTaskActions.tsx @@ -7,6 +7,9 @@ import { import { Task, TaskState } from "@app/api/models"; import { NotificationsContext } from "@app/components/NotificationsContext"; import { useTranslation } from "react-i18next"; +import { useHistory } from "react-router-dom"; +import { formatPath } from "@app/utils/utils"; +import { Paths } from "@app/Paths"; const canCancel = (state: TaskState = "No task") => !["Succeeded", "Failed", "Canceled"].includes(state); @@ -14,7 +17,7 @@ const canCancel = (state: TaskState = "No task") => const canTogglePreemption = (state: TaskState = "No task") => !["Succeeded", "Failed", "Canceled", "Running"].includes(state); -export const useTaskActions = () => { +const useAsyncTaskActions = () => { const { t } = useTranslation(); const { pushNotification } = React.useContext(NotificationsContext); const { mutate: cancelTask } = useCancelTaskMutation( @@ -55,5 +58,35 @@ export const useTaskActions = () => { }, }); - return { cancelTask, togglePreemption, canCancel, canTogglePreemption }; + return { cancelTask, togglePreemption }; +}; + +export const useTaskActions = (task: Task) => { + const { cancelTask, togglePreemption } = useAsyncTaskActions(); + const { t } = useTranslation(); + const history = useHistory(); + + return [ + { + title: t("actions.cancel"), + isDisabled: !canCancel(task.state), + onClick: () => cancelTask(task.id), + }, + { + title: task.policy?.preemptEnabled + ? t("actions.disablePreemption") + : t("actions.enablePreemption"), + isDisabled: !canTogglePreemption(task.state), + onClick: () => togglePreemption(task), + }, + { + title: t("actions.taskDetails"), + onClick: () => + history.push( + formatPath(Paths.taskDetails, { + taskId: task.id, + }) + ), + }, + ]; };