From cae184a944d936c3b530ac2d3abc0feaebd0cd30 Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Thu, 11 Jul 2024 10:00:01 -0400 Subject: [PATCH 1/8] Starting work on #1997 Signed-off-by: Scott J Dickerson --- client/public/locales/en/translation.json | 3 ++ client/src/app/api/models.ts | 3 +- .../app/components/Icons/TaskStateIcon.tsx | 7 +++ client/src/app/pages/tasks/tasks-page.tsx | 19 +++++++- client/src/app/queries/tasks.ts | 43 +++++++++++++------ 5 files changed, 59 insertions(+), 16 deletions(-) diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index 75b620e039..c06af21b3f 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -260,6 +260,9 @@ "dependencies": "Dependencies", "tasks": "Task Manager" }, + "taskState": { + "NoTask": "No Task" + }, "terms": { "accepted": "Accepted", "acceptedAppsAndDeps": "Accepted applications and dependencies", diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 635aa59886..6ccad4e4a9 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -304,7 +304,8 @@ export type TaskState = | "QuotaBlocked" | "Ready" | "Pending" - | "Postponed"; + | "Postponed" + | "SucceededWithErrors"; // synthetic state for ease-of-use in UI; export interface Task { id: number; diff --git a/client/src/app/components/Icons/TaskStateIcon.tsx b/client/src/app/components/Icons/TaskStateIcon.tsx index e78e30f343..4a481f5b7f 100644 --- a/client/src/app/components/Icons/TaskStateIcon.tsx +++ b/client/src/app/components/Icons/TaskStateIcon.tsx @@ -6,6 +6,7 @@ import { TimesCircleIcon, InProgressIcon, ExclamationCircleIcon, + ExclamationTriangleIcon, UnknownIcon, PendingIcon, TaskIcon, @@ -29,6 +30,12 @@ export const TaskStateIcon: FC<{ state?: TaskState }> = ({ state }) => { ); + case "SucceededWithErrors": + return ( + + + + ); case "Failed": return ( diff --git a/client/src/app/pages/tasks/tasks-page.tsx b/client/src/app/pages/tasks/tasks-page.tsx index 915c5f6ad3..e0d95bec6c 100644 --- a/client/src/app/pages/tasks/tasks-page.tsx +++ b/client/src/app/pages/tasks/tasks-page.tsx @@ -42,7 +42,7 @@ import { TablePersistenceKeyPrefix } from "@app/Constants"; import { useSelectionState } from "@migtools/lib-ui"; import { useServerTasks } from "@app/queries/tasks"; -import { Task } from "@app/api/models"; +import { Task, TaskState } 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"; @@ -50,6 +50,21 @@ import { formatPath } from "@app/utils/utils"; import { Paths } from "@app/Paths"; import { TaskActionColumn } from "./TaskActionColumn"; +const taskStateToLabel: Record = { + "No task": "taskState.NoTask", + "not supported": "", + Canceled: "Canceled", + Created: "Created", + Succeeded: "Succeeded", + Failed: "Failed", + Running: "Running", + QuotaBlocked: "Quota Blocked", + Ready: "Ready", + Pending: "Pending", + Postponed: "Postponed", + SucceededWithErrors: "Succeeded with Errors", +}; + export const TasksPage: React.FC = () => { const { t } = useTranslation(); const history = useHistory(); @@ -235,7 +250,7 @@ export const TasksPage: React.FC = () => { taskId: id, })} > - {state ?? "No task"} + {t(taskStateToLabel[state ?? "No task"])} } /> diff --git a/client/src/app/queries/tasks.ts b/client/src/app/queries/tasks.ts index 512e13fc3d..84967f174a 100644 --- a/client/src/app/queries/tasks.ts +++ b/client/src/app/queries/tasks.ts @@ -25,27 +25,36 @@ export const TaskStates = { Failed: ["Failed"], Queued: ["Ready", "Postponed", "Pending", "Running"], // "Created", "QuotaBlocked" ?? Running: ["Running"], - Success: ["Succeeded"], + Success: ["Succeeded", "SucceededWithErrors"], }; export const TasksQueryKey = "tasks"; -export const TasksQueueKey = "TasksQueue"; +export const TasksPagedQueryKey = "tasksPaged"; +export const TasksQueueKey = "tasksQueue"; export const TaskByIDQueryKey = "taskByID"; export const TaskAttachmentByIDQueryKey = "taskAttachmentByID"; +/** + * Rebuild the __state__ of a Task to include the UI synthetic "SucceededWithErrors" + */ +const calculateSyntheticState = (task: Task): Task => { + if (task.state === "Succeeded" && (task.errors?.length ?? 0) > 0) { + task.state = "SucceededWithErrors"; + } + + return task; +}; + export const useFetchTasks = (refetchDisabled: boolean = false) => { const { isLoading, error, refetch, data } = useQuery({ queryKey: [TasksQueryKey], queryFn: getTasks, - refetchInterval: !refetchDisabled ? 5000 : false, - select: (allTasks) => { - // sort by createTime (newest to oldest) - allTasks.sort( - (a, b) => -1 * universalComparator(a.createTime, b.createTime) - ); - return allTasks; - }, + select: (tasks) => + tasks + .map(calculateSyntheticState) + .sort((a, b) => -1 * universalComparator(a.createTime, b.createTime)), onError: (err) => console.log(err), + refetchInterval: !refetchDisabled ? 5000 : false, }); const hasActiveTasks = @@ -61,13 +70,19 @@ export const useFetchTasks = (refetchDisabled: boolean = false) => { }; export const useServerTasks = ( - params: HubRequestParams = {}, + params: HubRequestParams, refetchInterval?: number ) => { const { data, isLoading, error, refetch } = useQuery({ - queryKey: [TasksQueryKey, params], + queryKey: [TasksPagedQueryKey, params], queryFn: async () => await getServerTasks(params), - onError: (error) => console.log("error, ", error), + select: (data) => { + if (data?.data?.length > 0) { + data.data = data.data.map(calculateSyntheticState); + } + return data; + }, + onError: (error: Error) => console.log("error, ", error), keepPreviousData: true, refetchInterval: refetchInterval ?? false, }); @@ -184,6 +199,8 @@ export const useFetchTaskByID = (taskId?: number) => { const { isLoading, error, data, refetch } = useQuery({ queryKey: [TaskByIDQueryKey, taskId], queryFn: () => (taskId ? getTaskById(taskId) : null), + select: (task: Task | null) => + task === null ? null : calculateSyntheticState(task), enabled: !!taskId, }); From 363b1e59ef61fca0121b2aed148d4542a5fd22dd Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Mon, 15 Jul 2024 16:40:14 -0400 Subject: [PATCH 2/8] Finish string work on tasks page Signed-off-by: Scott J Dickerson --- client/public/locales/en/translation.json | 12 +++++++++++- client/src/app/pages/tasks/tasks-page.tsx | 20 ++++++++++---------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index c06af21b3f..b694fdd5ce 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -261,7 +261,17 @@ "tasks": "Task Manager" }, "taskState": { - "NoTask": "No Task" + "Canceled": "Canceled", + "Created": "Created", + "Failed": "Failed", + "NoTask": "No Task", + "Pending": "Pending", + "Postponed": "Postponed", + "QuotaBlocked": "Quota Blocked", + "Ready": "Ready", + "Running": "Running", + "Succeeded": "Succeeded", + "SucceededWithErrors": "Succeeded with Errors" }, "terms": { "accepted": "Accepted", diff --git a/client/src/app/pages/tasks/tasks-page.tsx b/client/src/app/pages/tasks/tasks-page.tsx index e0d95bec6c..d4452dbb52 100644 --- a/client/src/app/pages/tasks/tasks-page.tsx +++ b/client/src/app/pages/tasks/tasks-page.tsx @@ -53,16 +53,16 @@ import { TaskActionColumn } from "./TaskActionColumn"; const taskStateToLabel: Record = { "No task": "taskState.NoTask", "not supported": "", - Canceled: "Canceled", - Created: "Created", - Succeeded: "Succeeded", - Failed: "Failed", - Running: "Running", - QuotaBlocked: "Quota Blocked", - Ready: "Ready", - Pending: "Pending", - Postponed: "Postponed", - SucceededWithErrors: "Succeeded with Errors", + Canceled: "taskState.Canceled", + Created: "taskState.Created", + Succeeded: "taskState.Succeeded", + Failed: "taskState.Failed", + Running: "taskState.Running", + QuotaBlocked: "taskState.QuotaBlocked", + Ready: "taskState.Ready", + Pending: "taskState.Pending", + Postponed: "taskState.Postponed", + SucceededWithErrors: "taskState.SucceededWithErrors", }; export const TasksPage: React.FC = () => { From f0f1984acf7518851cde9abe6b582b8153d7f8f0 Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Mon, 15 Jul 2024 17:11:23 -0400 Subject: [PATCH 3/8] Completed with errors in analysis column Signed-off-by: Scott J Dickerson --- client/public/locales/en/translation.json | 1 + .../src/app/components/Icons/IconedStatus.tsx | 22 ++++++++++---- .../application-analysis-status.tsx | 29 +++++++------------ 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index b694fdd5ce..8ee495059e 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -325,6 +325,7 @@ "comments": "Comments", "commentsFromApplication": "Application comments", "completed": "Completed", + "completedWithErrors": "Completed with Errors", "confidence": "Confidence", "connected": "Connected", "contributors": "Contributors", diff --git a/client/src/app/components/Icons/IconedStatus.tsx b/client/src/app/components/Icons/IconedStatus.tsx index 6179f95b25..984031d995 100644 --- a/client/src/app/components/Icons/IconedStatus.tsx +++ b/client/src/app/components/Icons/IconedStatus.tsx @@ -1,15 +1,19 @@ import React from "react"; import { Icon } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; -import CheckCircleIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon"; -import TimesCircleIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon"; -import InProgressIcon from "@patternfly/react-icons/dist/esm/icons/in-progress-icon"; -import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon"; -import UnknownIcon from "@patternfly/react-icons/dist/esm/icons/unknown-icon"; -import TopologyIcon from "@patternfly/react-icons/dist/esm/icons/topology-icon"; import { IconWithLabel } from "./IconWithLabel"; import { ReactElement } from "react-markdown/lib/react-markdown"; +import { + CheckCircleIcon, + TimesCircleIcon, + InProgressIcon, + ExclamationCircleIcon, + ExclamationTriangleIcon, + UnknownIcon, + TopologyIcon, +} from "@patternfly/react-icons"; + export type IconedStatusPreset = | "InheritedReviews" | "InProgressInheritedReviews" @@ -17,6 +21,7 @@ export type IconedStatusPreset = | "InheritedAssessments" | "Canceled" | "Completed" + | "CompletedWithErrors" | "Error" | "Failed" | "InProgress" @@ -104,6 +109,11 @@ export const IconedStatus: React.FC = ({ status: "success", label: t("terms.completed"), }, + CompletedWithErrors: { + icon: , + status: "warning", + label: t("terms.completedWithErrors"), + }, Error: { icon: , status: "danger", diff --git a/client/src/app/pages/applications/components/application-analysis-status.tsx b/client/src/app/pages/applications/components/application-analysis-status.tsx index 5759c4293d..4462aacaff 100644 --- a/client/src/app/pages/applications/components/application-analysis-status.tsx +++ b/client/src/app/pages/applications/components/application-analysis-status.tsx @@ -1,25 +1,18 @@ import React from "react"; import { TaskState } from "@app/api/models"; -import { IconedStatus } from "@app/components/Icons"; +import { IconedStatus, IconedStatusPreset } from "@app/components/Icons"; export interface ApplicationAnalysisStatusProps { state: TaskState; } -export type AnalysisState = - | "Canceled" - | "Scheduled" - | "Completed" - | "Failed" - | "InProgress" - | "NotStarted"; - -const taskStateToAnalyze: Map = new Map([ +const taskStateToAnalyze: Map = new Map([ ["not supported", "Canceled"], ["Canceled", "Canceled"], ["Created", "Scheduled"], ["Succeeded", "Completed"], + ["SucceededWithErrors", "CompletedWithErrors"], ["Failed", "Failed"], ["Running", "InProgress"], ["No task", "NotStarted"], @@ -28,16 +21,16 @@ const taskStateToAnalyze: Map = new Map([ ["Ready", "Scheduled"], ]); +const getTaskStatus = (state: TaskState): IconedStatusPreset => { + if (taskStateToAnalyze.has(state)) { + const value = taskStateToAnalyze.get(state); + if (value) return value; + } + return "NotStarted"; +}; + export const ApplicationAnalysisStatus: React.FC< ApplicationAnalysisStatusProps > = ({ state }) => { - const getTaskStatus = (state: TaskState): AnalysisState => { - if (taskStateToAnalyze.has(state)) { - const value = taskStateToAnalyze.get(state); - if (value) return value; - } - return "NotStarted"; - }; - return ; }; From 8cfefae4c4fa7391198b6b85370affce6ff8a7e1 Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Mon, 15 Jul 2024 17:20:34 -0400 Subject: [PATCH 4/8] code review change Signed-off-by: Scott J Dickerson --- client/src/app/queries/tasks.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/app/queries/tasks.ts b/client/src/app/queries/tasks.ts index 84967f174a..4bc393ccd3 100644 --- a/client/src/app/queries/tasks.ts +++ b/client/src/app/queries/tasks.ts @@ -29,7 +29,6 @@ export const TaskStates = { }; export const TasksQueryKey = "tasks"; -export const TasksPagedQueryKey = "tasksPaged"; export const TasksQueueKey = "tasksQueue"; export const TaskByIDQueryKey = "taskByID"; export const TaskAttachmentByIDQueryKey = "taskAttachmentByID"; @@ -74,7 +73,7 @@ export const useServerTasks = ( refetchInterval?: number ) => { const { data, isLoading, error, refetch } = useQuery({ - queryKey: [TasksPagedQueryKey, params], + queryKey: [TasksQueryKey, params], queryFn: async () => await getServerTasks(params), select: (data) => { if (data?.data?.length > 0) { @@ -200,7 +199,7 @@ export const useFetchTaskByID = (taskId?: number) => { queryKey: [TaskByIDQueryKey, taskId], queryFn: () => (taskId ? getTaskById(taskId) : null), select: (task: Task | null) => - task === null ? null : calculateSyntheticState(task), + !task ? null : calculateSyntheticState(task), enabled: !!taskId, }); From 594920fb5e7d1f2699636805d88c439ef42b6e72 Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Mon, 15 Jul 2024 17:33:35 -0400 Subject: [PATCH 5/8] Add SucceededWithErrors for the ApplicationTasksStatus Signed-off-by: Scott J Dickerson --- .../components/column-application-name.tsx | 10 ++++++++++ .../applications-table/useDecoratedApplications.ts | 9 ++++++++- client/src/app/queries/tasks.ts | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/client/src/app/pages/applications/applications-table/components/column-application-name.tsx b/client/src/app/pages/applications/applications-table/components/column-application-name.tsx index 1b1cf2adda..b10a295861 100644 --- a/client/src/app/pages/applications/applications-table/components/column-application-name.tsx +++ b/client/src/app/pages/applications/applications-table/components/column-application-name.tsx @@ -7,6 +7,7 @@ import { TimesCircleIcon, InProgressIcon, ExclamationCircleIcon, + ExclamationTriangleIcon, PendingIcon, } from "@patternfly/react-icons"; @@ -79,6 +80,15 @@ const statusMap: Record = { ), }, + SuccessWithErrors: { + popoverVariant: "warning", + headerText: "All tasks succeeded, but some errors occurred", + icon: () => ( + + + + ), + }, }; const linkToTasks = (applicationName: string) => { diff --git a/client/src/app/pages/applications/applications-table/useDecoratedApplications.ts b/client/src/app/pages/applications/applications-table/useDecoratedApplications.ts index 2cf66dae1e..fc46eae3b9 100644 --- a/client/src/app/pages/applications/applications-table/useDecoratedApplications.ts +++ b/client/src/app/pages/applications/applications-table/useDecoratedApplications.ts @@ -19,7 +19,8 @@ export type ApplicationTasksStatus = | "Queued" | "Failed" | "Canceled" - | "Success"; + | "Success" + | "SuccessWithErrors"; export interface DecoratedApplication extends Application { /** reference to the Application being decorated */ @@ -33,6 +34,7 @@ export interface DecoratedApplication extends Application { latestHasQueued: boolean; latestHasRunning: boolean; latestHasSuccess: boolean; + latestHasSuccessWithErrors: boolean; /** The most recently created `kind === "analyzer"` task for the application */ currentAnalyzer: Task | undefined; @@ -83,6 +85,8 @@ const chooseApplicationTaskStatus = ({ ? "Failed" : tasks.latestHasCanceled ? "Canceled" + : tasks.latestHasSuccessWithErrors + ? "SuccessWithErrors" : "Success"; }; @@ -125,6 +129,9 @@ const decorateApplications = ( latestHasSuccess: latest.some((task) => TaskStates.Success.includes(task.state ?? "") ), + latestHasSuccessWithErrors: latest.some((task) => + TaskStates.SuccessWithErrors.includes(task.state ?? "") + ), currentAnalyzer: tasksByKind["analyzer"]?.[0], }, diff --git a/client/src/app/queries/tasks.ts b/client/src/app/queries/tasks.ts index 4bc393ccd3..1f95581d69 100644 --- a/client/src/app/queries/tasks.ts +++ b/client/src/app/queries/tasks.ts @@ -26,6 +26,7 @@ export const TaskStates = { Queued: ["Ready", "Postponed", "Pending", "Running"], // "Created", "QuotaBlocked" ?? Running: ["Running"], Success: ["Succeeded", "SucceededWithErrors"], + SuccessWithErrors: ["SucceededWithErrors"], }; export const TasksQueryKey = "tasks"; From 91f02097f6ba8275990ae44ffc4e398df2a12bb8 Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Mon, 15 Jul 2024 18:47:21 -0400 Subject: [PATCH 6/8] Updated testing snapshot Signed-off-by: Scott J Dickerson --- .../__snapshots__/StatusIcon.test.tsx.snap | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/client/src/app/components/tests/__snapshots__/StatusIcon.test.tsx.snap b/client/src/app/components/tests/__snapshots__/StatusIcon.test.tsx.snap index 4a5443690b..d623a0f8a3 100644 --- a/client/src/app/components/tests/__snapshots__/StatusIcon.test.tsx.snap +++ b/client/src/app/components/tests/__snapshots__/StatusIcon.test.tsx.snap @@ -17,7 +17,19 @@ exports[`StatusIcon Renders without crashing 1`] = ` - + @@ -42,7 +54,19 @@ exports[`StatusIcon Renders without crashing 1`] = ` - + From 849a2eca41257b69f1903076404004f1ba8eb6cc Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Tue, 16 Jul 2024 16:22:00 -0400 Subject: [PATCH 7/8] Use the /tasks/report/dashboard endpoint Also refactor some components to: - push data access closer to where it is used - break into smaller manageable components Signed-off-by: Scott J Dickerson --- client/src/app/api/models.ts | 18 + client/src/app/api/rest.ts | 7 +- .../applications-table/applications-table.tsx | 27 +- .../components/column-application-name.tsx | 12 +- .../useDecoratedApplications.ts | 14 +- .../application-detail-drawer.tsx | 721 ++++++++++-------- client/src/app/queries/archetypes.ts | 7 +- client/src/app/queries/tasks.ts | 30 +- 8 files changed, 453 insertions(+), 383 deletions(-) diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 6ccad4e4a9..98ffa8fdfc 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -335,6 +335,24 @@ export interface Task { attached?: TaskAttachment[]; } +/** A smaller version of `Task` fetched from the report/dashboard endpoint. */ +export interface TaskDashboard { + id: number; + createUser: string; + updateUser: string; + createTime: string; // ISO-8601 + name: string; + kind?: string; + addon?: string; + state: TaskState; + application: Ref; + started?: string; // ISO-8601 + terminated?: string; // ISO-8601 + + /** Count of errors recorded on the task - even Succeeded tasks may have errors. */ + errors?: number; +} + export interface TaskPolicy { isolated?: boolean; preemptEnabled?: boolean; diff --git a/client/src/app/api/rest.ts b/client/src/app/api/rest.ts index 232d28f50e..542507b07b 100644 --- a/client/src/app/api/rest.ts +++ b/client/src/app/api/rest.ts @@ -42,6 +42,7 @@ import { Task, Taskgroup, TaskQueue, + TaskDashboard, Ticket, Tracker, TrackerProject, @@ -357,8 +358,10 @@ export function getTaskByIdAndFormat( }); } -export const getTasks = () => - axios.get(TASKS).then((response) => response.data); +export const getTasksDashboard = () => + axios + .get(`${TASKS}/report/dashboard`) + .then((response) => response.data); export const getServerTasks = (params: HubRequestParams = {}) => getHubPaginatedResult(TASKS, params); 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 51e5f2cee3..f3b7c7baf1 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -75,14 +75,13 @@ import { useBulkDeleteApplicationMutation, useFetchApplications, } from "@app/queries/applications"; -import { useCancelTaskMutation, useFetchTasks } from "@app/queries/tasks"; import { - useDeleteAssessmentMutation, - useFetchAssessments, -} from "@app/queries/assessments"; + useCancelTaskMutation, + useFetchTaskDashboard, +} from "@app/queries/tasks"; +import { useDeleteAssessmentMutation } from "@app/queries/assessments"; import { useDeleteReviewMutation } from "@app/queries/reviews"; import { useFetchTagsWithTagItems } from "@app/queries/tags"; -import { useFetchArchetypes } from "@app/queries/archetypes"; // Relative components import { AnalysisWizard } from "../analysis-wizard/analysis-wizard"; @@ -182,7 +181,7 @@ export const ApplicationsTable: React.FC = () => { // ----- Table data fetches and mutations const { tagItems } = useFetchTagsWithTagItems(); - const { tasks, hasActiveTasks } = useFetchTasks(isAnalyzeModalOpen); + const { tasks, hasActiveTasks } = useFetchTaskDashboard(isAnalyzeModalOpen); const completedCancelTask = () => { pushNotification({ @@ -231,11 +230,6 @@ export const ApplicationsTable: React.FC = () => { referencedBusinessServiceRefs, } = useDecoratedApplications(baseApplications, tasks); - const { assessments, isFetching: isFetchingAssessments } = - useFetchAssessments(); - - const { archetypes, isFetching: isFetchingArchetypes } = useFetchArchetypes(); - const onDeleteApplicationSuccess = (appIDCount: number) => { pushNotification({ title: t("toastr.success.applicationDeleted", { @@ -913,11 +907,7 @@ export const ApplicationsTable: React.FC = () => { > @@ -1067,12 +1057,9 @@ export const ApplicationsTable: React.FC = () => { setSaveApplicationModalState(activeItem)} - task={activeItem ? activeItem.tasks.currentAnalyzer : null} + task={activeItem?.tasks?.currentAnalyzer ?? null} /> diff --git a/client/src/app/pages/applications/applications-table/components/column-application-name.tsx b/client/src/app/pages/applications/applications-table/components/column-application-name.tsx index b10a295861..627ca871e6 100644 --- a/client/src/app/pages/applications/applications-table/components/column-application-name.tsx +++ b/client/src/app/pages/applications/applications-table/components/column-application-name.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Link } from "react-router-dom"; +import dayjs from "dayjs"; import { Icon, Popover, PopoverProps, Tooltip } from "@patternfly/react-core"; import { @@ -10,17 +11,16 @@ import { ExclamationTriangleIcon, PendingIcon, } from "@patternfly/react-icons"; +import { Table, Tbody, Td, Thead, Tr } from "@patternfly/react-table"; import { IconWithLabel, TaskStateIcon } from "@app/components/Icons"; +import { Paths } from "@app/Paths"; +import { formatPath, universalComparator } from "@app/utils/utils"; +import { TaskDashboard } from "@app/api/models"; import { ApplicationTasksStatus, DecoratedApplication, } from "../useDecoratedApplications"; -import { Paths } from "@app/Paths"; -import { formatPath, universalComparator } from "@app/utils/utils"; -import dayjs from "dayjs"; -import { Table, Tbody, Td, Thead, Tr } from "@patternfly/react-table"; -import { Task } from "@app/api/models"; interface StatusData { popoverVariant: PopoverProps["alertSeverityVariant"]; @@ -96,7 +96,7 @@ const linkToTasks = (applicationName: string) => { return `${formatPath(Paths.tasks, {})}?${search}`; }; -const linkToDetails = (task: Task) => { +const linkToDetails = (task: TaskDashboard) => { return formatPath(Paths.taskDetails, { taskId: task.id, }); diff --git a/client/src/app/pages/applications/applications-table/useDecoratedApplications.ts b/client/src/app/pages/applications/applications-table/useDecoratedApplications.ts index fc46eae3b9..c9a3716052 100644 --- a/client/src/app/pages/applications/applications-table/useDecoratedApplications.ts +++ b/client/src/app/pages/applications/applications-table/useDecoratedApplications.ts @@ -1,12 +1,12 @@ import { useMemo } from "react"; -import { Application, Identity, Task } from "@app/api/models"; +import { Application, Identity, TaskDashboard } from "@app/api/models"; import { group, listify, mapEntries, unique } from "radash"; import { TaskStates } from "@app/queries/tasks"; import { universalComparator } from "@app/utils/utils"; import { useFetchIdentities } from "@app/queries/identities"; export interface TasksGroupedByKind { - [key: string]: Task[]; + [key: string]: TaskDashboard[]; } /** @@ -37,7 +37,7 @@ export interface DecoratedApplication extends Application { latestHasSuccessWithErrors: boolean; /** The most recently created `kind === "analyzer"` task for the application */ - currentAnalyzer: Task | undefined; + currentAnalyzer: TaskDashboard | undefined; }; tasksStatus: ApplicationTasksStatus; @@ -50,10 +50,10 @@ export interface DecoratedApplication extends Application { /** * Take an array of `Tasks`, group by application id and then by task kind. */ -const groupTasks = (tasks: Task[]) => { +const groupTasks = (tasks: TaskDashboard[]) => { const byApplicationId = group(tasks, (task) => task.application.id) as Record< number, - Task[] + TaskDashboard[] >; const groupedByIdByKind = mapEntries(byApplicationId, (id, tasks) => [ @@ -96,7 +96,7 @@ const chooseApplicationTaskStatus = ({ */ const decorateApplications = ( applications: Application[], - tasks: Task[], + tasks: TaskDashboard[], identities: Identity[] ) => { const { tasksById, tasksByIdByKind } = groupTasks(tasks); @@ -152,7 +152,7 @@ const decorateApplications = ( export const useDecoratedApplications = ( applications: Application[], - tasks: Task[] + tasks: TaskDashboard[] ) => { const { identities } = useFetchIdentities(); diff --git a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx index 23ab731159..e0356ffd65 100644 --- a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx +++ b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx @@ -29,13 +29,11 @@ import CheckCircleIcon from "@patternfly/react-icons/dist/esm/icons/check-circle import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon"; import { - Application, Identity, - Task, MimeType, Ref, Archetype, - AssessmentWithSectionOrder, + TaskDashboard, } from "@app/api/models"; import { COLOR_HEX_VALUES_BY_NAME } from "@app/Constants"; import { useFetchFacts } from "@app/queries/facts"; @@ -63,14 +61,14 @@ import { ApplicationDetailFields } from "./application-detail-fields"; import { ApplicationFacts } from "./application-facts"; import { formatPath } from "@app/utils/utils"; import { Paths } from "@app/Paths"; +import { useFetchArchetypes } from "@app/queries/archetypes"; +import { useFetchAssessments } from "@app/queries/assessments"; +import { DecoratedApplication } from "../../applications-table/useDecoratedApplications"; export interface IApplicationDetailDrawerProps extends Pick { - application: Application | null; - task: Task | undefined | null; - applications?: Application[]; - assessments?: AssessmentWithSectionOrder[]; - archetypes?: Archetype[]; + application: DecoratedApplication | null; + task: TaskDashboard | null; onEditClick: () => void; } @@ -84,54 +82,12 @@ enum TabKey { export const ApplicationDetailDrawer: React.FC< IApplicationDetailDrawerProps -> = ({ - onCloseClick, - application, - assessments, - archetypes, - task, - onEditClick, -}) => { +> = ({ application, task, onCloseClick, onEditClick }) => { const { t } = useTranslation(); const [activeTabKey, setActiveTabKey] = React.useState( TabKey.Details ); - const isTaskRunning = task?.state === "Running"; - - const { identities } = useFetchIdentities(); - const { facts, isFetching } = useFetchFacts(application?.id); - - let matchingSourceCredsRef: Identity | undefined; - let matchingMavenCredsRef: Identity | undefined; - if (application && identities) { - matchingSourceCredsRef = getKindIdByRef(identities, application, "source"); - matchingMavenCredsRef = getKindIdByRef(identities, application, "maven"); - } - - const notAvailable = ; - - const enableDownloadSetting = useSetting("download.html.enabled"); - const history = useHistory(); - const navigateToAnalysisDetails = () => - application?.id && - task?.id && - history.push( - formatPath(Paths.applicationsAnalysisDetails, { - applicationId: application?.id, - taskId: task?.id, - }) - ); - - const reviewedArchetypes = - application?.archetypes - ?.map( - (archetypeRef) => - archetypes?.find((archetype) => archetype.id === archetypeRef.id) - ) - .filter((fullArchetype) => fullArchetype?.review) - .filter(Boolean) || []; - return (
+ {/* this div is required so the tabs are visible */} setActiveTabKey(tabKey as TabKey)} className={spacing.mtLg} > - {t("terms.details")}} - > - - {application?.description} - - {application ? ( - <> - - - Issues - - - - - Dependencies - - - - ) : null} - - - {t("terms.effort")} - - - - {application?.effort !== 0 && - application?.effort !== undefined - ? application?.effort - : t("terms.unassigned")} - - - + {!application ? null : ( + {t("terms.details")}} + > + + + )} + + {!application ? null : ( + Tags} + > + + + )} + + {!application ? null : ( + {t("terms.reports")}} + > + + + )} + + {!application ? null : ( + {t("terms.review")}} + > + + + )} + +
+
+ ); +}; +const ArchetypeLabels: React.FC<{ archetypeRefs?: Ref[] }> = ({ + archetypeRefs, +}) => ; + +const ArchetypeItem: React.FC<{ archetype: Archetype }> = ({ archetype }) => { + return ; +}; + +const TabDetailsContent: React.FC<{ + application: DecoratedApplication; + onCloseClick: () => void; + onEditClick: () => void; +}> = ({ application, onCloseClick, onEditClick }) => { + const { t } = useTranslation(); + + const { assessments } = useFetchAssessments(); + + const { archetypesById } = useFetchArchetypes(); + const reviewedArchetypes = !application?.archetypes + ? [] + : application.archetypes + .map((archetypeRef) => archetypesById[archetypeRef.id]) + .filter((fullArchetype) => fullArchetype?.review) + .filter(Boolean); + + return ( + <> + + {application?.description} + + + {application ? ( <> - - {t("terms.archetypes")} - - - - - {t("terms.associatedArchetypes")} - - - {application?.archetypes?.length ? ( - <> - - {application.archetypes.length ?? 0 > 0 ? ( - - ) : ( - - )} - - - ) : ( - - )} - - - - - - {t("terms.archetypesAssessed")} - - {assessments && assessments.length ? ( - - - + + + Issues + + + + + Dependencies + + + + ) : null} + + + + {t("terms.effort")} + + + + {application?.effort !== 0 && application?.effort !== undefined + ? application?.effort + : t("terms.unassigned")} + + + + + + {t("terms.archetypes")} + + + + + {t("terms.associatedArchetypes")} + + + {application?.archetypes?.length ? ( + <> + + {application.archetypes.length ?? 0 > 0 ? ( + ) : ( )} - - - - - {t("terms.archetypesReviewed")} - - - - {reviewedArchetypes?.length ? ( - reviewedArchetypes.map((reviewedArchetype) => ( - - )) - ) : ( - - )} - - - - - - - {t("terms.riskFromApplication")} - - - - - - + + ) : ( + + )} + + + + + + {t("terms.archetypesAssessed")} + + {(assessments?.length ?? 0) > 0 ? ( + + - - - - Tags}> - {application && isTaskRunning ? ( - - - - {t("message.taskInProgressForTags")} - - - - - ) : null} - - {application ? : null} - - - {t("terms.reports")}} - > - - - Credentials - - {matchingSourceCredsRef && matchingMavenCredsRef ? ( - - - Source and Maven - - ) : matchingMavenCredsRef ? ( - - - Maven - - ) : matchingSourceCredsRef ? ( - - - Source - + + ) : ( + + )} + + + + + {t("terms.archetypesReviewed")} + + + + {reviewedArchetypes?.length ? ( + reviewedArchetypes.map((reviewedArchetype) => ( + + )) ) : ( - notAvailable + )} - - Analysis - - {task?.state === "Succeeded" && application ? ( - <> - - - Details - - - - - - Download - - - - HTML - - - {" | "} - - - YAML - - - - - - - - ) : task?.state === "Failed" ? ( - task ? ( - <> + + + + + + + + {t("terms.riskFromApplication")} + + + + + + + + + ); +}; + +const TabTagsContent: React.FC<{ + application: DecoratedApplication; + task: TaskDashboard | null; +}> = ({ application, task }) => { + const { t } = useTranslation(); + const isTaskRunning = task?.state === "Running"; + + return ( + <> + {isTaskRunning ? ( + + + + {t("message.taskInProgressForTags")} + + + + + ) : null} + + + + ); +}; + +const TabReportsContent: React.FC<{ + application: DecoratedApplication; + task: TaskDashboard | null; +}> = ({ application, task }) => { + const { t } = useTranslation(); + const { facts, isFetching } = useFetchFacts(application?.id); + + const { identities } = useFetchIdentities(); + let matchingSourceCredsRef: Identity | undefined; + let matchingMavenCredsRef: Identity | undefined; + if (application && identities) { + matchingSourceCredsRef = getKindIdByRef(identities, application, "source"); + matchingMavenCredsRef = getKindIdByRef(identities, application, "maven"); + } + + const notAvailable = ; + + const enableDownloadSetting = useSetting("download.html.enabled"); + + const history = useHistory(); + const navigateToAnalysisDetails = () => + application?.id && + task?.id && + history.push( + formatPath(Paths.applicationsAnalysisDetails, { + applicationId: application?.id, + taskId: task?.id, + }) + ); + + return ( + <> + + + Credentials + + {matchingSourceCredsRef && matchingMavenCredsRef ? ( + + + Source and Maven + + ) : matchingMavenCredsRef ? ( + + + Maven + + ) : matchingSourceCredsRef ? ( + + + Source + + ) : ( + notAvailable + )} + + + Analysis + + {task?.state === "Succeeded" && application ? ( + <> + + + Details + + - - ) : ( - + + + + Download + + + + HTML + + + {" | "} + + + YAML + + + + + + + + ) : task?.state === "Failed" ? ( + task ? ( + <> + - ) : ( - notAvailable - )} - - )} - - {!isFetching && !!facts.length && ( - + } + type="button" + variant="link" + onClick={navigateToAnalysisDetails} + className={spacing.ml_0} + style={{ margin: "0", padding: "0" }} + > + Analysis details + + + ) : ( + + + Failed + + ) + ) : ( + <> + {task ? ( + + ) : ( + notAvailable )} - - {t("terms.review")}} - > - - - - - + + )} + + {!isFetching && !!facts.length && } + ); }; -const ArchetypeLabels: React.FC<{ archetypeRefs?: Ref[] }> = ({ - archetypeRefs, -}) => ; - -const ArchetypeItem: React.FC<{ archetype: Archetype }> = ({ archetype }) => { - return ; -}; diff --git a/client/src/app/queries/archetypes.ts b/client/src/app/queries/archetypes.ts index e08cff282f..58e4772112 100644 --- a/client/src/app/queries/archetypes.ts +++ b/client/src/app/queries/archetypes.ts @@ -10,7 +10,7 @@ import { updateArchetype, } from "@app/api/rest"; import { assessmentsByItemIdQueryKey } from "./assessments"; -import { useState } from "react"; +import { useMemo, useState } from "react"; export const ARCHETYPES_QUERY_KEY = "archetypes"; export const ARCHETYPE_QUERY_KEY = "archetype"; @@ -42,8 +42,13 @@ export const useFetchArchetypes = (forApplication?: Application | null) => { onError: (error: AxiosError) => console.log(error), }); + const archetypesById = useMemo(() => { + return Object.fromEntries(filteredArchetypes.map((a) => [a.id, a])); + }, [filteredArchetypes]); + return { archetypes: filteredArchetypes, + archetypesById, isFetching: isLoading, isSuccess, error, diff --git a/client/src/app/queries/tasks.ts b/client/src/app/queries/tasks.ts index 1f95581d69..67dd81c5dc 100644 --- a/client/src/app/queries/tasks.ts +++ b/client/src/app/queries/tasks.ts @@ -7,7 +7,7 @@ import { getTaskById, getTaskByIdAndFormat, getTaskQueue, - getTasks, + getTasksDashboard, getTextFile, updateTask, } from "@app/api/rest"; @@ -17,6 +17,7 @@ import { HubRequestParams, Task, TaskQueue, + TaskDashboard, } from "@app/api/models"; export const TaskStates = { @@ -37,7 +38,7 @@ export const TaskAttachmentByIDQueryKey = "taskAttachmentByID"; /** * Rebuild the __state__ of a Task to include the UI synthetic "SucceededWithErrors" */ -const calculateSyntheticState = (task: Task): Task => { +const calculateSyntheticTaskState = (task: Task): Task => { if (task.state === "Succeeded" && (task.errors?.length ?? 0) > 0) { task.state = "SucceededWithErrors"; } @@ -45,13 +46,26 @@ const calculateSyntheticState = (task: Task): Task => { return task; }; -export const useFetchTasks = (refetchDisabled: boolean = false) => { +/** + * Rebuild the __state__ of a TaskDashboard to include the UI synthetic "SucceededWithErrors" + */ +const calculateSyntheticTaskDashboardState = ( + task: TaskDashboard +): TaskDashboard => { + if (task.state === "Succeeded" && (task?.errors ?? 0) > 0) { + task.state = "SucceededWithErrors"; + } + + return task; +}; + +export const useFetchTaskDashboard = (refetchDisabled: boolean = false) => { const { isLoading, error, refetch, data } = useQuery({ - queryKey: [TasksQueryKey], - queryFn: getTasks, + queryKey: [TasksQueryKey, "/report/dashboard"], + queryFn: getTasksDashboard, select: (tasks) => tasks - .map(calculateSyntheticState) + .map(calculateSyntheticTaskDashboardState) .sort((a, b) => -1 * universalComparator(a.createTime, b.createTime)), onError: (err) => console.log(err), refetchInterval: !refetchDisabled ? 5000 : false, @@ -78,7 +92,7 @@ export const useServerTasks = ( queryFn: async () => await getServerTasks(params), select: (data) => { if (data?.data?.length > 0) { - data.data = data.data.map(calculateSyntheticState); + data.data = data.data.map(calculateSyntheticTaskState); } return data; }, @@ -200,7 +214,7 @@ export const useFetchTaskByID = (taskId?: number) => { queryKey: [TaskByIDQueryKey, taskId], queryFn: () => (taskId ? getTaskById(taskId) : null), select: (task: Task | null) => - !task ? null : calculateSyntheticState(task), + !task ? null : calculateSyntheticTaskState(task), enabled: !!taskId, }); From d061d5301a776cb65a398aff84262e0a0f965337 Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Tue, 16 Jul 2024 16:33:07 -0400 Subject: [PATCH 8/8] Drawer tab for reports should allow download on SucceededWithErrors Signed-off-by: Scott J Dickerson --- .../application-detail-drawer.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx index e0356ffd65..bada7d1686 100644 --- a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx +++ b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx @@ -64,6 +64,7 @@ import { Paths } from "@app/Paths"; import { useFetchArchetypes } from "@app/queries/archetypes"; import { useFetchAssessments } from "@app/queries/assessments"; import { DecoratedApplication } from "../../applications-table/useDecoratedApplications"; +import { TaskStates } from "@app/queries/tasks"; export interface IApplicationDetailDrawerProps extends Pick { @@ -364,6 +365,10 @@ const TabReportsContent: React.FC<{ }) ); + const taskState = application.tasks.currentAnalyzer?.state ?? ""; + const taskSucceeded = TaskStates.Success.includes(taskState); + const taskFailed = TaskStates.Failed.includes(taskState); + return ( <> @@ -392,7 +397,7 @@ const TabReportsContent: React.FC<{ Analysis - {task?.state === "Succeeded" && application ? ( + {taskSucceeded ? ( <> @@ -458,7 +463,7 @@ const TabReportsContent: React.FC<{ - ) : task?.state === "Failed" ? ( + ) : taskFailed ? ( task ? ( <>