diff --git a/client/src/app/components/application-assessment-donut-chart/application-assessment-donut-chart.tsx b/client/src/app/components/application-assessment-donut-chart/application-assessment-donut-chart.tsx new file mode 100644 index 0000000000..efd11a4467 --- /dev/null +++ b/client/src/app/components/application-assessment-donut-chart/application-assessment-donut-chart.tsx @@ -0,0 +1,179 @@ +import React, { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { ChartDonut, ChartLegend } from "@patternfly/react-charts"; +import { RISK_LIST } from "@app/Constants"; +import { + Assessment, + Section, + IdRef, + AssessmentWithArchetypeApplications, +} from "@app/api/models"; + +import { global_palette_blue_300 as defaultColor } from "@patternfly/react-tokens"; +import { useFetchAssessmentsWithArchetypeApplications } from "@app/queries/assessments"; + +export interface ChartData { + red: number; + amber: number; + green: number; + unknown: number; +} + +export const getChartDataFromCategories = ( + categories: Section[] +): ChartData => { + let green = 0; + let amber = 0; + let red = 0; + let unknown = 0; + + categories + .flatMap((f) => f.questions) + .flatMap((f) => f.answers) + .filter((f) => f.selected === true) + .forEach((f) => { + switch (f.risk) { + case "GREEN": + green++; + break; + case "yellow": + amber++; + break; + case "RED": + red++; + break; + default: + unknown++; + } + }); + + return { + red, + amber, + green, + unknown, + } as ChartData; +}; + +export const getChartDataFromMultipleAssessments = ( + assessments: Assessment[] +): ChartData => { + let green = 0, + amber = 0, + red = 0, + unknown = 0; + + assessments.forEach((assessment) => { + assessment.sections + .flatMap((section) => section.questions) + .flatMap((question) => question.answers) + .filter((answer) => answer.selected) + .forEach((answer) => { + switch (answer.risk) { + case "green": + green++; + break; + case "yellow": + amber++; + break; + case "red": + red++; + break; + default: + unknown++; + } + }); + }); + + return { red, amber, green, unknown }; +}; + +export interface IApplicationAssessmentDonutChartProps { + assessmentRefs?: IdRef[]; +} + +export const ApplicationAssessmentDonutChart: React.FC< + IApplicationAssessmentDonutChartProps +> = ({ assessmentRefs }) => { + const { t } = useTranslation(); + const { assessmentsWithArchetypeApplications } = + useFetchAssessmentsWithArchetypeApplications(); + + const filterAssessmentsByRefs = ( + assessments: AssessmentWithArchetypeApplications[], + refs: IdRef[] + ) => { + if (refs && refs.length > 0) { + return assessments.filter((assessment) => + refs.some((ref) => ref.id === assessment.id) + ); + } + return assessments; + }; + + const filteredAssessments = filterAssessmentsByRefs( + assessmentsWithArchetypeApplications, + assessmentRefs || [] + ); + + const charData: ChartData = useMemo(() => { + return getChartDataFromMultipleAssessments(filteredAssessments); + }, [filteredAssessments]); + + const chartDefinition = [ + { + x: t(RISK_LIST["green"].i18Key), + y: charData.green, + color: RISK_LIST["green"].hexColor, + }, + { + x: t(RISK_LIST["yellow"].i18Key), + y: charData.amber, + color: RISK_LIST["yellow"].hexColor, + }, + { + x: t(RISK_LIST["red"].i18Key), + y: charData.red, + color: RISK_LIST["red"].hexColor, + }, + { + x: t(RISK_LIST["unknown"].i18Key), + y: charData.unknown, + color: RISK_LIST["unknown"].hexColor, + }, + ].filter((f) => f.y > 0); + + return ( +
+ ({ x: elem.x, y: elem.y }))} + labels={({ datum }) => `${datum.x}: ${datum.y}`} + colorScale={chartDefinition.map( + (elem) => elem.color || defaultColor.value + )} + legendComponent={ + ({ + name: `${elem.x}: ${elem.y}`, + }))} + colorScale={chartDefinition.map( + (elem) => elem.color || defaultColor.value + )} + /> + } + legendOrientation="vertical" + legendPosition="right" + padding={{ + bottom: 20, + left: 20, + right: 140, + top: 20, + }} + innerRadius={50} + width={380} + /> +
+ ); +}; diff --git a/client/src/app/pages/reports/components/assessment-landscape/assessment-landscape.tsx b/client/src/app/pages/reports/components/assessment-landscape/assessment-landscape.tsx deleted file mode 100644 index 72458c62c6..0000000000 --- a/client/src/app/pages/reports/components/assessment-landscape/assessment-landscape.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, { useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { Flex, FlexItem, Skeleton } from "@patternfly/react-core"; - -import { RISK_LIST } from "@app/Constants"; -import { Assessment, IdRef, Questionnaire } from "@app/api/models"; -import { ConditionalRender } from "@app/components/ConditionalRender"; -import { Donut } from "../donut/donut"; -import { useFetchAssessmentsWithArchetypeApplications } from "@app/queries/assessments"; - -interface IAggregateRiskData { - green: number; - yellow: number; - red: number; - unknown: number; - unassessed: number; - assessmentCount: number; -} - -const aggregateRiskData = (assessments: Assessment[]): IAggregateRiskData => { - let low = 0; - let medium = 0; - let high = 0; - let unknown = 0; - - assessments?.forEach((assessment) => { - switch (assessment.risk) { - case "green": - low++; - break; - case "yellow": - medium++; - break; - case "red": - high++; - break; - case "unknown": - unknown++; - break; - } - }); - - return { - green: low, - yellow: medium, - red: high, - unknown, - unassessed: assessments.length - low - medium - high, - assessmentCount: assessments.length, - }; -}; - -interface IAssessmentLandscapeProps { - /** - * The selected questionnaire or `null` if _all questionnaires_ is selected. - */ - questionnaire: Questionnaire | null; - - /** - * The set of assessments for the selected questionnaire. Risk values will be - * aggregated from the individual assessment risks. - */ - assessmentRefs?: IdRef[]; -} - -export const AssessmentLandscape: React.FC = ({ - questionnaire, - assessmentRefs, -}) => { - const { t } = useTranslation(); - - const { assessmentsWithArchetypeApplications } = - useFetchAssessmentsWithArchetypeApplications(); - - const filteredAssessments = assessmentsWithArchetypeApplications.filter( - (assessment) => assessmentRefs?.some((ref) => ref.id === assessment.id) - ); - - const landscapeData = useMemo( - () => aggregateRiskData(filteredAssessments), - [filteredAssessments] - ); - - return ( - - - - } - > - {landscapeData && ( - - - - - - - - - - - - - - - )} - - ); -}; diff --git a/client/src/app/pages/reports/components/assessment-landscape/index.ts b/client/src/app/pages/reports/components/assessment-landscape/index.ts deleted file mode 100644 index 06ef2e27b2..0000000000 --- a/client/src/app/pages/reports/components/assessment-landscape/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AssessmentLandscape } from "./assessment-landscape"; diff --git a/client/src/app/pages/reports/components/identified-risks-table/identified-risks-table.tsx b/client/src/app/pages/reports/components/identified-risks-table/identified-risks-table.tsx index 78a425379f..e135bfb238 100644 --- a/client/src/app/pages/reports/components/identified-risks-table/identified-risks-table.tsx +++ b/client/src/app/pages/reports/components/identified-risks-table/identified-risks-table.tsx @@ -35,10 +35,18 @@ import RiskIcon from "@app/components/risk-icon/risk-icon"; export interface IIdentifiedRisksTableProps { assessmentRefs?: IdRef[]; + isReviewPage?: boolean; } +const riskLevelMapping = { + red: 3, + yellow: 2, + green: 1, + unknown: 0, +}; export const IdentifiedRisksTable: React.FC = ({ assessmentRefs, + isReviewPage = false, }) => { const { t } = useTranslation(); @@ -152,8 +160,17 @@ export const IdentifiedRisksTable: React.FC = ({ question: item.question.text, answer: item.answer.text, applications: item.applications.length, + risk: + riskLevelMapping[item.answer.risk as keyof typeof riskLevelMapping] || + riskLevelMapping["unknown"], }), - sortableColumns: ["assessmentName", "section", "question", "answer"], + sortableColumns: [ + "assessmentName", + "section", + "question", + "answer", + "risk", + ], isExpansionEnabled: true, expandableVariant: "single", }); @@ -171,6 +188,7 @@ export const IdentifiedRisksTable: React.FC = ({ getTdProps, getExpandedContentTdProps, }, + sortState, expansionDerivedState: { isCellExpanded }, } = tableControls; @@ -202,9 +220,14 @@ export const IdentifiedRisksTable: React.FC = ({ Question Answer Risk - - Application - + { + /* Only show the applications column on the reports page */ + !isReviewPage && ( + + Application + + ) + } @@ -242,17 +265,19 @@ export const IdentifiedRisksTable: React.FC = ({ - - {item?.applications.length ? ( - - {t("composed.totalApplications", { - count: item.applications.length, - })} - - ) : ( - "N/A" - )} - + {!isReviewPage && ( + + {item?.applications.length ? ( + + {t("composed.totalApplications", { + count: item.applications.length, + })} + + ) : ( + "N/A" + )} + + )} {isCellExpanded(item) ? ( diff --git a/client/src/app/pages/review/review-page.tsx b/client/src/app/pages/review/review-page.tsx index 5cd57dd23e..81ef11d83e 100644 --- a/client/src/app/pages/review/review-page.tsx +++ b/client/src/app/pages/review/review-page.tsx @@ -26,7 +26,7 @@ import useIsArchetype from "@app/hooks/useIsArchetype"; import { useFetchApplicationById } from "@app/queries/applications"; import { useFetchArchetypeById } from "@app/queries/archetypes"; import { IdentifiedRisksTable } from "../reports/components/identified-risks-table"; -import { AssessmentLandscape } from "../reports/components/assessment-landscape"; +import { ApplicationAssessmentDonutChart } from "../../components/application-assessment-donut-chart/application-assessment-donut-chart"; const ReviewPage: React.FC = () => { const { t } = useTranslation(); @@ -112,12 +112,22 @@ const ReviewPage: React.FC = () => { + {application?.assessments?.length || + archetype?.assessments?.length ? ( + + + + ) : null} - {(application?.assessments?.length || archetype?.assessments?.length) && ( + {application?.assessments?.length || archetype?.assessments?.length ? ( @@ -126,21 +136,16 @@ const ReviewPage: React.FC = () => { - - )} + ) : null} ); };