From 41822432e53cde60c1be9407e2c4fd75bf0ffa62 Mon Sep 17 00:00:00 2001 From: Scott Dickerson Date: Tue, 16 Jul 2024 17:18:43 -0400 Subject: [PATCH] :sparkles: Add handling for tasks "Succeeded with errors" in task and application tables (#2011) ### Summary Resolves: #1997 When a task is succeeded but has attached errors, render task state, application tasks state and application analysis state with a warning. ### Details of Changes With this change, hub is not adding a new terminal state, but having a new terminal state is an easy way to handle things. The task queries will now evaluate the `state` and `errors` props to determine if "Succeeded" should be "SucceededWithErrors". This synthetic task status makes updating all of the UI component much simpler. To function properly, HUB [Issue 725](https://github.com/konveyor/tackle2-hub/issues/725) / [PR 720](https://github.com/konveyor/tackle2-hub/pull/720) are required. Changes have also been made to utilize the `/tasks/reports/dashboard` endpoint to get a much smaller view of tasks when dealing with them in aggregate. The application details drawer was refactored to push data fetching closer to actual use, and to break up a very large component into a container component with a component dedicated to each tab. ### Screenshots Application inventory page: ![screenshot-localhost_9000-2024 07 15-18_33_20](https://github.com/user-attachments/assets/b7015ebf-4305-4d6d-a670-5e4fe532981d) Application tasks popover: ![image](https://github.com/user-attachments/assets/14d4d19e-eaca-4505-88be-d5016da1e70b) Task Manager page: ![screenshot-localhost_9000-2024 07 15-18_35_15](https://github.com/user-attachments/assets/2547a46a-1fa6-456f-836c-4e59cc0fd074) --------- Signed-off-by: Scott J Dickerson --- client/public/locales/en/translation.json | 14 + client/src/app/api/models.ts | 21 +- client/src/app/api/rest.ts | 7 +- .../src/app/components/Icons/IconedStatus.tsx | 22 +- .../app/components/Icons/TaskStateIcon.tsx | 7 + .../__snapshots__/StatusIcon.test.tsx.snap | 28 +- .../applications-table/applications-table.tsx | 27 +- .../components/column-application-name.tsx | 22 +- .../useDecoratedApplications.ts | 23 +- .../application-analysis-status.tsx | 29 +- .../application-detail-drawer.tsx | 727 ++++++++++-------- client/src/app/pages/tasks/tasks-page.tsx | 19 +- client/src/app/queries/archetypes.ts | 7 +- client/src/app/queries/tasks.ts | 63 +- 14 files changed, 595 insertions(+), 421 deletions(-) diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index 75b620e03..8ee495059 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -260,6 +260,19 @@ "dependencies": "Dependencies", "tasks": "Task Manager" }, + "taskState": { + "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", "acceptedAppsAndDeps": "Accepted applications and dependencies", @@ -312,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/api/models.ts b/client/src/app/api/models.ts index 635aa5988..98ffa8fdf 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; @@ -334,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 232d28f50..542507b07 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/components/Icons/IconedStatus.tsx b/client/src/app/components/Icons/IconedStatus.tsx index 6179f95b2..984031d99 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/components/Icons/TaskStateIcon.tsx b/client/src/app/components/Icons/TaskStateIcon.tsx index e78e30f34..4a481f5b7 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/components/tests/__snapshots__/StatusIcon.test.tsx.snap b/client/src/app/components/tests/__snapshots__/StatusIcon.test.tsx.snap index 4a5443690..d623a0f8a 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`] = ` - + 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 51e5f2cee..f3b7c7baf 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 1b1cf2add..627ca871e 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 { @@ -7,19 +8,19 @@ import { TimesCircleIcon, InProgressIcon, ExclamationCircleIcon, + 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"]; @@ -79,6 +80,15 @@ const statusMap: Record = { ), }, + SuccessWithErrors: { + popoverVariant: "warning", + headerText: "All tasks succeeded, but some errors occurred", + icon: () => ( + + + + ), + }, }; const linkToTasks = (applicationName: string) => { @@ -86,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 2cf66dae1..c9a371605 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[]; } /** @@ -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,9 +34,10 @@ 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; + currentAnalyzer: TaskDashboard | undefined; }; tasksStatus: ApplicationTasksStatus; @@ -48,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) => [ @@ -83,6 +85,8 @@ const chooseApplicationTaskStatus = ({ ? "Failed" : tasks.latestHasCanceled ? "Canceled" + : tasks.latestHasSuccessWithErrors + ? "SuccessWithErrors" : "Success"; }; @@ -92,7 +96,7 @@ const chooseApplicationTaskStatus = ({ */ const decorateApplications = ( applications: Application[], - tasks: Task[], + tasks: TaskDashboard[], identities: Identity[] ) => { const { tasksById, tasksByIdByKind } = groupTasks(tasks); @@ -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], }, @@ -145,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-analysis-status.tsx b/client/src/app/pages/applications/components/application-analysis-status.tsx index 5759c4293..4462aacaf 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 ; }; 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 23ab73115..bada7d168 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,15 @@ 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"; +import { TaskStates } from "@app/queries/tasks"; 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 +83,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, + }) + ); + + const taskState = application.tasks.currentAnalyzer?.state ?? ""; + const taskSucceeded = TaskStates.Success.includes(taskState); + const taskFailed = TaskStates.Failed.includes(taskState); + + return ( + <> + + + Credentials + + {matchingSourceCredsRef && matchingMavenCredsRef ? ( + + + Source and Maven + + ) : matchingMavenCredsRef ? ( + + + Maven + + ) : matchingSourceCredsRef ? ( + + + Source + + ) : ( + notAvailable + )} + + + Analysis + + {taskSucceeded ? ( + <> + + + Details + + - - ) : ( - + + + + Download + + + + HTML + + + {" | "} + + + YAML + + + + + + + + ) : taskFailed ? ( + 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")}} - > - - - - - - ); -}; -const ArchetypeLabels: React.FC<{ archetypeRefs?: Ref[] }> = ({ - archetypeRefs, -}) => ; + + )} + -const ArchetypeItem: React.FC<{ archetype: Archetype }> = ({ archetype }) => { - return ; + {!isFetching && !!facts.length && } + + ); }; diff --git a/client/src/app/pages/tasks/tasks-page.tsx b/client/src/app/pages/tasks/tasks-page.tsx index 915c5f6ad..d4452dbb5 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: "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 = () => { 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/archetypes.ts b/client/src/app/queries/archetypes.ts index e08cff282..58e477211 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 512e13fc3..67dd81c5d 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 = { @@ -25,27 +26,49 @@ export const TaskStates = { Failed: ["Failed"], Queued: ["Ready", "Postponed", "Pending", "Running"], // "Created", "QuotaBlocked" ?? Running: ["Running"], - Success: ["Succeeded"], + Success: ["Succeeded", "SucceededWithErrors"], + SuccessWithErrors: ["SucceededWithErrors"], }; export const TasksQueryKey = "tasks"; -export const TasksQueueKey = "TasksQueue"; +export const TasksQueueKey = "tasksQueue"; export const TaskByIDQueryKey = "taskByID"; export const TaskAttachmentByIDQueryKey = "taskAttachmentByID"; -export const useFetchTasks = (refetchDisabled: boolean = false) => { +/** + * Rebuild the __state__ of a Task to include the UI synthetic "SucceededWithErrors" + */ +const calculateSyntheticTaskState = (task: Task): Task => { + if (task.state === "Succeeded" && (task.errors?.length ?? 0) > 0) { + task.state = "SucceededWithErrors"; + } + + return task; +}; + +/** + * 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, - refetchInterval: !refetchDisabled ? 5000 : false, - select: (allTasks) => { - // sort by createTime (newest to oldest) - allTasks.sort( - (a, b) => -1 * universalComparator(a.createTime, b.createTime) - ); - return allTasks; - }, + queryKey: [TasksQueryKey, "/report/dashboard"], + queryFn: getTasksDashboard, + select: (tasks) => + tasks + .map(calculateSyntheticTaskDashboardState) + .sort((a, b) => -1 * universalComparator(a.createTime, b.createTime)), onError: (err) => console.log(err), + refetchInterval: !refetchDisabled ? 5000 : false, }); const hasActiveTasks = @@ -61,13 +84,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], queryFn: async () => await getServerTasks(params), - onError: (error) => console.log("error, ", error), + select: (data) => { + if (data?.data?.length > 0) { + data.data = data.data.map(calculateSyntheticTaskState); + } + return data; + }, + onError: (error: Error) => console.log("error, ", error), keepPreviousData: true, refetchInterval: refetchInterval ?? false, }); @@ -184,6 +213,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 : calculateSyntheticTaskState(task), enabled: !!taskId, });