From 3a69a0ba81fbfd6c60956a942cc3a10bd3651ced Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Tue, 5 Dec 2023 16:20:04 -0500 Subject: [PATCH] :sparkles: Add missing risk chart for assessment review Signed-off-by: ibolton336 --- .../application-assessment-donut-chart.tsx | 179 ++++++++++++++++++ client/src/app/pages/review/review-page.tsx | 15 +- 2 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 client/src/app/components/application-assessment-donut-chart/application-assessment-donut-chart.tsx 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/review/review-page.tsx b/client/src/app/pages/review/review-page.tsx index 5cd57dd23e..8b3343100b 100644 --- a/client/src/app/pages/review/review-page.tsx +++ b/client/src/app/pages/review/review-page.tsx @@ -27,6 +27,7 @@ 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 +113,22 @@ const ReviewPage: React.FC = () => { + {application?.assessments?.length || + archetype?.assessments?.length ? ( + + + + ) : null} - {(application?.assessments?.length || archetype?.assessments?.length) && ( + {application?.assessments?.length || archetype?.assessments?.length ? ( @@ -140,7 +151,7 @@ const ReviewPage: React.FC = () => { - )} + ) : null} ); };