From e8666b58c6326daa78de293fcabf4e4a5620afa6 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Mon, 27 Nov 2023 13:02:01 -0500 Subject: [PATCH 01/16] :sparkles: Enhance identified-risks table to include archetype apps in calculations Signed-off-by: ibolton336 --- client/src/app/api/models.ts | 4 ++ .../identified-risks-table.tsx | 44 +++++++++++++----- client/src/app/queries/assessments.ts | 46 ++++++++++++++++++- 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 2f2c07102..9381dcec8 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -773,3 +773,7 @@ export interface SectionWithQuestionOrder extends Section { export interface AssessmentWithSectionOrder extends Assessment { sections: SectionWithQuestionOrder[]; } + +export interface AssessmentWithArchetypeApplications extends Assessment { + archetypeApplications: Ref[]; +} 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 74507382f..1a602bbd5 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 @@ -1,6 +1,6 @@ import React from "react"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; -import { useFetchAssessments } from "@app/queries/assessments"; +import { useFetchAssessmentsWithArchetypeApplications } from "@app/queries/assessments"; import { useTranslation } from "react-i18next"; import { Ref } from "@app/api/models"; import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; @@ -20,7 +20,8 @@ export const IdentifiedRisksTable: React.FC< > = () => { const { t } = useTranslation(); - const { assessments } = useFetchAssessments(); + const { assessmentsWithArchetypeApplications } = + useFetchAssessmentsWithArchetypeApplications(); interface ITableRowData { assessmentName: string; @@ -34,7 +35,25 @@ export const IdentifiedRisksTable: React.FC< const tableData: ITableRowData[] = []; // ... - assessments.forEach((assessment) => { + assessmentsWithArchetypeApplications.forEach((assessment) => { + let combinedApplications = assessment.application + ? [assessment.application] + : []; + + combinedApplications = combinedApplications.concat( + assessment.archetypeApplications || [] + ); + + const uniqueApplications = combinedApplications.reduce( + (acc: Ref[], current) => { + if (!acc.find((item) => item?.id === current.id)) { + // Assuming 'id' is the unique identifier + acc.push(current); + } + return acc; + }, + [] + ); assessment.sections.forEach((section) => { section.questions.forEach((question) => { question.answers.forEach((answer) => { @@ -52,14 +71,15 @@ export const IdentifiedRisksTable: React.FC< if (existingItemIndex !== -1) { const existingItem = tableData[existingItemIndex]; - if ( - assessment.application && - !existingItem.applications - .map((app) => app.name) - .includes(assessment.application.name) - ) { - existingItem.applications.push(assessment.application); - } + uniqueApplications.forEach((application) => { + if ( + !existingItem.applications.some( + (app) => app.id === application.id + ) + ) { + existingItem.applications.push(application); + } + }); } else { tableData.push({ section: section.name, @@ -150,7 +170,7 @@ export const IdentifiedRisksTable: React.FC< diff --git a/client/src/app/queries/assessments.ts b/client/src/app/queries/assessments.ts index d805b47ca..8b47de1e0 100644 --- a/client/src/app/queries/assessments.ts +++ b/client/src/app/queries/assessments.ts @@ -1,9 +1,15 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMemo } from "react"; +import { + useMutation, + useQueries, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import { createAssessment, deleteAssessment, + getArchetypeById, getAssessmentById, getAssessments, getAssessmentsByItemId, @@ -13,6 +19,7 @@ import { AxiosError } from "axios"; import { Assessment, AssessmentWithSectionOrder, + AssessmentWithArchetypeApplications, InitialAssessment, } from "@app/api/models"; import { QuestionnairesQueryKey } from "./questionnaires"; @@ -210,3 +217,40 @@ const removeSectionOrderFromQuestions = ( })), }; }; + +export const useFetchAssessmentsWithArchetypeApplications = () => { + const { assessments, isFetching: assessmentsLoading } = useFetchAssessments(); + + const archetypeQueries = useQueries({ + queries: + assessments?.map((assessment) => ({ + queryKey: ["archetype", assessment.archetype?.id], + queryFn: () => + assessment.archetype?.id + ? getArchetypeById(assessment.archetype.id) + : undefined, + enabled: !!assessment.archetype?.id, + })) || [], + }); + + const isArchetypesLoading = archetypeQueries.some((query) => query.isLoading); + const archetypesData = archetypeQueries + .map((query) => query.data) + .filter(Boolean); + + const assessmentsWithArchetypeApplications: AssessmentWithArchetypeApplications[] = + assessments.map((assessment, index) => { + const archetypeInfo = archetypesData[index]; + return { + ...assessment, + archetypeApplications: archetypeInfo.applications + ? archetypeInfo.applications + : [], + }; + }); + + return { + assessmentsWithArchetypeApplications, + isLoading: assessmentsLoading || isArchetypesLoading, + }; +}; From 4ca6b66225c595b980b91ec5ab1aa731d10b2cfa Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Mon, 27 Nov 2023 13:45:24 -0500 Subject: [PATCH 02/16] Update count to reflect apps inheriting assessments from archetypes Port over initial filter values change Properly serialize & deserialize filters to match server side implementation Signed-off-by: ibolton336 --- .../MultiselectFilterControl.tsx | 2 -- .../filtering/useFilterState.ts | 5 ++- .../applications-table/applications-table.tsx | 24 +++++++++++--- .../identified-risks-table.tsx | 33 +++++++++++++++---- client/src/app/queries/assessments.ts | 32 ++++++++++-------- 5 files changed, 69 insertions(+), 27 deletions(-) diff --git a/client/src/app/components/FilterToolbar/MultiselectFilterControl.tsx b/client/src/app/components/FilterToolbar/MultiselectFilterControl.tsx index f79e677df..627faa55f 100644 --- a/client/src/app/components/FilterToolbar/MultiselectFilterControl.tsx +++ b/client/src/app/components/FilterToolbar/MultiselectFilterControl.tsx @@ -138,8 +138,6 @@ export const MultiselectFilterControl = ({ const input = textInput?.toLowerCase(); return renderSelectOptions((optionProps, groupName) => { - if (!input) return false; - // TODO: Checking for a filter match against the key or the value may not be desirable. return ( groupName?.toLowerCase().includes(input) || diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 1b2b267d6..a7deb2bf1 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -44,6 +44,7 @@ export type IFilterStateArgs< * Definitions of the filters to be used (must include `getItemValue` functions for each category when performing filtering locally) */ filterCategories: FilterCategory[]; + initialFilterValues?: IFilterValues; } >; @@ -62,6 +63,8 @@ export const useFilterState = < IFeaturePersistenceArgs ): IFilterState => { const { isFilterEnabled, persistTo = "state", persistenceKeyPrefix } = args; + const initialFilterValues: IFilterValues = + (isFilterEnabled && args.initialFilterValues) || {}; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -71,7 +74,7 @@ export const useFilterState = < "filters" >({ isEnabled: !!isFilterEnabled, - defaultValue: {}, + defaultValue: initialFilterValues, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), 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 7afc874fb..91d86f667 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -67,8 +67,11 @@ import { checkAccess } from "@app/utils/rbac-utils"; import WarningTriangleIcon from "@patternfly/react-icons/dist/esm/icons/warning-triangle-icon"; // Hooks -import { useQueryClient } from "@tanstack/react-query"; -import { useLocalTableControls } from "@app/hooks/table-controls"; +import { useIsFetching, useQueryClient } from "@tanstack/react-query"; +import { + deserializeFilterUrlParams, + useLocalTableControls, +} from "@app/hooks/table-controls"; // Queries import { Application, Assessment, Ref, Task } from "@app/api/models"; @@ -111,6 +114,8 @@ export const ApplicationsTable: React.FC = () => { const history = useHistory(); const token = keycloak.tokenParsed; + const isFetching = useIsFetching(); + const { pushNotification } = React.useContext(NotificationsContext); const { identities } = useFetchIdentities(); @@ -303,6 +308,11 @@ export const ApplicationsTable: React.FC = () => { } }; + const urlParams = new URLSearchParams(window.location.search); + const filters = urlParams.get("filters"); + + const deserializedFilterValues = deserializeFilterUrlParams({ filters }); + const tableControls = useLocalTableControls({ idProperty: "id", items: applications || [], @@ -321,6 +331,7 @@ export const ApplicationsTable: React.FC = () => { isActiveItemEnabled: true, sortableColumns: ["name", "businessService", "tags", "effort"], initialSort: { columnKey: "name", direction: "asc" }, + initialFilterValues: deserializedFilterValues, getSortValues: (app) => ({ name: app.name, businessService: app.businessService?.name || "", @@ -331,12 +342,17 @@ export const ApplicationsTable: React.FC = () => { { key: "name", title: t("terms.name"), - type: FilterType.search, + type: FilterType.multiselect, placeholderText: t("actions.filterBy", { what: t("terms.name").toLowerCase(), }) + "...", getItemValue: (item) => item?.name || "", + selectOptions: [ + ...new Set( + applications.map((application) => application.name).filter(Boolean) + ), + ].map((name) => ({ key: name, value: name })), }, { key: "archetypes", @@ -690,7 +706,7 @@ export const ApplicationsTable: React.FC = () => { return ( } >
{ if (!acc.find((item) => item?.id === current.id)) { - // Assuming 'id' is the unique identifier acc.push(current); } return acc; @@ -85,9 +89,7 @@ export const IdentifiedRisksTable: React.FC< section: section.name, question: question.text, answer: answer.text, - applications: assessment.application - ? [assessment.application] - : [], + applications: uniqueApplications ? uniqueApplications : [], assessmentName: assessment.questionnaire.name, questionId: itemId, }); @@ -200,7 +202,13 @@ export const IdentifiedRisksTable: React.FC< {item.answer ?? "N/A"} - {item?.applications.length ?? "N/A"} + {item?.applications.length ? ( + + {item.applications.length} + + ) : ( + "N/A" + )} @@ -212,3 +220,16 @@ export const IdentifiedRisksTable: React.FC< ); }; + +const getApplicationsUrl = (applications: Ref[]) => { + const filterValues = { + name: applications.map((app) => app.name), + }; + + const serializedParams = serializeFilterUrlParams(filterValues); + + const queryString = serializedParams.filters + ? `filters=${serializedParams.filters}` + : ""; + return `${Paths.applications}?${queryString}`; +}; diff --git a/client/src/app/queries/assessments.ts b/client/src/app/queries/assessments.ts index 8b47de1e0..3ec9b8925 100644 --- a/client/src/app/queries/assessments.ts +++ b/client/src/app/queries/assessments.ts @@ -19,7 +19,6 @@ import { AxiosError } from "axios"; import { Assessment, AssessmentWithSectionOrder, - AssessmentWithArchetypeApplications, InitialAssessment, } from "@app/api/models"; import { QuestionnairesQueryKey } from "./questionnaires"; @@ -234,20 +233,25 @@ export const useFetchAssessmentsWithArchetypeApplications = () => { }); const isArchetypesLoading = archetypeQueries.some((query) => query.isLoading); - const archetypesData = archetypeQueries - .map((query) => query.data) - .filter(Boolean); - const assessmentsWithArchetypeApplications: AssessmentWithArchetypeApplications[] = - assessments.map((assessment, index) => { - const archetypeInfo = archetypesData[index]; - return { - ...assessment, - archetypeApplications: archetypeInfo.applications - ? archetypeInfo.applications - : [], - }; - }); + const archetypeApplicationsMap = new Map(); + archetypeQueries.forEach((query, index) => { + if (query.data && assessments[index].archetype?.id) { + archetypeApplicationsMap.set( + assessments[index]?.archetype?.id, + query.data.applications + ); + } + }); + + const assessmentsWithArchetypeApplications = assessments.map( + (assessment) => ({ + ...assessment, + archetypeApplications: assessment.archetype?.id + ? archetypeApplicationsMap.get(assessment.archetype.id) || [] + : [], + }) + ); return { assessmentsWithArchetypeApplications, From 4d40495262154b32646710192246cf59a2ec2d13 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Mon, 27 Nov 2023 16:30:03 -0500 Subject: [PATCH 03/16] Add expandable for rationale and mitigation Signed-off-by: ibolton336 --- .../identified-risks-table.tsx | 111 ++++++++++++------ 1 file changed, 74 insertions(+), 37 deletions(-) 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 9c90221a7..540fe5e15 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 @@ -2,7 +2,7 @@ import React from "react"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import { useFetchAssessmentsWithArchetypeApplications } from "@app/queries/assessments"; import { useTranslation } from "react-i18next"; -import { Ref } from "@app/api/models"; +import { Answer, Question, Ref } from "@app/api/models"; import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { TableHeaderContentWithControls, @@ -14,9 +14,17 @@ import { useLocalTableControls, } from "@app/hooks/table-controls"; import { SimplePagination } from "@app/components/SimplePagination"; -import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core"; +import { + TextContent, + Toolbar, + ToolbarContent, + ToolbarItem, + Text, + Divider, +} from "@patternfly/react-core"; import { Link } from "react-router-dom"; import { Paths } from "@app/Paths"; +import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; export interface IIdentifiedRisksTableProps {} @@ -32,8 +40,8 @@ export const IdentifiedRisksTable: React.FC< assessmentName: string; questionId: string; section: string; - question: string; - answer: string; + question: Question; + answer: Answer; applications: Ref[]; } @@ -87,8 +95,8 @@ export const IdentifiedRisksTable: React.FC< } else { tableData.push({ section: section.name, - question: question.text, - answer: answer.text, + question: question, + answer: answer, applications: uniqueApplications ? uniqueApplications : [], assessmentName: assessment.questionnaire.name, questionId: itemId, @@ -117,11 +125,13 @@ export const IdentifiedRisksTable: React.FC< getSortValues: (item) => ({ assessmentName: item.assessmentName, section: item.section, - question: item.question, - answer: item.answer, + question: item.question.text, + answer: item.answer.text, applications: item.applications.length, }), sortableColumns: ["assessmentName", "section", "question", "answer"], + isExpansionEnabled: true, + expandableVariant: "single", }); const { @@ -135,7 +145,9 @@ export const IdentifiedRisksTable: React.FC< getThProps, getTrProps, getTdProps, + getExpandedContentTdProps, }, + expansionDerivedState: { isCellExpanded }, } = tableControls; return ( @@ -183,35 +195,60 @@ export const IdentifiedRisksTable: React.FC< {currentPageItems?.map((item, rowIndex) => { return ( - - - - {item.assessmentName} - - - {item?.section ?? "N/A"} - - - {item?.question ?? "N/A"} - - - {item.answer ?? "N/A"} - - - {item?.applications.length ? ( - - {item.applications.length} - - ) : ( - "N/A" - )} - - - + <> + + + + {item.assessmentName} + + + {item?.section ?? "N/A"} + + + {item?.question.text ?? "N/A"} + + + {item.answer.text ?? "N/A"} + + + {item?.applications.length ? ( + + {item.applications.length} + + ) : ( + "N/A" + )} + + + + {isCellExpanded(item) ? ( + + + + + Rationale + + {item?.answer?.rationale + ? item.answer.rationale + : "N/A"} + + + + Mitigation + + {item?.answer?.mitigation + ? item.answer.mitigation + : "N/A"} + + + + + ) : null} + ); })} From 10b6ec395373eb018bb76ca7f03f72861f5e6da1 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Mon, 27 Nov 2023 16:42:45 -0500 Subject: [PATCH 04/16] Add risk column to table with icon Signed-off-by: ibolton336 --- .../components/answer-table/answer-table.tsx | 16 ++------------ .../app/components/risk-icon/risk-icon.tsx | 22 +++++++++++++++++++ .../identified-risks-table.tsx | 6 +++++ 3 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 client/src/app/components/risk-icon/risk-icon.tsx diff --git a/client/src/app/components/answer-table/answer-table.tsx b/client/src/app/components/answer-table/answer-table.tsx index 0ca99f038..4a6d903ec 100644 --- a/client/src/app/components/answer-table/answer-table.tsx +++ b/client/src/app/components/answer-table/answer-table.tsx @@ -16,6 +16,7 @@ import { IconedStatus } from "@app/components/IconedStatus"; import { TimesCircleIcon } from "@patternfly/react-icons"; import { WarningTriangleIcon } from "@patternfly/react-icons"; import { List, ListItem } from "@patternfly/react-core"; +import RiskIcon from "../risk-icon/risk-icon"; export interface IAnswerTableProps { answers: Answer[]; @@ -45,19 +46,6 @@ const AnswerTable: React.FC = ({ propHelpers: { tableProps, getThProps, getTrProps, getTdProps }, } = tableControls; - const getIconByRisk = (risk: string): React.ReactElement => { - switch (risk) { - case "green": - return ; - case "red": - return } status="danger" />; - case "yellow": - return } status="warning" />; - default: - return ; - } - }; - return ( <> @@ -126,7 +114,7 @@ const AnswerTable: React.FC = ({ {answer.text} diff --git a/client/src/app/components/risk-icon/risk-icon.tsx b/client/src/app/components/risk-icon/risk-icon.tsx new file mode 100644 index 000000000..d4a7ddf52 --- /dev/null +++ b/client/src/app/components/risk-icon/risk-icon.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { TimesCircleIcon, WarningTriangleIcon } from "@patternfly/react-icons"; +import { IconedStatus } from "@app/components/IconedStatus"; + +interface RiskIconProps { + risk: string; +} + +const RiskIcon: React.FC = ({ risk }) => { + switch (risk) { + case "green": + return ; + case "red": + return } status="danger" />; + case "yellow": + return } status="warning" />; + default: + return ; + } +}; + +export default RiskIcon; 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 540fe5e15..45885f41a 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 @@ -25,6 +25,7 @@ import { import { Link } from "react-router-dom"; import { Paths } from "@app/Paths"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import RiskIcon from "@app/components/risk-icon/risk-icon"; export interface IIdentifiedRisksTableProps {} @@ -116,6 +117,7 @@ export const IdentifiedRisksTable: React.FC< section: "Section", question: "Question", answer: "Answer", + risk: "Risk", applications: "Applications", }, variant: "compact", @@ -177,6 +179,7 @@ export const IdentifiedRisksTable: React.FC< + @@ -214,6 +217,9 @@ export const IdentifiedRisksTable: React.FC< +
- {getIconByRisk(answer.risk)} +
Section Question AnswerRisk Application {item.answer.text ?? "N/A"} + + {item?.applications.length ? ( From b7bcf7d118218b9d3c355d1fa3b4ba50efac274a Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Tue, 28 Nov 2023 14:50:59 -0500 Subject: [PATCH 05/16] Add identified risks table to review page Signed-off-by: ibolton336 --- client/src/app/api/models.ts | 3 +- .../identified-risks-table.tsx | 42 ++- .../application-details.tsx | 64 ----- .../components/application-details/index.ts | 1 - .../application-details.test.tsx.snap | 252 ------------------ .../tests/application-details.test.tsx | 46 ---- client/src/app/pages/review/review-page.tsx | 98 ++++--- client/src/app/queries/assessments.ts | 8 +- 8 files changed, 96 insertions(+), 418 deletions(-) delete mode 100644 client/src/app/pages/review/components/application-details/application-details.tsx delete mode 100644 client/src/app/pages/review/components/application-details/index.ts delete mode 100644 client/src/app/pages/review/components/application-details/tests/__snapshots__/application-details.test.tsx.snap delete mode 100644 client/src/app/pages/review/components/application-details/tests/application-details.test.tsx diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 9381dcec8..74c68fe89 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -774,6 +774,7 @@ export interface AssessmentWithSectionOrder extends Assessment { sections: SectionWithQuestionOrder[]; } -export interface AssessmentWithArchetypeApplications extends Assessment { +export interface AssessmentWithArchetypeApplications + extends AssessmentWithSectionOrder { archetypeApplications: Ref[]; } 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 45885f41a..1c65c2f0e 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 @@ -2,7 +2,12 @@ import React from "react"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import { useFetchAssessmentsWithArchetypeApplications } from "@app/queries/assessments"; import { useTranslation } from "react-i18next"; -import { Answer, Question, Ref } from "@app/api/models"; +import { + Answer, + AssessmentWithArchetypeApplications, + Question, + Ref, +} from "@app/api/models"; import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { TableHeaderContentWithControls, @@ -27,11 +32,13 @@ import { Paths } from "@app/Paths"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import RiskIcon from "@app/components/risk-icon/risk-icon"; -export interface IIdentifiedRisksTableProps {} +export interface IIdentifiedRisksTableProps { + assessmentRefs?: Ref[]; +} -export const IdentifiedRisksTable: React.FC< - IIdentifiedRisksTableProps -> = () => { +export const IdentifiedRisksTable: React.FC = ({ + assessmentRefs, +}) => { const { t } = useTranslation(); const { assessmentsWithArchetypeApplications } = @@ -48,8 +55,24 @@ export const IdentifiedRisksTable: React.FC< const tableData: ITableRowData[] = []; - // ... - assessmentsWithArchetypeApplications.forEach((assessment) => { + const filterAssessmentsByRefs = ( + assessments: AssessmentWithArchetypeApplications[], + refs: Ref[] + ) => { + if (refs && refs.length > 0) { + return assessments.filter((assessment) => + refs.some((ref) => ref.id === assessment.id) + ); + } + return assessments; + }; + + const filteredAssessments = filterAssessmentsByRefs( + assessmentsWithArchetypeApplications, + assessmentRefs || [] + ); + + filteredAssessments.forEach((assessment) => { let combinedApplications = assessment.application ? [assessment.application] : []; @@ -260,6 +283,11 @@ export const IdentifiedRisksTable: React.FC<
+ ); }; diff --git a/client/src/app/pages/review/components/application-details/application-details.tsx b/client/src/app/pages/review/components/application-details/application-details.tsx deleted file mode 100644 index bab21fcfb..000000000 --- a/client/src/app/pages/review/components/application-details/application-details.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; - -import { - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - List, -} from "@patternfly/react-core"; - -import { Application, Assessment } from "@app/api/models"; -import { useFetchQuestionnaires } from "@app/queries/questionnaires"; - -export interface IApplicationDetailsProps { - application?: Application; - assessment?: Assessment; -} - -export const ApplicationDetails: React.FC = ({ - application, - assessment, -}) => { - const { questionnaires } = useFetchQuestionnaires(); - - const matchingQuestionnaire = questionnaires.find( - (questionnaire) => questionnaire.id === assessment?.questionnaire?.id - ); - const { t } = useTranslation(); - if (!matchingQuestionnaire || !application) { - return null; - } - - return ( - - - {t("terms.applicationName")} - - {application.name} - - - - {t("terms.description")} - - {application.description} - - - - {t("terms.assessmentNotes")} - - - {/* {matchingQuestionnaire.sections - .filter((f) => f.comment && f.comment.trim().length > 0) - .map((category, i) => ( - - {category.title}: {category.comment} - - ))} */} - - - - - ); -}; diff --git a/client/src/app/pages/review/components/application-details/index.ts b/client/src/app/pages/review/components/application-details/index.ts deleted file mode 100644 index 7ecfc28a6..000000000 --- a/client/src/app/pages/review/components/application-details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ApplicationDetails } from "./application-details"; diff --git a/client/src/app/pages/review/components/application-details/tests/__snapshots__/application-details.test.tsx.snap b/client/src/app/pages/review/components/application-details/tests/__snapshots__/application-details.test.tsx.snap deleted file mode 100644 index 0c82534d4..000000000 --- a/client/src/app/pages/review/components/application-details/tests/__snapshots__/application-details.test.tsx.snap +++ /dev/null @@ -1,252 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AppTable Renders without crashing 1`] = ` -{ - "asFragment": [Function], - "baseElement": -
-
-
-
- - terms.applicationName - -
-
-
- myApp -
-
-
-
-
- - terms.description - -
-
-
- myDescription -
-
-
-
-
- - terms.assessmentNotes - -
-
-
-
    -
  • - title1 - : - comments1 -
  • -
  • - title2 - : - comments2 -
  • -
  • - title3 - : - comments3 -
  • -
-
-
-
-
-
- , - "container":
-
-
-
- - terms.applicationName - -
-
-
- myApp -
-
-
-
-
- - terms.description - -
-
-
- myDescription -
-
-
-
-
- - terms.assessmentNotes - -
-
-
-
    -
  • - title1 - : - comments1 -
  • -
  • - title2 - : - comments2 -
  • -
  • - title3 - : - comments3 -
  • -
-
-
-
-
-
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; diff --git a/client/src/app/pages/review/components/application-details/tests/application-details.test.tsx b/client/src/app/pages/review/components/application-details/tests/application-details.test.tsx deleted file mode 100644 index 31443d7f1..000000000 --- a/client/src/app/pages/review/components/application-details/tests/application-details.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Application } from "@app/api/models"; - -describe("AppTable", () => { - it.skip("Renders without crashing", () => { - const application: Application = { - id: 1, - name: "myApp", - description: "myDescription", - migrationWave: null, - }; - - // const assessment: Assessment = { - // applicationId: 1, - // status: "COMPLETE", - // questionnaire: { - // categories: [ - // { - // id: 1, - // order: 1, - // questions: [], - // title: "title1", - // comment: "comments1", - // }, - // { - // id: 2, - // order: 2, - // questions: [], - // title: "title2", - // comment: "comments2", - // }, - // { - // id: 3, - // order: 3, - // questions: [], - // title: "title3", - // comment: "comments3", - // }, - // ], - // }, - // }; - // const wrapper = render( - // - // ); - // expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/client/src/app/pages/review/review-page.tsx b/client/src/app/pages/review/review-page.tsx index 9a0814801..f691de12d 100644 --- a/client/src/app/pages/review/review-page.tsx +++ b/client/src/app/pages/review/review-page.tsx @@ -1,13 +1,17 @@ -import React from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Bullseye, + Card, + CardBody, + CardHeader, FormSection, Grid, GridItem, PageSection, Text, + TextContent, } from "@patternfly/react-core"; import BanIcon from "@patternfly/react-icons/dist/esm/icons/ban-icon"; @@ -16,19 +20,18 @@ import { ReviewForm } from "./components/review-form"; import { SimpleEmptyState } from "@app/components/SimpleEmptyState"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; -import { ApplicationAssessmentDonutChart } from "./components/application-assessment-donut-chart/application-assessment-donut-chart"; -import QuestionnaireSummary, { - SummaryType, -} from "@app/components/questionnaire-summary/questionnaire-summary"; import { PageHeader } from "@app/components/PageHeader"; import { useFetchReviewById } from "@app/queries/reviews"; 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"; const ReviewPage: React.FC = () => { const { t } = useTranslation(); + const [isRiskCardOpen, setIsRiskCardOpen] = useState(false); + const { applicationId, archetypeId } = useParams(); const isArchetype = useIsArchetype(); @@ -38,7 +41,6 @@ const ReviewPage: React.FC = () => { const { review, fetchError, isFetching } = useFetchReviewById( isArchetype ? archetype?.review?.id : application?.review?.id ); - const assessment = undefined; const breadcrumbs = [ ...(isArchetype ? [ @@ -53,10 +55,10 @@ const ReviewPage: React.FC = () => { path: Paths.applications, }, ]), - // { - // title: t("terms.review"), - // path: Paths.applicationsReview, - // }, + { + title: t("terms.review"), + path: Paths.applicationsReview, + }, ]; if (fetchError) { @@ -94,40 +96,50 @@ const ReviewPage: React.FC = () => { breadcrumbs={breadcrumbs} /> - - }> - - -
- {/* - - */} - - - -
-
- {assessment && ( - - - - )} -
-
- {assessment && ( - - )} + + + + }> + + +
+ + + + +
+
+ {/* + + */} +
+
+
+
+ {(application?.assessments?.length || archetype?.assessments?.length) && ( + + + + + {t("terms.assessmentSummary")} + + + + + + )} ); }; diff --git a/client/src/app/queries/assessments.ts b/client/src/app/queries/assessments.ts index 3ec9b8925..d070f4ef2 100644 --- a/client/src/app/queries/assessments.ts +++ b/client/src/app/queries/assessments.ts @@ -18,6 +18,7 @@ import { import { AxiosError } from "axios"; import { Assessment, + AssessmentWithArchetypeApplications, AssessmentWithSectionOrder, InitialAssessment, } from "@app/api/models"; @@ -244,14 +245,13 @@ export const useFetchAssessmentsWithArchetypeApplications = () => { } }); - const assessmentsWithArchetypeApplications = assessments.map( - (assessment) => ({ + const assessmentsWithArchetypeApplications: AssessmentWithArchetypeApplications[] = + assessments.map((assessment) => ({ ...assessment, archetypeApplications: assessment.archetype?.id ? archetypeApplicationsMap.get(assessment.archetype.id) || [] : [], - }) - ); + })); return { assessmentsWithArchetypeApplications, From beb6f4d91c2edff3473349b2104a1d594fd1e70d Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Tue, 28 Nov 2023 16:06:01 -0500 Subject: [PATCH 06/16] Add landscape to review page Signed-off-by: ibolton336 --- client/src/app/api/models.ts | 3 ++ .../identified-risks-table.tsx | 2 +- .../components/landscape/landscape.tsx | 20 ++++--- client/src/app/pages/reports/reports.tsx | 54 ++++++++++++------- client/src/app/pages/review/review-page.tsx | 28 ++++++---- client/src/app/utils/model-utils.tsx | 9 ++++ 6 files changed, 82 insertions(+), 34 deletions(-) diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 74c68fe89..aaf47dc38 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -72,6 +72,9 @@ export interface Ref { id: number; name: string; } +export interface IdRef { + id: number; +} export interface JobFunction { id: number; 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 1c65c2f0e..84da7e4ef 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 @@ -246,7 +246,7 @@ export const IdentifiedRisksTable: React.FC = ({ {item?.applications.length ? ( - {item.applications.length} + {item.applications.length} Application(s) ) : ( "N/A" diff --git a/client/src/app/pages/reports/components/landscape/landscape.tsx b/client/src/app/pages/reports/components/landscape/landscape.tsx index 6a9fec9b3..56cd945e5 100644 --- a/client/src/app/pages/reports/components/landscape/landscape.tsx +++ b/client/src/app/pages/reports/components/landscape/landscape.tsx @@ -3,9 +3,10 @@ import { useTranslation } from "react-i18next"; import { Flex, FlexItem, Skeleton } from "@patternfly/react-core"; import { RISK_LIST } from "@app/Constants"; -import { Assessment, Questionnaire } from "@app/api/models"; +import { Assessment, IdRef, Questionnaire } from "@app/api/models"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { Donut } from "./donut"; +import { useFetchAssessmentsWithArchetypeApplications } from "@app/queries/assessments"; interface IAggregateRiskData { green: number; @@ -59,23 +60,30 @@ interface ILandscapeProps { * The set of assessments for the selected questionnaire. Risk values will be * aggregated from the individual assessment risks. */ - assessments: Assessment[]; + assessmentRefs?: IdRef[]; } export const Landscape: React.FC = ({ questionnaire, - assessments, + assessmentRefs, }) => { const { t } = useTranslation(); + const { assessmentsWithArchetypeApplications } = + useFetchAssessmentsWithArchetypeApplications(); + + const filteredAssessments = assessmentsWithArchetypeApplications.filter( + (assessment) => assessmentRefs?.some((ref) => ref.id === assessment.id) + ); + const landscapeData = useMemo( - () => aggregateRiskData(assessments), - [assessments] + () => aggregateRiskData(filteredAssessments), + [filteredAssessments] ); return ( diff --git a/client/src/app/pages/reports/reports.tsx b/client/src/app/pages/reports/reports.tsx index 201b3bcad..c1269e02d 100644 --- a/client/src/app/pages/reports/reports.tsx +++ b/client/src/app/pages/reports/reports.tsx @@ -38,6 +38,7 @@ import { Landscape } from "./components/landscape"; import AdoptionCandidateTable from "./components/adoption-candidate-table/adoption-candidate-table"; import { AdoptionPlan } from "./components/adoption-plan"; import { IdentifiedRisksTable } from "./components/identified-risks-table"; +import { toIdRef } from "@app/utils/model-utils"; const ALL_QUESTIONNAIRES = -1; @@ -115,12 +116,40 @@ export const Reports: React.FC = () => { const answeredQuestionnaires: Questionnaire[] = isAssessmentsFetching || isQuestionnairesFetching ? [] - : assessments - .map((assessment) => assessment?.questionnaire?.id) - .filter((id) => id > 0) + : Array.from( + new Set( + assessments + .map((assessment) => assessment?.questionnaire?.id) + .filter((id) => id > 0) + ) + ) .map((id) => questionnairesById[id]) - .sort((a, b) => a.name.localeCompare(b.name)) - .filter((questionnaire) => questionnaire !== undefined); + .filter((questionnaire) => questionnaire !== undefined) + .sort((a, b) => a.name.localeCompare(b.name)); + + const isAllQuestionnairesSelected = + selectedQuestionnaireId === ALL_QUESTIONNAIRES; + + const questionnaire = isAllQuestionnairesSelected + ? null + : questionnairesById[selectedQuestionnaireId]; + + const assessmentRefs = isAllQuestionnairesSelected + ? assessments + .map((assessment) => { + const assessmentRef = toIdRef(assessment); + return assessmentRef; + }) + .filter(Boolean) + : assessments + .filter( + ({ questionnaire }) => questionnaire.id === selectedQuestionnaireId + ) + .map((assessment) => { + const assessmentRef = toIdRef(assessment); + return assessmentRef; + }) + .filter(Boolean); return ( <> @@ -197,19 +226,8 @@ export const Reports: React.FC = () => { - questionnaire.id === selectedQuestionnaireId - ) - } + questionnaire={questionnaire} + assessmentRefs={assessmentRefs} /> diff --git a/client/src/app/pages/review/review-page.tsx b/client/src/app/pages/review/review-page.tsx index f691de12d..c859441c2 100644 --- a/client/src/app/pages/review/review-page.tsx +++ b/client/src/app/pages/review/review-page.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { @@ -26,12 +26,11 @@ 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 { Landscape } from "../reports/components/landscape"; const ReviewPage: React.FC = () => { const { t } = useTranslation(); - const [isRiskCardOpen, setIsRiskCardOpen] = useState(false); - const { applicationId, archetypeId } = useParams(); const isArchetype = useIsArchetype(); @@ -113,12 +112,17 @@ const ReviewPage: React.FC = () => {
- {/* - - */} + + {(application?.assessments?.length || + archetype?.assessments?.length) && ( + + )} +
@@ -132,6 +136,12 @@ const ReviewPage: React.FC = () => { {t("terms.assessmentSummary")} + [] = [ }, ]; +export const toIdRef = ( + source: RefLike | undefined +): IdRef | undefined => { + if (!source || !source.id) return undefined; + + return { id: source.id }; +}; + /** * Convert any object that looks like a `Ref` into a `Ref`. If the source object * is `undefined`, or doesn't look like a `Ref`, return `undefined`. From e2b647b220e4f9d3bcc40c231dde5782103652ae Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Tue, 28 Nov 2023 16:48:37 -0500 Subject: [PATCH 07/16] remove old cards Signed-off-by: ibolton336 --- .../application-landscape.tsx | 143 ++++++++++++++++++ .../donut.tsx | 0 .../components/application-landscape/index.ts | 1 + .../assessment-landscape.tsx} | 4 +- .../components/assessment-landscape/donut.tsx | 62 ++++++++ .../components/assessment-landscape/index.ts | 1 + .../identified-risks-table.tsx | 17 +-- .../reports/components/landscape/index.ts | 1 - client/src/app/pages/reports/reports.tsx | 77 +--------- client/src/app/pages/review/review-page.tsx | 6 +- 10 files changed, 226 insertions(+), 86 deletions(-) create mode 100644 client/src/app/pages/reports/components/application-landscape/application-landscape.tsx rename client/src/app/pages/reports/components/{landscape => application-landscape}/donut.tsx (100%) create mode 100644 client/src/app/pages/reports/components/application-landscape/index.ts rename client/src/app/pages/reports/components/{landscape/landscape.tsx => assessment-landscape/assessment-landscape.tsx} (97%) create mode 100644 client/src/app/pages/reports/components/assessment-landscape/donut.tsx create mode 100644 client/src/app/pages/reports/components/assessment-landscape/index.ts delete mode 100644 client/src/app/pages/reports/components/landscape/index.ts diff --git a/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx b/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx new file mode 100644 index 000000000..509d24c8a --- /dev/null +++ b/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx @@ -0,0 +1,143 @@ +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"; +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 IApplicationLandscapeProps { + /** + * 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 ApplicationLandscape: 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/landscape/donut.tsx b/client/src/app/pages/reports/components/application-landscape/donut.tsx similarity index 100% rename from client/src/app/pages/reports/components/landscape/donut.tsx rename to client/src/app/pages/reports/components/application-landscape/donut.tsx diff --git a/client/src/app/pages/reports/components/application-landscape/index.ts b/client/src/app/pages/reports/components/application-landscape/index.ts new file mode 100644 index 000000000..c94255222 --- /dev/null +++ b/client/src/app/pages/reports/components/application-landscape/index.ts @@ -0,0 +1 @@ +export { ApplicationLandscape } from "./application-landscape"; diff --git a/client/src/app/pages/reports/components/landscape/landscape.tsx b/client/src/app/pages/reports/components/assessment-landscape/assessment-landscape.tsx similarity index 97% rename from client/src/app/pages/reports/components/landscape/landscape.tsx rename to client/src/app/pages/reports/components/assessment-landscape/assessment-landscape.tsx index 56cd945e5..5243b861d 100644 --- a/client/src/app/pages/reports/components/landscape/landscape.tsx +++ b/client/src/app/pages/reports/components/assessment-landscape/assessment-landscape.tsx @@ -50,7 +50,7 @@ const aggregateRiskData = (assessments: Assessment[]): IAggregateRiskData => { }; }; -interface ILandscapeProps { +interface IAssessmentLandscapeProps { /** * The selected questionnaire or `null` if _all questionnaires_ is selected. */ @@ -63,7 +63,7 @@ interface ILandscapeProps { assessmentRefs?: IdRef[]; } -export const Landscape: React.FC = ({ +export const AssessmentLandscape: React.FC = ({ questionnaire, assessmentRefs, }) => { diff --git a/client/src/app/pages/reports/components/assessment-landscape/donut.tsx b/client/src/app/pages/reports/components/assessment-landscape/donut.tsx new file mode 100644 index 000000000..6d40bada9 --- /dev/null +++ b/client/src/app/pages/reports/components/assessment-landscape/donut.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { ChartDonut } from "@patternfly/react-charts"; +import { global_palette_black_300 as black } from "@patternfly/react-tokens"; + +import { + Bullseye, + Stack, + StackItem, + Text, + TextContent, +} from "@patternfly/react-core"; + +export interface IDonutProps { + id: string; + value: number; + total: number; + color: string; + riskLabel: string; + riskDescription?: string; +} + +export const Donut: React.FC = ({ + id, + value, + total, + color, + riskLabel, + riskDescription, +}) => { + const { t } = useTranslation(); + + return ( + + + + `${datum.x}: ${datum.y}`} + colorScale={[color, black.value]} + /> + + + + + {riskLabel} + {riskDescription} + + + + ); +}; diff --git a/client/src/app/pages/reports/components/assessment-landscape/index.ts b/client/src/app/pages/reports/components/assessment-landscape/index.ts new file mode 100644 index 000000000..06ef2e27b --- /dev/null +++ b/client/src/app/pages/reports/components/assessment-landscape/index.ts @@ -0,0 +1 @@ +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 84da7e4ef..b9806d55e 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 @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { Answer, AssessmentWithArchetypeApplications, + IdRef, Question, Ref, } from "@app/api/models"; @@ -33,7 +34,7 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import RiskIcon from "@app/components/risk-icon/risk-icon"; export interface IIdentifiedRisksTableProps { - assessmentRefs?: Ref[]; + assessmentRefs?: IdRef[]; } export const IdentifiedRisksTable: React.FC = ({ @@ -57,7 +58,7 @@ export const IdentifiedRisksTable: React.FC = ({ const filterAssessmentsByRefs = ( assessments: AssessmentWithArchetypeApplications[], - refs: Ref[] + refs: IdRef[] ) => { if (refs && refs.length > 0) { return assessments.filter((assessment) => @@ -73,13 +74,10 @@ export const IdentifiedRisksTable: React.FC = ({ ); filteredAssessments.forEach((assessment) => { - let combinedApplications = assessment.application - ? [assessment.application] - : []; - - combinedApplications = combinedApplications.concat( - assessment.archetypeApplications || [] - ); + const combinedApplications = [ + ...(assessment.application ? [assessment.application] : []), + ...(assessment.archetypeApplications || []), + ]; const uniqueApplications = combinedApplications.reduce( (acc: Ref[], current) => { @@ -90,6 +88,7 @@ export const IdentifiedRisksTable: React.FC = ({ }, [] ); + assessment.sections.forEach((section) => { section.questions.forEach((question) => { question.answers.forEach((answer) => { diff --git a/client/src/app/pages/reports/components/landscape/index.ts b/client/src/app/pages/reports/components/landscape/index.ts deleted file mode 100644 index b3d59d110..000000000 --- a/client/src/app/pages/reports/components/landscape/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Landscape } from "./landscape"; diff --git a/client/src/app/pages/reports/reports.tsx b/client/src/app/pages/reports/reports.tsx index c1269e02d..d19f6b2c6 100644 --- a/client/src/app/pages/reports/reports.tsx +++ b/client/src/app/pages/reports/reports.tsx @@ -2,17 +2,13 @@ import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Bullseye, - Button, - ButtonVariant, Card, CardBody, - CardExpandableContent, CardHeader, CardTitle, MenuToggle, PageSection, PageSectionVariants, - Popover, Select, SelectOption, Split, @@ -22,7 +18,6 @@ import { Text, TextContent, } from "@patternfly/react-core"; -import { HelpIcon } from "@patternfly/react-icons"; import { Questionnaire } from "@app/api/models"; import { useFetchApplications } from "@app/queries/applications"; @@ -34,11 +29,9 @@ import { ConditionalRender } from "@app/components/ConditionalRender"; import { StateError } from "@app/components/StateError"; import { ApplicationSelectionContextProvider } from "./application-selection-context"; -import { Landscape } from "./components/landscape"; -import AdoptionCandidateTable from "./components/adoption-candidate-table/adoption-candidate-table"; -import { AdoptionPlan } from "./components/adoption-plan"; import { IdentifiedRisksTable } from "./components/identified-risks-table"; import { toIdRef } from "@app/utils/model-utils"; +import { ApplicationLandscape } from "./components/application-landscape"; const ALL_QUESTIONNAIRES = -1; @@ -225,7 +218,7 @@ export const Reports: React.FC = () => { - @@ -233,64 +226,8 @@ export const Reports: React.FC = () => { - + - - - - {t("terms.adoptionCandidateDistribution")} - - - - - - - - - - - - setAdoptionPlanOpen((current) => !current)} - > - - - - {t("terms.suggestedAdoptionPlan")} - - {t("message.suggestedAdoptionPlanHelpText")} - - } - position="right" - > - - - - - - - - - {isAdoptionPlanOpen && } - - - - - - - setIsRiskCardOpen((current) => !current)} - > @@ -305,11 +242,9 @@ export const Reports: React.FC = () => { - - - {isRiskCardOpen && } - - + + + diff --git a/client/src/app/pages/review/review-page.tsx b/client/src/app/pages/review/review-page.tsx index c859441c2..2557885e0 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 { Landscape } from "../reports/components/landscape"; +import { AssessmentLandscape } from "../reports/components/assessment-landscape"; const ReviewPage: React.FC = () => { const { t } = useTranslation(); @@ -115,7 +115,7 @@ const ReviewPage: React.FC = () => { {(application?.assessments?.length || archetype?.assessments?.length) && ( - { {t("terms.assessmentSummary")} - Date: Tue, 28 Nov 2023 17:18:48 -0500 Subject: [PATCH 08/16] Add new application landscape logic Signed-off-by: ibolton336 --- client/public/locales/en/translation.json | 2 + .../application-landscape.tsx | 89 +++++++++++++------ .../application-landscape/donut.tsx | 4 +- .../components/assessment-landscape/donut.tsx | 2 - client/src/app/pages/reports/reports.tsx | 6 +- client/src/app/pages/review/review-page.tsx | 11 --- 6 files changed, 68 insertions(+), 46 deletions(-) diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index cfa6b25d2..8687ab048 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -91,6 +91,8 @@ "noDataStateBody": "Create a new {{what}} to start seeing data here.", "noDataStateTitle": "No {{what}} available", "Nquestions": "{{n}} questions", + "ofTotalApplications": "Of {{count}} application", + "ofTotalApplications_plural": "Of {{count}} applications", "ofTotalAssessments": "Of {{count}} assessment", "ofTotalAssessments_plural": "Of {{count}} assessments", "selectMany": "Select {{what}}", diff --git a/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx b/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx index 509d24c8a..c9b3498b0 100644 --- a/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx +++ b/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx @@ -3,10 +3,17 @@ 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 { + Application, + AssessmentWithArchetypeApplications, + IdRef, + Questionnaire, + Ref, +} from "@app/api/models"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { Donut } from "./donut"; import { useFetchAssessmentsWithArchetypeApplications } from "@app/queries/assessments"; +import { useFetchApplications } from "@app/queries/applications"; interface IAggregateRiskData { green: number; @@ -14,39 +21,70 @@ interface IAggregateRiskData { red: number; unknown: number; unassessed: number; - assessmentCount: number; + applicationsCount: number; } -const aggregateRiskData = (assessments: Assessment[]): IAggregateRiskData => { +const aggregateRiskData = ( + assessments: AssessmentWithArchetypeApplications[], + applications: Application[] +): IAggregateRiskData => { let low = 0; let medium = 0; let high = 0; let unknown = 0; + const processedAppIds = new Set(); // Set to track processed application IDs + + const findFullApplication = (ref: Ref) => { + return applications.find((app) => app.id === ref.id); + }; assessments?.forEach((assessment) => { - switch (assessment.risk) { - case "green": - low++; - break; - case "yellow": - medium++; - break; - case "red": - high++; - break; - case "unknown": - unknown++; - break; - } + const combinedApplications = [ + ...(assessment.application ? [assessment.application] : []), + ...(assessment.archetypeApplications || []), + ]; + + const uniqueApplications = combinedApplications.reduce( + (acc: Ref[], current) => { + if (!acc.find((item) => item?.id === current.id)) { + acc.push(current); + } + return acc; + }, + [] + ); + + uniqueApplications.forEach((appRef) => { + const fullApp = findFullApplication(appRef); + if (fullApp && fullApp.risk && !processedAppIds.has(fullApp.id)) { + processedAppIds.add(fullApp.id); + + switch (fullApp.risk) { + case "green": + low++; + break; + case "yellow": + medium++; + break; + case "red": + high++; + break; + case "unknown": + unknown++; + break; + } + } + }); }); + const unassessed = applications.length - processedAppIds.size; return { green: low, yellow: medium, red: high, unknown, - unassessed: assessments.length - low - medium - high, - assessmentCount: assessments.length, + unassessed, + applicationsCount: processedAppIds.size, }; }; @@ -71,14 +109,15 @@ export const ApplicationLandscape: React.FC = ({ const { assessmentsWithArchetypeApplications } = useFetchAssessmentsWithArchetypeApplications(); + const { data: applications } = useFetchApplications(); const filteredAssessments = assessmentsWithArchetypeApplications.filter( (assessment) => assessmentRefs?.some((ref) => ref.id === assessment.id) ); const landscapeData = useMemo( - () => aggregateRiskData(filteredAssessments), - [filteredAssessments] + () => aggregateRiskData(filteredAssessments, applications), + [filteredAssessments, applications] ); return ( @@ -100,7 +139,7 @@ export const ApplicationLandscape: React.FC = ({ = ({ = ({ = ({ = ({ total, color, riskLabel, - riskDescription, }) => { const { t } = useTranslation(); @@ -38,7 +37,7 @@ export const Donut: React.FC = ({ = ({ {riskLabel} - {riskDescription} diff --git a/client/src/app/pages/reports/components/assessment-landscape/donut.tsx b/client/src/app/pages/reports/components/assessment-landscape/donut.tsx index 6d40bada9..346720641 100644 --- a/client/src/app/pages/reports/components/assessment-landscape/donut.tsx +++ b/client/src/app/pages/reports/components/assessment-landscape/donut.tsx @@ -27,7 +27,6 @@ export const Donut: React.FC = ({ total, color, riskLabel, - riskDescription, }) => { const { t } = useTranslation(); @@ -54,7 +53,6 @@ export const Donut: React.FC = ({ {riskLabel} - {riskDescription} diff --git a/client/src/app/pages/reports/reports.tsx b/client/src/app/pages/reports/reports.tsx index d19f6b2c6..db2c377c0 100644 --- a/client/src/app/pages/reports/reports.tsx +++ b/client/src/app/pages/reports/reports.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from "react"; +import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Bullseye, @@ -71,10 +71,6 @@ export const Reports: React.FC = () => { const [selectedQuestionnaireId, setSelectedQuestionnaireId] = React.useState(ALL_QUESTIONNAIRES); - const [isAdoptionPlanOpen, setAdoptionPlanOpen] = useState(false); - - const [isRiskCardOpen, setIsRiskCardOpen] = useState(false); - const pageHeaderSection = ( diff --git a/client/src/app/pages/review/review-page.tsx b/client/src/app/pages/review/review-page.tsx index 2557885e0..0acfe9f2d 100644 --- a/client/src/app/pages/review/review-page.tsx +++ b/client/src/app/pages/review/review-page.tsx @@ -112,17 +112,6 @@ const ReviewPage: React.FC = () => { - - {(application?.assessments?.length || - archetype?.assessments?.length) && ( - - )} - From e07e0ba2a50b06c19106fb107d2ec04ddfd4f640 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Wed, 29 Nov 2023 11:27:00 -0500 Subject: [PATCH 09/16] Add nullish coalescing and fix risk id leading to dupe table rows for same question Signed-off-by: ibolton336 --- .../app/hooks/table-controls/filtering/useFilterState.ts | 6 ++++-- .../application-landscape/application-landscape.tsx | 2 +- .../identified-risks-table/identified-risks-table.tsx | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index a7deb2bf1..890f9dd68 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -63,8 +63,10 @@ export const useFilterState = < IFeaturePersistenceArgs ): IFilterState => { const { isFilterEnabled, persistTo = "state", persistenceKeyPrefix } = args; - const initialFilterValues: IFilterValues = - (isFilterEnabled && args.initialFilterValues) || {}; + + const initialFilterValues: IFilterValues = isFilterEnabled + ? args?.initialFilterValues ?? {} + : {}; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 diff --git a/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx b/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx index c9b3498b0..310f2b902 100644 --- a/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx +++ b/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx @@ -41,7 +41,7 @@ const aggregateRiskData = ( assessments?.forEach((assessment) => { const combinedApplications = [ ...(assessment.application ? [assessment.application] : []), - ...(assessment.archetypeApplications || []), + ...(assessment.archetypeApplications ?? []), ]; const uniqueApplications = combinedApplications.reduce( 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 b9806d55e..78a425379 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 @@ -76,7 +76,7 @@ export const IdentifiedRisksTable: React.FC = ({ filteredAssessments.forEach((assessment) => { const combinedApplications = [ ...(assessment.application ? [assessment.application] : []), - ...(assessment.archetypeApplications || []), + ...(assessment.archetypeApplications ?? []), ]; const uniqueApplications = combinedApplications.reduce( @@ -94,7 +94,7 @@ export const IdentifiedRisksTable: React.FC = ({ question.answers.forEach((answer) => { if (answer.selected) { const itemId = [ - assessment.id, + assessment.questionnaire.id, section.order, question.order, answer.order, @@ -245,7 +245,9 @@ export const IdentifiedRisksTable: React.FC = ({ {item?.applications.length ? ( - {item.applications.length} Application(s) + {t("composed.totalApplications", { + count: item.applications.length, + })} ) : ( "N/A" From 1e95f916e1c0eeef4e450a57c5bcdd7dac1bdb24 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Wed, 29 Nov 2023 11:32:51 -0500 Subject: [PATCH 10/16] Refactor fetch assessments with archetype applications query Signed-off-by: ibolton336 --- client/src/app/queries/assessments.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/client/src/app/queries/assessments.ts b/client/src/app/queries/assessments.ts index d070f4ef2..f74c4c43e 100644 --- a/client/src/app/queries/assessments.ts +++ b/client/src/app/queries/assessments.ts @@ -223,13 +223,17 @@ export const useFetchAssessmentsWithArchetypeApplications = () => { const archetypeQueries = useQueries({ queries: - assessments?.map((assessment) => ({ - queryKey: ["archetype", assessment.archetype?.id], + [ + ...new Set( + assessments + .map((assessment) => assessment?.archetype?.id) + .filter(Boolean) + ), + ].map((archetypeId) => ({ + queryKey: ["archetype", archetypeId], queryFn: () => - assessment.archetype?.id - ? getArchetypeById(assessment.archetype.id) - : undefined, - enabled: !!assessment.archetype?.id, + archetypeId ? getArchetypeById(archetypeId) : undefined, + enabled: !!archetypeId, })) || [], }); From 09756be6794275354ffb0a7572b00c5d82975a29 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Wed, 29 Nov 2023 11:35:12 -0500 Subject: [PATCH 11/16] Naming fix and get() usage improvement Signed-off-by: ibolton336 --- client/src/app/queries/assessments.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/src/app/queries/assessments.ts b/client/src/app/queries/assessments.ts index f74c4c43e..caffb76c3 100644 --- a/client/src/app/queries/assessments.ts +++ b/client/src/app/queries/assessments.ts @@ -221,7 +221,7 @@ const removeSectionOrderFromQuestions = ( export const useFetchAssessmentsWithArchetypeApplications = () => { const { assessments, isFetching: assessmentsLoading } = useFetchAssessments(); - const archetypeQueries = useQueries({ + const archetypesUsedInAnAssessmentQueries = useQueries({ queries: [ ...new Set( @@ -237,10 +237,12 @@ export const useFetchAssessmentsWithArchetypeApplications = () => { })) || [], }); - const isArchetypesLoading = archetypeQueries.some((query) => query.isLoading); + const isArchetypesLoading = archetypesUsedInAnAssessmentQueries.some( + (query) => query.isLoading + ); const archetypeApplicationsMap = new Map(); - archetypeQueries.forEach((query, index) => { + archetypesUsedInAnAssessmentQueries.forEach((query, index) => { if (query.data && assessments[index].archetype?.id) { archetypeApplicationsMap.set( assessments[index]?.archetype?.id, @@ -252,9 +254,8 @@ export const useFetchAssessmentsWithArchetypeApplications = () => { const assessmentsWithArchetypeApplications: AssessmentWithArchetypeApplications[] = assessments.map((assessment) => ({ ...assessment, - archetypeApplications: assessment.archetype?.id - ? archetypeApplicationsMap.get(assessment.archetype.id) || [] - : [], + archetypeApplications: + archetypeApplicationsMap.get(assessment?.archetype?.id) ?? [], })); return { From 487d980a5ca2d3709ed5edd05556ba7b137fd459 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Wed, 29 Nov 2023 11:43:06 -0500 Subject: [PATCH 12/16] Consolidate donut usage Signed-off-by: ibolton336 --- .../applications-table/applications-table.tsx | 8 +-- .../application-landscape.tsx | 6 +- .../assessment-landscape.tsx | 6 +- .../components/assessment-landscape/donut.tsx | 60 ------------------- .../donut.tsx | 14 ++++- 5 files changed, 25 insertions(+), 69 deletions(-) delete mode 100644 client/src/app/pages/reports/components/assessment-landscape/donut.tsx rename client/src/app/pages/reports/components/{application-landscape => donut}/donut.tsx (79%) 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 91d86f667..7a443f418 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -67,7 +67,7 @@ import { checkAccess } from "@app/utils/rbac-utils"; import WarningTriangleIcon from "@patternfly/react-icons/dist/esm/icons/warning-triangle-icon"; // Hooks -import { useIsFetching, useQueryClient } from "@tanstack/react-query"; +import { useQueryClient } from "@tanstack/react-query"; import { deserializeFilterUrlParams, useLocalTableControls, @@ -114,8 +114,6 @@ export const ApplicationsTable: React.FC = () => { const history = useHistory(); const token = keycloak.tokenParsed; - const isFetching = useIsFetching(); - const { pushNotification } = React.useContext(NotificationsContext); const { identities } = useFetchIdentities(); @@ -706,7 +704,9 @@ export const ApplicationsTable: React.FC = () => { return ( } >
= ({ > = ({ = ({ = ({ = ({ > = ({ = ({ = ({ = ({ - id, - value, - total, - color, - riskLabel, -}) => { - const { t } = useTranslation(); - - return ( - - - - `${datum.x}: ${datum.y}`} - colorScale={[color, black.value]} - /> - - - - - {riskLabel} - - - - ); -}; diff --git a/client/src/app/pages/reports/components/application-landscape/donut.tsx b/client/src/app/pages/reports/components/donut/donut.tsx similarity index 79% rename from client/src/app/pages/reports/components/application-landscape/donut.tsx rename to client/src/app/pages/reports/components/donut/donut.tsx index f80c9235e..4c7912a13 100644 --- a/client/src/app/pages/reports/components/application-landscape/donut.tsx +++ b/client/src/app/pages/reports/components/donut/donut.tsx @@ -19,6 +19,7 @@ export interface IDonutProps { color: string; riskLabel: string; riskDescription?: string; + isAssessment: boolean; } export const Donut: React.FC = ({ @@ -27,6 +28,7 @@ export const Donut: React.FC = ({ total, color, riskLabel, + isAssessment, }) => { const { t } = useTranslation(); @@ -37,9 +39,15 @@ export const Donut: React.FC = ({ Date: Wed, 29 Nov 2023 11:47:34 -0500 Subject: [PATCH 13/16] Cleanup assessmentRefs filter logic Signed-off-by: ibolton336 --- client/src/app/pages/reports/reports.tsx | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/client/src/app/pages/reports/reports.tsx b/client/src/app/pages/reports/reports.tsx index db2c377c0..9ddc6ebe1 100644 --- a/client/src/app/pages/reports/reports.tsx +++ b/client/src/app/pages/reports/reports.tsx @@ -123,22 +123,14 @@ export const Reports: React.FC = () => { ? null : questionnairesById[selectedQuestionnaireId]; - const assessmentRefs = isAllQuestionnairesSelected - ? assessments - .map((assessment) => { - const assessmentRef = toIdRef(assessment); - return assessmentRef; - }) - .filter(Boolean) - : assessments - .filter( - ({ questionnaire }) => questionnaire.id === selectedQuestionnaireId - ) - .map((assessment) => { - const assessmentRef = toIdRef(assessment); - return assessmentRef; - }) - .filter(Boolean); + const assessmentRefs = assessments + .filter( + (assessment) => + isAllQuestionnairesSelected || + assessment.questionnaire.id === selectedQuestionnaireId + ) + .map((assessment) => toIdRef(assessment)) + .filter(Boolean); return ( <> From dd86b6453179ef0b4effbcf3ebc0b3dbc0eb38e9 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Wed, 29 Nov 2023 14:04:14 -0500 Subject: [PATCH 14/16] Add risks link from report Signed-off-by: ibolton336 --- .../applications-table/applications-table.tsx | 16 ++++++++ .../application-landscape.tsx | 37 ++++++++++++++++--- .../pages/reports/components/donut/donut.tsx | 4 +- client/src/app/queries/risks.ts | 26 ------------- 4 files changed, 50 insertions(+), 33 deletions(-) delete mode 100644 client/src/app/queries/risks.ts 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 7a443f418..e6c8ad878 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -482,6 +482,22 @@ export const ApplicationsTable: React.FC = () => { return matchString; }, }, + { + key: "risk", + title: t("terms.risk"), + type: FilterType.multiselect, + placeholderText: + t("actions.filterBy", { + what: t("terms.risk").toLowerCase(), + }) + "...", + selectOptions: [ + { key: "green", value: "Green" }, + { key: "yellow", value: "Yellow" }, + { key: "red", value: "Red" }, + { key: "unknown", value: "Unknown" }, + ], + getItemValue: (item) => item.risk || "", + }, ], initialItemsPerPage: 10, hasActionsColumn: true, diff --git a/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx b/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx index 1bd5a16ff..3c0107c83 100644 --- a/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx +++ b/client/src/app/pages/reports/components/application-landscape/application-landscape.tsx @@ -14,6 +14,9 @@ import { ConditionalRender } from "@app/components/ConditionalRender"; import { useFetchAssessmentsWithArchetypeApplications } from "@app/queries/assessments"; import { useFetchApplications } from "@app/queries/applications"; import { Donut } from "../donut/donut"; +import { serializeFilterUrlParams } from "@app/hooks/table-controls"; +import { Paths } from "@app/Paths"; +import { Link } from "react-router-dom"; interface IAggregateRiskData { green: number; @@ -142,7 +145,9 @@ export const ApplicationLandscape: React.FC = ({ value={landscapeData.red} total={landscapeData.applicationsCount} color={RISK_LIST.red.hexColor} - riskLabel={t("terms.highRisk")} + riskLabel={ + {t("terms.highRisk")} + } riskDescription={questionnaire?.riskMessages?.red ?? ""} /> @@ -153,7 +158,11 @@ export const ApplicationLandscape: React.FC = ({ value={landscapeData.yellow} total={landscapeData.applicationsCount} color={RISK_LIST.yellow.hexColor} - riskLabel={t("terms.mediumRisk")} + riskLabel={ + + {t("terms.mediumRisk")} + + } riskDescription={questionnaire?.riskMessages?.yellow ?? ""} /> @@ -164,7 +173,9 @@ export const ApplicationLandscape: React.FC = ({ value={landscapeData.green} total={landscapeData.applicationsCount} color={RISK_LIST.green.hexColor} - riskLabel={t("terms.lowRisk")} + riskLabel={ + {t("terms.lowRisk")} + } riskDescription={questionnaire?.riskMessages?.green ?? ""} /> @@ -175,8 +186,11 @@ export const ApplicationLandscape: React.FC = ({ value={landscapeData.unassessed} total={landscapeData.applicationsCount} color={RISK_LIST.unknown.hexColor} - riskLabel={`${t("terms.unassessed")}/${t("terms.unknown")}`} - riskDescription={questionnaire?.riskMessages?.unknown ?? ""} + riskLabel={ + + {`${t("terms.unassessed")}/${t("terms.unknown")}`} + + } /> @@ -184,3 +198,16 @@ export const ApplicationLandscape: React.FC = ({ ); }; + +const getRisksUrl = (risks: string[]) => { + const filterValues = { + risk: risks, + }; + + const serializedParams = serializeFilterUrlParams(filterValues); + + const queryString = serializedParams.filters + ? `filters=${serializedParams.filters}` + : ""; + return `${Paths.applications}?${queryString}`; +}; diff --git a/client/src/app/pages/reports/components/donut/donut.tsx b/client/src/app/pages/reports/components/donut/donut.tsx index 4c7912a13..8c1d41326 100644 --- a/client/src/app/pages/reports/components/donut/donut.tsx +++ b/client/src/app/pages/reports/components/donut/donut.tsx @@ -17,8 +17,8 @@ export interface IDonutProps { value: number; total: number; color: string; - riskLabel: string; - riskDescription?: string; + riskLabel: string | React.ReactElement; + riskDescription?: string | React.ReactElement; isAssessment: boolean; } diff --git a/client/src/app/queries/risks.ts b/client/src/app/queries/risks.ts deleted file mode 100644 index cc5b8bc9e..000000000 --- a/client/src/app/queries/risks.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { AssessmentRisk } from "@app/api/models"; - -export const RisksQueryKey = "risks"; - -/** @deprecated Risk is attached to assessments now. */ -export const useFetchRisks = (applicationIDs: number[]) => { - const { data, refetch, isFetching, error } = useQuery({ - queryKey: ["assessmentrisks", applicationIDs], - queryFn: async () => { - if (applicationIDs.length > 0) - // return (await getAssessmentLandscape(applicationIDs)).data; - //TODO see if we still need this - return []; - else return []; - }, - onError: (error) => console.log("error, ", error), - }); - - return { - risks: data || [], - isFetching, - error, - refetch, - }; -}; From 294aba778e3da09b7e8956cb2656c9e10782982c Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Wed, 29 Nov 2023 14:16:35 -0500 Subject: [PATCH 15/16] Move dropdown to right of title as requested by UXD Signed-off-by: ibolton336 --- client/src/app/pages/reports/reports.tsx | 108 ++++++++++++----------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/client/src/app/pages/reports/reports.tsx b/client/src/app/pages/reports/reports.tsx index 9ddc6ebe1..7ea098017 100644 --- a/client/src/app/pages/reports/reports.tsx +++ b/client/src/app/pages/reports/reports.tsx @@ -6,6 +6,8 @@ import { CardBody, CardHeader, CardTitle, + Flex, + FlexItem, MenuToggle, PageSection, PageSectionVariants, @@ -150,59 +152,61 @@ export const Reports: React.FC = () => { - - setIsQuestionnaireSelectOpen(false) - } - toggle={(toggleRef) => ( - { - setIsQuestionnaireSelectOpen( - !isQuestionnaireSelectOpen - ); - }} - isExpanded={isQuestionnaireSelectOpen} - > - {selectedQuestionnaireId === ALL_QUESTIONNAIRES - ? "All questionnaires" - : questionnairesById[selectedQuestionnaireId] - ?.name} - - )} - shouldFocusToggleOnSelect - > - - All questionnaires - - {...answeredQuestionnaires.map( - (answeredQuestionnaire) => ( - - {answeredQuestionnaire.name} - - ) - )} - - ), - }} - > + - {t("terms.currentLandscape")} + + + + {t("terms.currentLandscape")} + + + + + + From b083796a01db98f05cafa35044fe2c1d16bf7ec9 Mon Sep 17 00:00:00 2001 From: ibolton336 Date: Wed, 29 Nov 2023 14:21:12 -0500 Subject: [PATCH 16/16] Update filter values Signed-off-by: ibolton336 --- .../applications/applications-table/applications-table.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 e6c8ad878..55a787fa8 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -491,9 +491,9 @@ export const ApplicationsTable: React.FC = () => { what: t("terms.risk").toLowerCase(), }) + "...", selectOptions: [ - { key: "green", value: "Green" }, - { key: "yellow", value: "Yellow" }, - { key: "red", value: "Red" }, + { key: "green", value: "Low" }, + { key: "yellow", value: "Medium" }, + { key: "red", value: "High" }, { key: "unknown", value: "Unknown" }, ], getItemValue: (item) => item.risk || "",