diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index fb35753ffc..f20cca1179 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -55,6 +55,7 @@ "selectNone": "Select none", "selectPage": "Select page", "submitReview": "Submit review", + "taskDetails": "Task details", "unlink": "Unlink", "view": "View", "viewErrorReport": "View error report", @@ -463,6 +464,7 @@ "tagCategory": "Tag category", "tagCategoryDeleted": "Tag category deleted", "tagCategories": "Tag categories", + "tasks": "Tasks", "teamMember": "team member", "terminated": "Terminated", "ticket": "Ticket", @@ -483,7 +485,8 @@ "titles": { "archetypeDrawer": "Archetype details", "taskManager": "Task Manager", - "task": "Task" + "task": "Task", + "taskWithId": "Task {{taskId}}" }, "toastr": { "success": { diff --git a/client/src/app/Paths.ts b/client/src/app/Paths.ts index 7f3551cc86..8bd2e6f1d0 100644 --- a/client/src/app/Paths.ts +++ b/client/src/app/Paths.ts @@ -41,6 +41,8 @@ export const DevPaths = { dependencies: "/dependencies", tasks: "/tasks", + taskDetails: "/tasks/:taskId", + taskDetailsAttachment: "/tasks/:taskId/attachments/:attachmentId", } as const; export type DevPathValues = (typeof DevPaths)[keyof typeof DevPaths]; @@ -102,3 +104,8 @@ export interface AnalysisDetailsAttachmentRoute { taskId: string; attachmentId: string; } + +export interface TaskDetailsAttachmentRoute { + taskId: string; + attachmentId: string; +} diff --git a/client/src/app/Routes.tsx b/client/src/app/Routes.tsx index ba970d2336..64cb995cc2 100644 --- a/client/src/app/Routes.tsx +++ b/client/src/app/Routes.tsx @@ -65,6 +65,7 @@ const AssessmentSummary = lazy( ); const TaskManager = lazy(() => import("./pages/tasks/tasks-page")); +const TaskDetails = lazy(() => import("./pages/tasks/TaskDetails")); export interface IRoute { path: T; @@ -195,6 +196,16 @@ export const devRoutes: IRoute[] = [ { path: Paths.tasks, comp: TaskManager, + exact: true, + }, + { + path: Paths.taskDetails, + comp: TaskDetails, + exact: true, + }, + { + path: Paths.taskDetailsAttachment, + comp: TaskDetails, exact: false, }, ]; diff --git a/client/src/app/pages/applications/analysis-details/AnalysisDetails.tsx b/client/src/app/pages/applications/analysis-details/AnalysisDetails.tsx index ecc51b5e0d..a6361ec673 100644 --- a/client/src/app/pages/applications/analysis-details/AnalysisDetails.tsx +++ b/client/src/app/pages/applications/analysis-details/AnalysisDetails.tsx @@ -1,116 +1,56 @@ import React from "react"; -import { useHistory, useLocation, useParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { PageSection } from "@patternfly/react-core"; - import { AnalysisDetailsAttachmentRoute, Paths } from "@app/Paths"; -import { PageHeader } from "@app/components/PageHeader"; import { formatPath } from "@app/utils/utils"; -import { - DocumentId, - SimpleDocumentViewer, -} from "@app/components/simple-document-viewer"; -import { useFetchApplicationById } from "@app/queries/applications"; -import { useFetchTaskByID } from "@app/queries/tasks"; + import "@app/components/simple-document-viewer/SimpleDocumentViewer.css"; +import { TaskDetailsBase } from "@app/pages/tasks/TaskDetailsBase"; +import { useFetchApplicationById } from "@app/queries/applications"; -export const AnalysisDetails: React.FC = () => { +export const AnalysisDetails = () => { const { t } = useTranslation(); - const { applicationId, taskId, attachmentId } = useParams(); - const { search } = useLocation(); - const hasMergedParam = new URLSearchParams(search).has("merged"); - - const history = useHistory(); - const onDocumentChange = (documentId: DocumentId) => - typeof documentId === "number" - ? history.push( - formatPath(Paths.applicationsAnalysisDetailsAttachment, { - applicationId: applicationId, - taskId: taskId, - attachmentId: documentId, - }) - ) - : history.push({ - pathname: formatPath(Paths.applicationsAnalysisDetails, { - applicationId: applicationId, - taskId: taskId, - }), - search: documentId === "MERGED_VIEW" ? "?merged=true" : undefined, - }); + const detailsPath = formatPath(Paths.applicationsAnalysisDetails, { + applicationId: applicationId, + taskId: taskId, + }); const { application } = useFetchApplicationById(applicationId); - const { task } = useFetchTaskByID(Number(taskId)); - - const taskName = task?.name ?? t("terms.unknown"); const appName: string = application?.name ?? t("terms.unknown"); - const attachmentName = task?.attached?.find( - ({ id }) => String(id) === attachmentId - )?.name; - const resolvedAttachmentId = attachmentName - ? Number(attachmentId) - : undefined; - const resolvedLogMode = hasMergedParam ? "MERGED_VIEW" : "LOG_VIEW"; return ( - <> - - - - -
- -
-
- + `Analysis details for ${taskName}`} + formatAttachmentPath={(attachmentId) => + formatPath(Paths.applicationsAnalysisDetailsAttachment, { + applicationId, + taskId, + attachmentId, + }) + } + taskId={Number(taskId)} + attachmentId={attachmentId} + /> ); }; diff --git a/client/src/app/pages/applications/applications-table/applications-table.tsx b/client/src/app/pages/applications/applications-table/applications-table.tsx index 049c739ddc..457797ddfc 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -319,7 +319,12 @@ export const ApplicationsTable: React.FC = () => { isSortEnabled: true, isPaginationEnabled: true, isActiveItemEnabled: true, - persistTo: { activeItem: "urlParams" }, + persistTo: { + activeItem: "urlParams", + filter: "sessionStorage", + pagination: "sessionStorage", + sort: "sessionStorage", + }, isLoading: isFetchingApplications, sortableColumns: ["name", "businessService", "tags", "effort"], initialSort: { columnKey: "name", direction: "asc" }, diff --git a/client/src/app/pages/tasks/TaskDetails.tsx b/client/src/app/pages/tasks/TaskDetails.tsx new file mode 100644 index 0000000000..86381a285c --- /dev/null +++ b/client/src/app/pages/tasks/TaskDetails.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + +import { Paths, TaskDetailsAttachmentRoute } from "@app/Paths"; +import "@app/components/simple-document-viewer/SimpleDocumentViewer.css"; +import { formatPath } from "@app/utils/utils"; +import { TaskDetailsBase } from "./TaskDetailsBase"; + +export const TaskDetails = () => { + const { t } = useTranslation(); + const { taskId, attachmentId } = useParams(); + const detailsPath = formatPath(Paths.taskDetails, { taskId }); + return ( + `Task details for task ${taskId}, ${taskName}`} + formatAttachmentPath={(attachmentId) => + formatPath(Paths.taskDetailsAttachment, { + taskId, + attachmentId, + }) + } + taskId={Number(taskId)} + attachmentId={attachmentId} + /> + ); +}; + +export default TaskDetails; diff --git a/client/src/app/pages/tasks/TaskDetailsBase.tsx b/client/src/app/pages/tasks/TaskDetailsBase.tsx new file mode 100644 index 0000000000..27b30eb1bb --- /dev/null +++ b/client/src/app/pages/tasks/TaskDetailsBase.tsx @@ -0,0 +1,96 @@ +import React from "react"; +import { useHistory, useLocation } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + +import { PageSection } from "@patternfly/react-core"; + +import { PageHeader, PageHeaderProps } from "@app/components/PageHeader"; +import { + DocumentId, + SimpleDocumentViewer, +} from "@app/components/simple-document-viewer"; +import { useFetchTaskByID } from "@app/queries/tasks"; +import "@app/components/simple-document-viewer/SimpleDocumentViewer.css"; + +export const TaskDetailsBase: React.FC<{ + breadcrumbs: PageHeaderProps["breadcrumbs"]; + formatTitle: (taskName: string) => string; + detailsPath: string; + formatAttachmentPath: (attachmentId: number | string) => string; + taskId: number; + attachmentId?: string; +}> = ({ + breadcrumbs, + formatTitle, + detailsPath, + formatAttachmentPath, + taskId, + attachmentId, +}) => { + const { t } = useTranslation(); + + const { search } = useLocation(); + const hasMergedParam = new URLSearchParams(search).has("merged"); + + const history = useHistory(); + const onDocumentChange = (documentId: DocumentId) => + typeof documentId === "number" + ? history.push(formatAttachmentPath(documentId)) + : history.push({ + pathname: detailsPath, + search: documentId === "MERGED_VIEW" ? "?merged=true" : undefined, + }); + + const { task } = useFetchTaskByID(taskId); + + const taskName: string = task?.name ?? t("terms.unknown"); + const attachmentName = task?.attached?.find( + ({ id }) => String(id) === attachmentId + )?.name; + const resolvedAttachmentId = attachmentName + ? Number(attachmentId) + : undefined; + const resolvedLogMode = hasMergedParam ? "MERGED_VIEW" : "LOG_VIEW"; + + return ( + <> + + + + +
+ +
+
+ + ); +}; diff --git a/client/src/app/pages/tasks/tasks-page.tsx b/client/src/app/pages/tasks/tasks-page.tsx index 1b178f149f..915deccbcb 100644 --- a/client/src/app/pages/tasks/tasks-page.tsx +++ b/client/src/app/pages/tasks/tasks-page.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { useHistory } from "react-router-dom"; +import { Link, useHistory } from "react-router-dom"; import { EmptyState, EmptyStateHeader, @@ -47,6 +47,8 @@ import { IconWithLabel, taskStateToIcon } 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"; export const TasksPage: React.FC = () => { const { t } = useTranslation(); @@ -59,7 +61,11 @@ export const TasksPage: React.FC = () => { const tableControlState = useTableControlState({ tableName: "tasks-table", - persistTo: { filter: "urlParams" }, + persistTo: { + filter: "urlParams", + pagination: "sessionStorage", + sort: "sessionStorage", + }, persistenceKeyPrefix: TablePersistenceKeyPrefix.tasks, columnNames: { id: "ID", @@ -199,7 +205,8 @@ export const TasksPage: React.FC = () => { filterToolbarProps.setFilterValues({}); }; - const { cancelTask, togglePreemption } = useTaskActions(); + const { cancelTask, togglePreemption, canCancel, canTogglePreemption } = + useTaskActions(); const toCells = ({ id, @@ -218,7 +225,18 @@ export const TasksPage: React.FC = () => { application: application.name, kind: kind ?? addon, state: ( - + + {state ?? "No task"} + + } + /> ), priority, preemption: String(!!policy?.preemptEnabled), @@ -316,23 +334,28 @@ export const TasksPage: React.FC = () => { id={`row-actions-${task.id}`} > cancelTask(task.id), }, { title: task.policy?.preemptEnabled ? t("actions.disablePreemption") : t("actions.enablePreemption"), - isDisabled: "Running" === task.state, + 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 3d29aca495..33b60e34ab 100644 --- a/client/src/app/pages/tasks/useTaskActions.tsx +++ b/client/src/app/pages/tasks/useTaskActions.tsx @@ -4,10 +4,16 @@ import { useCancelTaskMutation, useUpdateTaskMutation, } from "@app/queries/tasks"; -import { Task } from "@app/api/models"; +import { Task, TaskState } from "@app/api/models"; import { NotificationsContext } from "@app/components/NotificationsContext"; import { useTranslation } from "react-i18next"; +const canCancel = (state: TaskState = "No task") => + !["Succeeded", "Failed", "Canceled"].includes(state); + +const canTogglePreemption = (state: TaskState = "No task") => + !["Succeeded", "Failed", "Canceled", "Running"].includes(state); + export const useTaskActions = () => { const { t } = useTranslation(); const { pushNotification } = React.useContext(NotificationsContext); @@ -49,5 +55,5 @@ export const useTaskActions = () => { }, }); - return { cancelTask, togglePreemption }; + return { cancelTask, togglePreemption, canCancel, canTogglePreemption }; };