From 849a2eca41257b69f1903076404004f1ba8eb6cc Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Tue, 16 Jul 2024 16:22:00 -0400 Subject: [PATCH] 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, });