diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index 64d2901ea..e4d1bf749 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -184,6 +184,10 @@ "duplicateWave": "The migration wave could not be created due to a conflict with an existing wave. Make sure the name and start/end dates are unique and try again.", "importErrorCheckDocumentation": "For status Error imports, check the documentation to ensure your file is structured correctly.", "insecureTracker": "Insecure mode deactivates certificate verification. Use insecure mode for instances that have self-signed certificates.", + "inheritedReviewTooltip": "This application is inheriting a review from an archetype.", + "inheritedReviewTooltip_plural": "This application is inheriting reviews from {{count}} archetypes.", + "inheritedAssessmentTooltip": "This application is inheriting an assessment from an archetype.", + "inheritedAssessmentTooltip_plural": "This application is inheriting assessments from {{count}} archetypes.", "jiraInstanceNotConnected": "Jira instance {{name}} is not connected.", "manageDependenciesInstructions": "Add northbound and southbound dependencies for the selected application here. Note that any selections made will be saved automatically. To undo any changes, you must manually delete the applications from the dropdowns.", "noDataAvailableBody": "No data available to be shown here.", @@ -237,6 +241,7 @@ "associatedApplications": "Associated applications", "associatedArchetypes": "Associated archetypes", "archetypesReviewed": "Archetypes reviewed", + "archetypesAssessed": "Archetypes assessed", "add": "Add", "additionalNotesOrComments": "Additional notes or comments", "adoptionCandidateDistribution": "Application confidence and risk", @@ -327,6 +332,7 @@ "inProgress": "In-progress", "instanceType": "Instance type", "instance": "Instance", + "inherited": "Inherited", "issueType": "Issue type", "jiraConfig": "Jira configuration", "issue": "Issue", diff --git a/client/src/app/components/IconedStatus.tsx b/client/src/app/components/IconedStatus.tsx index 665d4b058..ff347cb3a 100644 --- a/client/src/app/components/IconedStatus.tsx +++ b/client/src/app/components/IconedStatus.tsx @@ -1,13 +1,16 @@ import React from "react"; -import { Flex, FlexItem, Icon } from "@patternfly/react-core"; +import { Flex, FlexItem, Icon, Tooltip } 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 QuestionCircleIcon from "@patternfly/react-icons/dist/esm/icons/question-circle-icon"; export type IconedStatusPreset = + | "InheritedReviews" + | "InheritedAssessments" | "Canceled" | "Completed" | "Error" @@ -35,6 +38,8 @@ export interface IIconedStatusProps { icon?: React.ReactNode; className?: string; label?: React.ReactNode | string; + tooltipMessage?: string; + tooltipCount?: number; } export const IconedStatus: React.FC = ({ @@ -43,9 +48,26 @@ export const IconedStatus: React.FC = ({ icon, className = "", label, + tooltipCount = 0, }: IIconedStatusProps) => { const { t } = useTranslation(); const presets: IconedStatusPresetType = { + InheritedReviews: { + icon: , + status: "info", + label: t("terms.inherited"), + tooltipMessage: t("message.inheritedReviewTooltip", { + count: tooltipCount, + }), + }, + InheritedAssessments: { + icon: , + status: "info", + label: t("terms.inherited"), + tooltipMessage: t("message.inheritedAssessmentTooltip", { + count: tooltipCount, + }), + }, Canceled: { icon: , status: "info", @@ -89,6 +111,14 @@ export const IconedStatus: React.FC = ({ }, }; const presetProps = preset && presets[preset]; + const IconWithOptionalTooltip: React.FC<{ children: React.ReactElement }> = ({ + children, + }) => + presetProps?.tooltipMessage ? ( + {children} + ) : ( + <>{children} + ); return ( = ({ spaceItems={{ default: "spaceItemsSm" }} > - - {icon || presetProps?.icon || } - + + + {icon || presetProps?.icon || } + + {label || presetProps?.label} 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 2189169a0..96ab5b594 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -17,8 +17,6 @@ import { MenuToggle, MenuToggleElement, Modal, - Flex, - FlexItem, } from "@patternfly/react-core"; import { PencilAltIcon, TagIcon, EllipsisVIcon } from "@patternfly/react-icons"; import { @@ -30,7 +28,6 @@ import { ActionsColumn, Tbody, } from "@patternfly/react-table"; -import { QuestionCircleIcon } from "@patternfly/react-icons/dist/esm/icons/question-circle-icon"; // @app components and utilities import { AppPlaceholder } from "@app/components/AppPlaceholder"; @@ -44,7 +41,6 @@ import { ConditionalTableBody, TableRowContentWithControls, } from "@app/components/TableControls"; -import { IconedStatus } from "@app/components/IconedStatus"; import { ToolbarBulkSelector } from "@app/components/ToolbarBulkSelector"; import { ConfirmDialog } from "@app/components/ConfirmDialog"; import { NotificationsContext } from "@app/components/NotificationsContext"; @@ -108,7 +104,6 @@ import { getTaskById, } from "@app/api/rest"; import { ApplicationDependenciesForm } from "@app/components/ApplicationDependenciesFormContainer/ApplicationDependenciesForm"; -import { useFetchArchetypes } from "@app/queries/archetypes"; import { useState } from "react"; import { ApplicationAnalysisStatus } from "../components/application-analysis-status"; import { ApplicationDetailDrawer } from "../components/application-detail-drawer/application-detail-drawer"; @@ -116,6 +111,7 @@ import { SimpleDocumentViewerModal } from "@app/components/SimpleDocumentViewer" import { AnalysisWizard } from "../analysis-wizard/analysis-wizard"; import { TaskGroupProvider } from "../analysis-wizard/components/TaskGroupContext"; import { ApplicationIdentityForm } from "../components/application-identity-form/application-identity-form"; +import { ApplicationReviewStatus } from "../components/application-review-status/application-review-status"; export const ApplicationsTable: React.FC = () => { const { t } = useTranslation(); @@ -221,8 +217,6 @@ export const ApplicationsTable: React.FC = () => { refetch: fetchApplications, } = useFetchApplications(); - const { archetypes } = useFetchArchetypes(); - const onDeleteApplicationSuccess = (appIDCount: number) => { pushNotification({ title: t("toastr.success.applicationDeleted", { @@ -870,23 +864,6 @@ export const ApplicationsTable: React.FC = () => { > {currentPageItems?.map((application, rowIndex) => { - const isAppReviewed = !!application.review; - const applicationArchetypes = application.archetypes?.map( - (archetypeRef) => { - return archetypes.find( - (archetype) => archetype.id === archetypeRef.id - ); - } - ); - - const hasReviewedArchetype = applicationArchetypes?.some( - (archetype) => !!archetype?.review - ); - - const hasAssessedArchetype = applicationArchetypes?.some( - (archetype) => !!archetype?.assessments?.length - ); - return ( { modifier="truncate" {...getTdProps({ columnKey: "assessment" })} > - - - - - - {hasAssessedArchetype ? ( - - - - ) : null} - - + - - - - - - {hasReviewedArchetype ? ( - - - - ) : null} - - + = ({ application, isLoading = false }) => { +> = ({ application }) => { const { t } = useTranslation(); + const { archetypes, isFetching } = useFetchArchetypes(); + + const applicationArchetypes = application.archetypes?.map((archetypeRef) => { + return archetypes?.find((archetype) => archetype.id === archetypeRef.id); + }); + + const hasAssessedArchetype = applicationArchetypes?.some( + (archetype) => !!archetype?.assessments?.length ?? 0 > 0 + ); + const { assessments, isFetching: isFetchingAssessmentsById, fetchError, } = useFetchAssessmentsByItemId(false, application.id); - const { questionnaires } = useFetchQuestionnaires(); - const requiredQuestionnaireExists = questionnaires?.some( - (q) => q.required === true - ); - //NOTE: Application.assessed is true if an app is assigned to an archetype and no required questionnaires exist - if (application?.assessed && requiredQuestionnaireExists) { - return ; - } if (fetchError) { return ; } - if (isLoading || isFetchingAssessmentsById) { + if (isFetching || isFetchingAssessmentsById) { return ; } - if ( - assessments?.some((a) => a.status === "started" || a.status === "complete") + let statusPreset: IconedStatusPreset = "NotStarted"; // Default status + let tooltipCount: number = 0; + const isDirectlyAssessed = + application.assessed && (application.assessments?.length ?? 0) > 0; + if (isDirectlyAssessed) { + statusPreset = "Completed"; + } else if (hasAssessedArchetype) { + statusPreset = "InheritedAssessments"; + const assessedArchetypeCount = + applicationArchetypes?.filter( + (archetype) => archetype?.assessments?.length ?? 0 > 0 + ).length || 0; + tooltipCount = assessedArchetypeCount; + } else if ( + assessments?.some( + (assessment) => + assessment.status === "started" || assessment.status === "complete" + ) ) { - return ; + statusPreset = "InProgress"; } - - return ; + 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 050925b54..12887de1d 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 @@ -20,9 +20,17 @@ import { DescriptionListTerm, Divider, Tooltip, + Label, } from "@patternfly/react-core"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { Application, Identity, Task, MimeType, Ref } from "@app/api/models"; +import { + Application, + Identity, + Task, + MimeType, + Ref, + Archetype, +} from "@app/api/models"; import { IPageDrawerContentProps, PageDrawerContent, @@ -46,9 +54,9 @@ import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclam import { ApplicationFacts } from "./application-facts"; import { ReviewFields } from "./review-fields"; import { LabelsFromItems } from "@app/components/labels/labels-from-items/labels-from-items"; -import { ReviewedArchetypeItem } from "./reviewed-archetype-item"; import { RiskLabel } from "@app/components/RiskLabel"; import { ApplicationDetailFields } from "./application-detail-fields"; +import { useFetchArchetypes } from "@app/queries/archetypes"; export interface IApplicationDetailDrawerProps extends Pick { @@ -77,7 +85,9 @@ export const ApplicationDetailDrawer: React.FC< const isTaskRunning = task?.state === "Running"; const { identities } = useFetchIdentities(); + const { archetypes } = useFetchArchetypes(); const { facts, isFetching } = useFetchFacts(application?.id); + const [taskIdToView, setTaskIdToView] = React.useState(); let matchingSourceCredsRef: Identity | undefined; @@ -91,6 +101,22 @@ export const ApplicationDetailDrawer: React.FC< const enableDownloadSetting = useSetting("download.html.enabled"); + const assessedArchetypes = + application?.archetypes + ?.map((archetypeRef) => + archetypes.find((archetype) => archetype.id === archetypeRef.id) + ) + .filter((fullArchetype) => fullArchetype?.assessed) + .filter(Boolean) || []; + + const reviewedArchetypes = + application?.archetypes + ?.map((archetypeRef) => + archetypes.find((archetype) => archetype.id === archetypeRef.id) + ) + .filter((fullArchetype) => fullArchetype?.review) + .filter(Boolean) || []; + return ( + + + {t("terms.archetypesAssessed")} + + + {assessedArchetypes?.length ? ( + assessedArchetypes.map((assessedArchetype) => ( + + )) + ) : ( + + )} + + + {t("terms.archetypesReviewed")} - {application?.archetypes?.length ?? 0 > 0 ? ( - application?.archetypes?.map((archetypeRef) => ( - ( + )) ) : ( @@ -408,3 +452,7 @@ export const ApplicationDetailDrawer: React.FC< const ArchetypeLabels: React.FC<{ archetypeRefs?: Ref[] }> = ({ archetypeRefs, }) => ; + +const ArchetypeItem: React.FC<{ archetype: Archetype }> = ({ archetype }) => { + return ; +}; diff --git a/client/src/app/pages/applications/components/application-detail-drawer/reviewed-archetype-item.tsx b/client/src/app/pages/applications/components/application-detail-drawer/reviewed-archetype-item.tsx deleted file mode 100644 index 912d8588d..000000000 --- a/client/src/app/pages/applications/components/application-detail-drawer/reviewed-archetype-item.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useFetchArchetypeById } from "@app/queries/archetypes"; -import { Label } from "@patternfly/react-core"; -import React from "react"; - -export const ReviewedArchetypeItem = ({ id }: { id: number }) => { - const { archetype } = useFetchArchetypeById(id); - - if (!archetype?.review) return null; - - return ( - - ); -}; diff --git a/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx b/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx new file mode 100644 index 000000000..fccb89c46 --- /dev/null +++ b/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { Application } from "@app/api/models"; +import { IconedStatus, IconedStatusPreset } from "@app/components/IconedStatus"; +import { Spinner } from "@patternfly/react-core"; +import { EmptyTextMessage } from "@app/components/EmptyTextMessage"; +import { useTranslation } from "react-i18next"; +import { useFetchArchetypes } from "@app/queries/archetypes"; + +export interface ApplicationReviewStatusProps { + application: Application; + isLoading?: boolean; +} + +export const ApplicationReviewStatus: React.FC< + ApplicationReviewStatusProps +> = ({ application }) => { + const { t } = useTranslation(); + + const { archetypes, isFetching } = useFetchArchetypes(); + const isAppReviewed = !!application.review; + + const applicationArchetypes = application.archetypes?.map((archetypeRef) => { + return archetypes?.find((archetype) => archetype.id === archetypeRef.id); + }); + + const reviewedArchetypeCount = + applicationArchetypes?.filter((archetype) => !!archetype?.review).length || + 0; + + if (isFetching) { + return ; + } + + let statusPreset: IconedStatusPreset; + let tooltipCount = 0; + + if (isAppReviewed) { + statusPreset = "Completed"; + } else if (reviewedArchetypeCount > 0) { + statusPreset = "InheritedReviews"; + tooltipCount = reviewedArchetypeCount; + } else { + statusPreset = "NotStarted"; + } + + if (!applicationArchetypes || applicationArchetypes.length === 0) { + return ; + } + + return ; +};