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}
>
);
};