diff --git a/client/src/app/Paths.ts b/client/src/app/Paths.ts index fc4d1a05ef..71335666e3 100644 --- a/client/src/app/Paths.ts +++ b/client/src/app/Paths.ts @@ -20,6 +20,7 @@ export enum Paths { applicationsImportsDetails = "/applications/application-imports/:importId", applicationsAssessment = "/applications/assessment/:assessmentId", assessmentActions = "/applications/assessment-actions/:applicationId", + assessmentSummary = "/applications/assessment-summary/:assessmentId", applicationsReview = "/applications/application/:applicationId/review", applicationsAnalysis = "/applications/analysis", archetypes = "/archetypes", @@ -50,7 +51,7 @@ export enum Paths { proxies = "/proxies", migrationTargets = "/migration-targets", assessment = "/assessment", - questionnaire = "/questionnaire", + questionnaire = "/questionnaire/:questionnaireId", jira = "/jira", } diff --git a/client/src/app/Routes.tsx b/client/src/app/Routes.tsx index df23bd9de9..8eae84dbea 100644 --- a/client/src/app/Routes.tsx +++ b/client/src/app/Routes.tsx @@ -40,15 +40,23 @@ const AssessmentSettings = lazy( "./pages/assessment-management/assessment-settings/assessment-settings-page" ) ); + const Questionnaire = lazy( () => import("./pages/assessment-management/questionnaire/questionnaire-page") ); + const AssessmentActions = lazy( () => import("./pages/applications/assessment-actions/assessment-actions-page") ); const Archetypes = lazy(() => import("./pages/archetypes/archetypes-page")); +const AssessmentSummary = lazy( + () => + import( + "./pages/applications/application-assessment/components/assessment-summary/assessment-summary-page" + ) +); export interface IRoute { path: string; comp: React.ComponentType; @@ -77,7 +85,11 @@ export const devRoutes: IRoute[] = [ comp: AssessmentActions, exact: false, }, - + { + path: Paths.assessmentSummary, + comp: AssessmentSummary, + exact: false, + }, { path: Paths.applicationsReview, comp: Reviews, diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 2d805a6236..d594e20572 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -122,6 +122,7 @@ export interface Application { binary?: string; migrationWave: Ref | null; assessments?: Ref[]; + assessed?: boolean; } export interface Review { diff --git a/client/src/app/api/rest.ts b/client/src/app/api/rest.ts index c4b2c805a8..99417d3d30 100644 --- a/client/src/app/api/rest.ts +++ b/client/src/app/api/rest.ts @@ -237,6 +237,14 @@ export const getAssessments = (filters: { .then((response) => response.data); }; +export const getAssessmentsByAppId = ( + applicationId?: number +): Promise => { + return axios + .get(`${APPLICATIONS}/${applicationId}/assessments`) + .then((response) => response.data); +}; + export const createAssessment = ( obj: InitialAssessment ): Promise => { diff --git a/client/src/app/pages/assessment-management/questionnaire/components/answer-table.tsx b/client/src/app/components/answer-table/answer-table.tsx similarity index 89% rename from client/src/app/pages/assessment-management/questionnaire/components/answer-table.tsx rename to client/src/app/components/answer-table/answer-table.tsx index 5898f44f3c..a2b3799ff3 100644 --- a/client/src/app/pages/assessment-management/questionnaire/components/answer-table.tsx +++ b/client/src/app/components/answer-table/answer-table.tsx @@ -15,16 +15,23 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { IconedStatus } from "@app/components/IconedStatus"; import { TimesCircleIcon } from "@patternfly/react-icons"; import { WarningTriangleIcon } from "@patternfly/react-icons"; + export interface IAnswerTableProps { answers: Answer[]; + hideAnswerKey?: boolean; } -const AnswerTable: React.FC = ({ answers }) => { +const AnswerTable: React.FC = ({ + answers, + hideAnswerKey, +}) => { const { t } = useTranslation(); const tableControls = useLocalTableControls({ idProperty: "text", - items: answers, + items: hideAnswerKey + ? answers.filter((answer) => answer.selected) + : answers, columnNames: { choice: "Answer choice", weight: "Weight", @@ -99,10 +106,10 @@ const AnswerTable: React.FC = ({ answers }) => { > Tags to be applied: - {answer?.autoAnswerFor?.map((tag: any) => { + {answer?.autoAnswerFor?.map((tag, index) => { return ( -
- +
+
); })} diff --git a/client/src/app/pages/assessment-management/questionnaire/components/questions-table.tsx b/client/src/app/components/questions-table/questions-table.tsx similarity index 90% rename from client/src/app/pages/assessment-management/questionnaire/components/questions-table.tsx rename to client/src/app/components/questions-table/questions-table.tsx index dea534ba75..bd4112a9e5 100644 --- a/client/src/app/pages/assessment-management/questionnaire/components/questions-table.tsx +++ b/client/src/app/components/questions-table/questions-table.tsx @@ -15,24 +15,26 @@ import { } from "@app/components/TableControls"; import { useTranslation } from "react-i18next"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { Assessment, Question } from "@app/api/models"; +import { Assessment, Question, Questionnaire } from "@app/api/models"; import { useLocalTableControls } from "@app/hooks/table-controls"; import { Label } from "@patternfly/react-core"; -import AnswerTable from "./answer-table"; import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; +import AnswerTable from "@app/components/answer-table/answer-table"; const QuestionsTable: React.FC<{ fetchError?: Error; questions?: Question[]; isSearching?: boolean; - assessmentData?: Assessment | null; + data?: Assessment | Questionnaire | null; isAllQuestionsTab?: boolean; + hideAnswerKey?: boolean; }> = ({ fetchError, questions, isSearching = false, - assessmentData, + data, isAllQuestionsTab = false, + hideAnswerKey, }) => { const tableControls = useLocalTableControls({ idProperty: "text", @@ -90,7 +92,7 @@ const QuestionsTable: React.FC<{ {currentPageItems?.map((question, rowIndex) => { const sectionName = - assessmentData?.sections.find((section) => + data?.sections.find((section) => section.questions.includes(question) )?.name || ""; return ( @@ -127,7 +129,10 @@ const QuestionsTable: React.FC<{ > {question.explanation} - + diff --git a/client/src/app/pages/applications/application-assessment/application-assessment.tsx b/client/src/app/pages/applications/application-assessment/application-assessment.tsx index ca1512db8a..1101dfe201 100644 --- a/client/src/app/pages/applications/application-assessment/application-assessment.tsx +++ b/client/src/app/pages/applications/application-assessment/application-assessment.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; @@ -8,25 +8,21 @@ import { Bullseye, } from "@patternfly/react-core"; import BanIcon from "@patternfly/react-icons/dist/esm/icons/ban-icon"; -import yaml from "js-yaml"; - import { AssessmentRoute } from "@app/Paths"; -import { Assessment } from "@app/api/models"; -import { getAssessmentById } from "@app/api/rest"; import { getAxiosErrorMessage } from "@app/utils/utils"; import { ApplicationAssessmentPage } from "./components/application-assessment-page"; import { ApplicationAssessmentWizard } from "./components/application-assessment-wizard"; import { SimpleEmptyState } from "@app/components/SimpleEmptyState"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; -import { useFetchAssessmentByID } from "@app/queries/assessments"; +import { useFetchAssessmentById } from "@app/queries/assessments"; export const ApplicationAssessment: React.FC = () => { const { t } = useTranslation(); const { assessmentId } = useParams(); const { assessment, isFetching, fetchError } = - useFetchAssessmentByID(assessmentId); + useFetchAssessmentById(assessmentId); const [saveError, setSaveError] = useState(); diff --git a/client/src/app/pages/applications/application-assessment/components/application-assessment-wizard/application-assessment-wizard.tsx b/client/src/app/pages/applications/application-assessment/components/application-assessment-wizard/application-assessment-wizard.tsx index 179ef900e1..215c1b5921 100644 --- a/client/src/app/pages/applications/application-assessment/components/application-assessment-wizard/application-assessment-wizard.tsx +++ b/client/src/app/pages/applications/application-assessment/components/application-assessment-wizard/application-assessment-wizard.tsx @@ -229,7 +229,12 @@ export const ApplicationAssessmentWizard: React.FC< .then(() => { switch (saveAction) { case SAVE_ACTION_VALUE.SAVE: - history.push(Paths.applications); + history.push( + formatPath(Paths.assessmentActions, { + applicationId: assessment?.application?.id, + }) + ); + break; case SAVE_ACTION_VALUE.SAVE_AND_REVIEW: assessment?.application?.id && diff --git a/client/src/app/pages/applications/application-assessment/components/assessment-summary/assessment-summary-page.css b/client/src/app/pages/applications/application-assessment/components/assessment-summary/assessment-summary-page.css new file mode 100644 index 0000000000..52420e3fe3 --- /dev/null +++ b/client/src/app/pages/applications/application-assessment/components/assessment-summary/assessment-summary-page.css @@ -0,0 +1,11 @@ +.tabs-vertical-container { + display: flex; +} + +.tabs-vertical-container .pf-v5-c-tabs { + width: 20%; +} + +.tabs-vertical-container .pf-v5-c-tab-content { + width: 80%; +} diff --git a/client/src/app/pages/applications/application-assessment/components/assessment-summary/assessment-summary-page.tsx b/client/src/app/pages/applications/application-assessment/components/assessment-summary/assessment-summary-page.tsx new file mode 100644 index 0000000000..ccab31b8ec --- /dev/null +++ b/client/src/app/pages/applications/application-assessment/components/assessment-summary/assessment-summary-page.tsx @@ -0,0 +1,201 @@ +import React, { useEffect, useState, useMemo } from "react"; +import yaml from "js-yaml"; +import { + Text, + TextContent, + PageSection, + PageSectionVariants, + Breadcrumb, + BreadcrumbItem, + Button, + Tabs, + Toolbar, + ToolbarItem, + SearchInput, + ToolbarContent, + Tab, +} from "@patternfly/react-core"; +import AngleLeftIcon from "@patternfly/react-icons/dist/esm/icons/angle-left-icon"; +import { Assessment } from "@app/api/models"; +import { Link, useParams } from "react-router-dom"; +import { Paths, formatPath } from "@app/Paths"; +import { ConditionalRender } from "@app/components/ConditionalRender"; +import { AppPlaceholder } from "@app/components/AppPlaceholder"; +import { useTranslation } from "react-i18next"; +import QuestionnaireSectionTabTitle from "./components/questionnaire-section-tab-title"; +import "./assessment-summary-page.css"; +import { useFetchQuestionnaires } from "@app/queries/questionnaires"; +import { useFetchAssessmentById } from "@app/queries/assessments"; +import QuestionsTable from "@app/components/questions-table/questions-table"; + +interface AssessmentSummaryRouteParams { + assessmentId: string; + applicationId: string; +} + +const AssessmentSummaryPage: React.FC = () => { + const { assessmentId } = useParams(); + + const { t } = useTranslation(); + + const [activeSectionIndex, setActiveSectionIndex] = React.useState< + "all" | number + >("all"); + + const handleTabClick = ( + _event: React.MouseEvent | React.KeyboardEvent | MouseEvent, + tabKey: string | number + ) => { + setActiveSectionIndex(tabKey as "all" | number); + }; + + const { + assessment, + isFetching: fetchingAssessment, + fetchError: fetchAssessmentError, + } = useFetchAssessmentById(assessmentId); + + const { questionnaires, isFetching, fetchError } = useFetchQuestionnaires(); + + const [searchValue, setSearchValue] = React.useState(""); + const filteredAssessmentData = useMemo(() => { + if (!assessment) return null; + return { + ...assessment, + sections: assessment?.sections.map((section) => ({ + ...section, + questions: section.questions.filter(({ text, explanation }) => + [text, explanation].some( + (text) => text?.toLowerCase().includes(searchValue.toLowerCase()) + ) + ), + })), + }; + }, [assessment, searchValue]); + const allQuestions = + assessment?.sections.flatMap((section) => section.questions) || []; + const allMatchingQuestions = + filteredAssessmentData?.sections.flatMap((section) => section.questions) || + []; + if (!assessment?.application?.id) { + return
No assessment data available.
; // You can render a message or redirect here + } + return ( + <> + + + Assessment + + + + + Assessment + + + + {assessment?.name} + + + + + }> +
+ + + + setSearchValue(value)} + onClear={() => setSearchValue("")} + resultsCount={ + (searchValue && allMatchingQuestions.length) || undefined + } + /> + + + + + + + +
+ + {[ + + } + > + + , + ...(assessment?.sections.map((section, index) => { + const filteredQuestions = + filteredAssessmentData?.sections[index]?.questions || []; + return ( + + } + > + + + ); + }) || []), + ]} + +
+
+
+
+ + ); +}; + +export default AssessmentSummaryPage; diff --git a/client/src/app/pages/applications/application-assessment/components/assessment-summary/components/questionnaire-section-tab-title.tsx b/client/src/app/pages/applications/application-assessment/components/assessment-summary/components/questionnaire-section-tab-title.tsx new file mode 100644 index 0000000000..20e5e600dc --- /dev/null +++ b/client/src/app/pages/applications/application-assessment/components/assessment-summary/components/questionnaire-section-tab-title.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { TabTitleText, Badge } from "@patternfly/react-core"; +import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import { Question } from "@app/api/models"; + +const QuestionnaireSectionTabTitle: React.FC<{ + isSearching: boolean; + sectionName: string; + unfilteredQuestions: Question[]; + filteredQuestions: Question[]; +}> = ({ isSearching, sectionName, unfilteredQuestions, filteredQuestions }) => ( + + {sectionName} +
+ + {unfilteredQuestions.length} questions + {isSearching ? ( + + {`${filteredQuestions.length} match${ + filteredQuestions.length === 1 ? "" : "es" + }`} + + ) : null} + +
+); + +export default QuestionnaireSectionTabTitle; diff --git a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx b/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx index 65d420158f..521d93ed5c 100644 --- a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx +++ b/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx @@ -174,6 +174,9 @@ export const ApplicationsTable: React.FC = () => { refetch: fetchApplications, } = useFetchApplications(); + //TODO: check if any archetypes match this application here + const matchingArchetypes = []; + const onDeleteApplicationSuccess = (appIDCount: number) => { pushNotification({ title: t("toastr.success.applicationDeleted", { @@ -499,8 +502,18 @@ export const ApplicationsTable: React.FC = () => { const assessSelectedApp = (application: Application) => { // if application/archetype has an assessment, ask if user wants to override it - setAssessModalOpen(true); - setApplicationToAssess(application); + if (matchingArchetypes.length) { + setAssessModalOpen(true); + setApplicationToAssess(application); + } else { + application?.id && + history.push( + formatPath(Paths.assessmentActions, { + applicationId: application?.id, + }) + ); + setApplicationToAssess(null); + } }; const reviewSelectedApp = (application: Application) => { if (application.review) { diff --git a/client/src/app/pages/applications/assessment-actions/components/assessment-actions-table.tsx b/client/src/app/pages/applications/assessment-actions/components/assessment-actions-table.tsx index f4173ea2d6..a4a319657a 100644 --- a/client/src/app/pages/applications/assessment-actions/components/assessment-actions-table.tsx +++ b/client/src/app/pages/applications/assessment-actions/components/assessment-actions-table.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { useTranslation } from "react-i18next"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import { useLocalTableControls } from "@app/hooks/table-controls"; @@ -9,17 +8,9 @@ import { TableRowContentWithControls, } from "@app/components/TableControls"; import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; -import { - Application, - Assessment, - InitialAssessment, - Questionnaire, -} from "@app/api/models"; -import { Button } from "@patternfly/react-core"; -import { Paths, formatPath } from "@app/Paths"; -import { useHistory } from "react-router-dom"; +import { Application } from "@app/api/models"; import { useFetchQuestionnaires } from "@app/queries/questionnaires"; -import { useCreateAssessmentMutation } from "@app/queries/assessments"; +import DynamicAssessmentButton from "./dynamic-assessment-button"; export interface AssessmentActionsTableProps { application?: Application; } @@ -45,42 +36,9 @@ const AssessmentActionsTable: React.FC = ({ propHelpers: { tableProps, getThProps, getTdProps }, } = tableControls; - const onSuccessHandler = () => {}; - const onErrorHandler = () => {}; - - const history = useHistory(); - const { mutateAsync: createAssessmentAsync } = useCreateAssessmentMutation( - onSuccessHandler, - onErrorHandler - ); - - const handleAssessmentCreationAndNav = async ( - questionnaire: Questionnaire - ) => { - //TODO handle archetypes here too - if (!application) { - console.error("Application is undefined. Cannot proceed."); - return; - } - - // Replace with your actual assessment data - const newAssessment: InitialAssessment = { - questionnaire: { name: questionnaire.name, id: questionnaire.id }, - application: { name: application.name, id: application?.id }, - //TODO handle archetypes here too - }; - - try { - const result = await createAssessmentAsync(newAssessment); - history.push( - formatPath(Paths.applicationsAssessment, { - assessmentId: result.id, - }) - ); - } catch (error) { - console.error("Error while creating assessment:", error); - } - }; + if (!application) { + return
Application is undefined
; + } return ( <> @@ -121,16 +79,10 @@ const AssessmentActionsTable: React.FC = ({ {questionnaire.name} - + diff --git a/client/src/app/pages/applications/assessment-actions/components/dynamic-assessment-button.tsx b/client/src/app/pages/applications/assessment-actions/components/dynamic-assessment-button.tsx new file mode 100644 index 0000000000..be5efe081e --- /dev/null +++ b/client/src/app/pages/applications/assessment-actions/components/dynamic-assessment-button.tsx @@ -0,0 +1,103 @@ +import { Paths, formatPath } from "@app/Paths"; +import { Application, InitialAssessment, Questionnaire } from "@app/api/models"; +import { + useCreateAssessmentMutation, + useFetchAssessmentsByAppId, +} from "@app/queries/assessments"; +import { Button } from "@patternfly/react-core"; +import React, { FunctionComponent } from "react"; +import { useHistory } from "react-router-dom"; +import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; + +interface DynamicAssessmentButtonProps { + questionnaire: Questionnaire; + application: Application; +} + +const DynamicAssessmentButton: FunctionComponent< + DynamicAssessmentButtonProps +> = ({ questionnaire, application }) => { + const history = useHistory(); + const { assessments } = useFetchAssessmentsByAppId(application?.id); + const matchingAssessment = assessments?.find( + (assessment) => assessment.questionnaire.id === questionnaire.id + ); + + const onSuccessHandler = () => {}; + const onErrorHandler = () => {}; + + const { mutateAsync: createAssessmentAsync } = useCreateAssessmentMutation( + onSuccessHandler, + onErrorHandler + ); + + const onHandleAssessmentAction = async () => { + // TODO handle archetypes here too + if (!application) { + console.error("Application is undefined. Cannot proceed."); + return; + } + + if (!matchingAssessment) { + const newAssessment: InitialAssessment = { + questionnaire: { name: questionnaire.name, id: questionnaire.id }, + application: { name: application.name, id: application?.id }, + // TODO handle archetypes here too + }; + + try { + const result = await createAssessmentAsync(newAssessment); + history.push( + formatPath(Paths.applicationsAssessment, { + assessmentId: result.id, + }) + ); + } catch (error) { + console.error("Error while creating assessment:", error); + } + } else { + history.push( + formatPath(Paths.applicationsAssessment, { + assessmentId: matchingAssessment.id, + }) + ); + } + }; + + const viewButtonLabel = "View"; + + return ( +
+ + {matchingAssessment && ( + + )} +
+ ); +}; + +export default DynamicAssessmentButton; diff --git a/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx b/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx index 7ff02aa9ce..3ed325c26b 100644 --- a/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx +++ b/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx @@ -6,10 +6,7 @@ import { Spinner } from "@patternfly/react-core"; import { EmptyTextMessage } from "@app/components/EmptyTextMessage"; import { Assessment, Ref } from "@app/api/models"; import { IconedStatus, IconedStatusPreset } from "@app/components/IconedStatus"; -import { - useFetchApplicationAssessments, - useFetchAssessmentByID, -} from "@app/queries/assessments"; +import { useFetchAssessmentById } from "@app/queries/assessments"; export interface ApplicationAssessmentStatusProps { assessments?: Ref[]; @@ -35,7 +32,7 @@ export const ApplicationAssessmentStatus: React.FC< > = ({ assessments, isLoading, fetchError }) => { const { t } = useTranslation(); //TODO: remove this once we have a proper assessment status - const { assessment } = useFetchAssessmentByID(assessments?.[0]?.id || 0); + const { assessment } = useFetchAssessmentById(assessments?.[0]?.id || 0); if (fetchError) { return ; diff --git a/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx b/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx index 5a3966ef52..9f062c7eb3 100644 --- a/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx +++ b/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx @@ -49,7 +49,7 @@ import { NotificationsContext } from "@app/components/NotificationsContext"; import { getAxiosErrorMessage } from "@app/utils/utils"; import { Questionnaire } from "@app/api/models"; import { useHistory } from "react-router-dom"; -import { Paths } from "@app/Paths"; +import { Paths, formatPath } from "@app/Paths"; import { ImportQuestionnaireForm } from "@app/pages/assessment/import-questionnaire-form/import-questionnaire-form"; const AssessmentSettings: React.FC = () => { @@ -334,7 +334,11 @@ const AssessmentSettings: React.FC = () => { key="view" component="button" onClick={() => { - history.push(Paths.questionnaire); + history.push( + formatPath(Paths.questionnaire, { + questionnaireId: questionnaire.id, + }) + ); }} > {t("actions.view")} diff --git a/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.tsx b/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.tsx index 17effc26c3..78ca416f06 100644 --- a/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.tsx +++ b/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.tsx @@ -16,18 +16,23 @@ import { Tab, } from "@patternfly/react-core"; import AngleLeftIcon from "@patternfly/react-icons/dist/esm/icons/angle-left-icon"; -import { Assessment } from "@app/api/models"; -import { Link } from "react-router-dom"; +import { Questionnaire } from "@app/api/models"; +import { Link, useParams } from "react-router-dom"; import { Paths } from "@app/Paths"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; import { useTranslation } from "react-i18next"; import QuestionnaireSectionTabTitle from "./components/questionnaire-section-tab-title"; -import QuestionsTable from "./components/questions-table"; import "./questionnaire-page.css"; -import { useFetchQuestionnaires } from "@app/queries/questionnaires"; +import { useFetchQuestionnaireById } from "@app/queries/questionnaires"; +import QuestionsTable from "@app/components/questions-table/questions-table"; + +interface QuestionnairePageParams { + questionnaireId: string; +} const Questionnaire: React.FC = () => { + const { questionnaireId } = useParams(); const { t } = useTranslation(); const [activeSectionIndex, setActiveSectionIndex] = React.useState< @@ -41,16 +46,15 @@ const Questionnaire: React.FC = () => { setActiveSectionIndex(tabKey as "all" | number); }; - const [assessmentData, setAssessmentData] = useState(null); - - const { questionnaires, isFetching, fetchError } = useFetchQuestionnaires(); + const { questionnaire, isFetching, fetchError } = + useFetchQuestionnaireById(questionnaireId); const [searchValue, setSearchValue] = React.useState(""); - const filteredAssessmentData = useMemo(() => { - if (!assessmentData) return null; + const filteredQuestionnaireData = useMemo(() => { + if (!questionnaire) return null; return { - ...assessmentData, - sections: assessmentData?.sections.map((section) => ({ + ...questionnaire, + sections: questionnaire?.sections.map((section) => ({ ...section, questions: section.questions.filter(({ text, explanation }) => [text, explanation].some( @@ -59,12 +63,13 @@ const Questionnaire: React.FC = () => { ), })), }; - }, [assessmentData, searchValue]); + }, [questionnaire, searchValue]); const allQuestions = - assessmentData?.sections.flatMap((section) => section.questions) || []; + questionnaire?.sections.flatMap((section) => section.questions) || []; const allMatchingQuestions = - filteredAssessmentData?.sections.flatMap((section) => section.questions) || - []; + filteredQuestionnaireData?.sections.flatMap( + (section) => section.questions + ) || []; return ( <> @@ -77,7 +82,7 @@ const Questionnaire: React.FC = () => { Assessment - {assessmentData?.name} + {questionnaire?.name} @@ -118,6 +123,7 @@ const Questionnaire: React.FC = () => { > {[ { fetchError={fetchError as Error} questions={allMatchingQuestions} isSearching={!!searchValue} - assessmentData={assessmentData} + data={questionnaire} isAllQuestionsTab /> , - ...(assessmentData?.sections.map((section, index) => { + ...(questionnaire?.sections.map((section, index) => { const filteredQuestions = - filteredAssessmentData?.sections[index]?.questions || []; + filteredQuestionnaireData?.sections[index]?.questions || + []; return ( { fetchError={fetchError as Error} questions={filteredQuestions} isSearching={!!searchValue} - assessmentData={assessmentData} + data={questionnaire} /> ); diff --git a/client/src/app/queries/assessments.ts b/client/src/app/queries/assessments.ts index 183db6a24b..c84e082a76 100644 --- a/client/src/app/queries/assessments.ts +++ b/client/src/app/queries/assessments.ts @@ -11,12 +11,14 @@ import { deleteAssessment, getAssessmentById, getAssessments, + getAssessmentsByAppId, } from "@app/api/rest"; import { AxiosError, AxiosResponse } from "axios"; import { Application, Assessment, InitialAssessment } from "@app/api/models"; export const assessmentsQueryKey = "assessments"; export const assessmentQueryKey = "assessment"; +export const assessmentsByAppIdQueryKey = "assessmentsByAppId"; export const useFetchApplicationAssessments = ( applications: Application[] = [] @@ -81,7 +83,7 @@ export const useDeleteAssessmentMutation = ( }); }; -export const useFetchAssessmentByID = (id: number | string) => { +export const useFetchAssessmentById = (id: number | string) => { const { data, isLoading, error } = useQuery({ queryKey: [assessmentQueryKey, id], queryFn: () => getAssessmentById(id), @@ -93,3 +95,16 @@ export const useFetchAssessmentByID = (id: number | string) => { fetchError: error, }; }; + +export const useFetchAssessmentsByAppId = (applicationId: number) => { + const { data, isLoading, error } = useQuery({ + queryKey: [assessmentsByAppIdQueryKey, applicationId], + queryFn: () => getAssessmentsByAppId(applicationId), + onError: (error: AxiosError) => console.log("error, ", error), + }); + return { + assessments: data, + isFetching: isLoading, + fetchError: error, + }; +}; diff --git a/client/src/mocks/stub-new-work/applications.ts b/client/src/mocks/stub-new-work/applications.ts index e05f65a9a0..669c472791 100644 --- a/client/src/mocks/stub-new-work/applications.ts +++ b/client/src/mocks/stub-new-work/applications.ts @@ -26,14 +26,29 @@ export const mockApplicationArray: Application[] = [ ], binary: "app1-bin.zip", migrationWave: { id: 501, name: "Wave 1" }, - assessments: [], + assessments: [{ id: 43, name: "test" }], }, ]; export const handlers = [ - // rest.get(AppRest.APPLICATIONS, (req, res, ctx) => { - // return res(ctx.json(mockApplicationArray)); - // }), + rest.get(AppRest.APPLICATIONS, (req, res, ctx) => { + return res(ctx.json(mockApplicationArray)); + }), + rest.get(`${AppRest.APPLICATIONS}/:id`, (req, res, ctx) => { + const { id } = req.params; + const mockApplication = mockApplicationArray.find( + (app) => app.id === parseInt(id as string, 10) + ); + + if (mockApplication) { + return res(ctx.json(mockApplication)); + } else { + return res( + ctx.status(404), + ctx.json({ message: "Application not found" }) + ); + } + }), ]; export default handlers; diff --git a/client/src/mocks/stub-new-work/assessments.ts b/client/src/mocks/stub-new-work/assessments.ts index 5f32264c25..d11e189c5b 100644 --- a/client/src/mocks/stub-new-work/assessments.ts +++ b/client/src/mocks/stub-new-work/assessments.ts @@ -1,4 +1,4 @@ -import { Questionnaire, Assessment } from "@app/api/models"; +import { Questionnaire, Assessment, InitialAssessment } from "@app/api/models"; import { rest } from "msw"; import * as AppRest from "@app/api/rest"; @@ -189,7 +189,198 @@ function generateNewAssessmentId() { return newAssessmentId; } -const mockAssessmentArray: Assessment[] = []; +const mockAssessmentArray: Assessment[] = [ + { + id: 43, + status: "STARTED", + name: "test", + questionnaire: { id: 1, name: "Sample Questionnaire" }, + description: "Sample assessment description", + risk: "AMBER", + sections: [ + { + name: "Application technologies 1", + order: 1, + questions: [ + { + order: 1, + text: "What is the main technology in your application?", + explanation: + "What would you describe as the main framework used to build your application.", + answers: [ + { + order: 1, + text: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: + "Gathering more information about this is required.", + risk: "unknown", + selected: false, + }, + { + order: 2, + text: "Quarkus", + risk: "green", + autoAnswerFor: [ + { + category: { + name: "Cat 1", + id: 23, + }, + tag: { + id: 34, + name: "Tag 1", + }, + }, + ], + applyTags: [ + { + category: { + name: "Cat 1", + id: 23, + }, + tag: { + id: 34, + name: "Tag 1", + }, + }, + ], + selected: true, + }, + { + order: 3, + text: "Spring Boot", + risk: "green", + selected: false, + }, + { + order: 4, + text: "Java EE", + rationale: + "This might not be the most cloud friendly technology.", + mitigation: + "Maybe start thinking about migrating to Quarkus or Jakarta EE.", + risk: "yellow", + selected: false, + }, + { + order: 5, + text: "J2EE", + rationale: "This is obsolete.", + mitigation: + "Maybe start thinking about migrating to Quarkus or Jakarta EE.", + risk: "red", + selected: false, + }, + ], + }, + { + order: 2, + text: "What version of Java EE does the application use?", + explanation: + "What version of the Java EE specification is your application using?", + answers: [ + { + order: 1, + text: "Below 5.", + rationale: "This technology stack is obsolete.", + mitigation: "Consider migrating to at least Java EE 7.", + risk: "red", + selected: true, + }, + { + order: 2, + text: "5 or 6", + rationale: "This is a mostly outdated stack.", + mitigation: "Consider migrating to at least Java EE 7.", + risk: "yellow", + selected: false, + }, + { + order: 3, + text: "7", + risk: "green", + selected: false, + }, + ], + }, + { + order: 3, + text: "Does your application use any caching mechanism?", + answers: [ + { + order: 1, + text: "Yes", + rationale: + "This could be problematic in containers and Kubernetes.", + mitigation: + "Review the clustering mechanism to check compatibility and support for container environments.", + risk: "yellow", + selected: true, + }, + { + order: 2, + text: "No", + risk: "green", + selected: false, + }, + { + order: 3, + text: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: + "Gathering more information about this is required.", + risk: "unknown", + selected: false, + }, + ], + }, + { + order: 4, + text: "What implementation of JAX-WS does your application use?", + answers: [ + { + order: 1, + text: "Apache Axis", + rationale: "This version is obsolete", + mitigation: "Consider migrating to Apache CXF", + risk: "red", + selected: false, + }, + { + order: 2, + text: "Apache CXF", + risk: "green", + selected: true, + }, + { + order: 3, + text: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: + "Gathering more information about this is required.", + risk: "unknown", + selected: false, + }, + ], + }, + ], + }, + ], + riskMessages: { + green: "Low risk", + red: "High risk", + unknown: "Unknown risk", + yellow: "Moderate risk", + }, + thresholds: { + red: 3, + unknown: 2, + yellow: 4, + }, + application: { id: 1, name: "App 1" }, + }, +]; export const handlers = [ rest.get(AppRest.QUESTIONNAIRES, (req, res, ctx) => { @@ -200,6 +391,21 @@ export const handlers = [ return res(ctx.json(mockAssessmentArray)); }), + rest.get( + `${AppRest.APPLICATIONS}/:applicationId/assessments`, + (req, res, ctx) => { + // Extract the applicationId from the route parameters + const applicationId = parseInt(req?.params?.applicationId as string, 10); + + // Filter the mock assessments based on the applicationId + const filteredAssessments = mockAssessmentArray.filter( + (assessment) => assessment?.application?.id === applicationId + ); + + return res(ctx.json(filteredAssessments)); + } + ), + rest.get(`${AppRest.ASSESSMENTS}/:assessmentId`, (req, res, ctx) => { const { assessmentId } = req.params; @@ -213,14 +419,17 @@ export const handlers = [ return res(ctx.status(404), ctx.json({ error: "Assessment not found" })); } }), - rest.post(AppRest.ASSESSMENTS, (req, res, ctx) => { + rest.post(AppRest.ASSESSMENTS, async (req, res, ctx) => { + console.log("req need to find questionnaire id", req); + + const initialAssessment: InitialAssessment = await req.json(); + const newAssessmentId = generateNewAssessmentId(); const newAssessment: Assessment = { id: newAssessmentId, status: "STARTED", name: "test", - questionnaire: { id: 1, name: "Sample Questionnaire" }, description: "Sample assessment description", risk: "AMBER", sections: [], @@ -235,7 +444,8 @@ export const handlers = [ unknown: 2, yellow: 4, }, - application: { id: 1, name: "App 1" }, + application: initialAssessment.application, + questionnaire: initialAssessment.questionnaire, }; mockAssessmentArray.push(newAssessment); diff --git a/client/src/mocks/stub-new-work/questionnaires.ts b/client/src/mocks/stub-new-work/questionnaires.ts index 9b0ebc2535..015d9f4c57 100644 --- a/client/src/mocks/stub-new-work/questionnaires.ts +++ b/client/src/mocks/stub-new-work/questionnaires.ts @@ -267,7 +267,163 @@ const data: Record = { dateImported: "9 Aug. 2023, 03:32 PM EST", required: true, system: false, - sections: [], + sections: [ + { + name: "Application technologies 1", + order: 1, + questions: [ + { + order: 1, + text: "What is the main technology in your application?", + explanation: + "What would you describe as the main framework used to build your application.", + answers: [ + { + order: 1, + text: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: + "Gathering more information about this is required.", + risk: "unknown", + }, + { + order: 2, + text: "Quarkus", + risk: "green", + autoAnswerFor: [ + { + category: { + name: "Cat 1", + id: 23, + }, + tag: { + id: 34, + name: "Tag 1", + }, + }, + ], + applyTags: [ + { + category: { + name: "Cat 1", + id: 23, + }, + tag: { + id: 34, + name: "Tag 1", + }, + }, + ], + }, + { + order: 3, + text: "Spring Boot", + risk: "green", + }, + { + order: 4, + text: "Java EE", + rationale: + "This might not be the most cloud friendly technology.", + mitigation: + "Maybe start thinking about migrating to Quarkus or Jakarta EE.", + risk: "yellow", + }, + { + order: 5, + text: "J2EE", + rationale: "This is obsolete.", + mitigation: + "Maybe start thinking about migrating to Quarkus or Jakarta EE.", + risk: "red", + }, + ], + }, + { + order: 2, + text: "What version of Java EE does the application use?", + explanation: + "What version of the Java EE specification is your application using?", + answers: [ + { + order: 1, + text: "Below 5.", + rationale: "This technology stack is obsolete.", + mitigation: "Consider migrating to at least Java EE 7.", + risk: "red", + }, + { + order: 2, + text: "5 or 6", + rationale: "This is a mostly outdated stack.", + mitigation: "Consider migrating to at least Java EE 7.", + risk: "yellow", + }, + { + order: 3, + text: "7", + risk: "green", + }, + ], + }, + { + order: 3, + text: "Does your application use any caching mechanism?", + answers: [ + { + order: 1, + text: "Yes", + rationale: + "This could be problematic in containers and Kubernetes.", + mitigation: + "Review the clustering mechanism to check compatibility and support for container environments.", + risk: "yellow", + }, + { + order: 2, + + text: "No", + risk: "green", + }, + { + order: 3, + text: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: + "Gathering more information about this is required.", + risk: "unknown", + }, + ], + }, + { + order: 4, + text: "What implementation of JAX-WS does your application use?", + answers: [ + { + order: 1, + text: "Apache Axis", + rationale: "This version is obsolete", + mitigation: "Consider migrating to Apache CXF", + risk: "red", + }, + { + order: 2, + text: "Apache CXF", + risk: "green", + }, + { + order: 3, + text: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: + "Gathering more information about this is required.", + risk: "unknown", + }, + ], + }, + ], + }, + ], thresholds: { red: 3, unknown: 2, @@ -291,7 +447,81 @@ const data: Record = { dateImported: "10 Aug. 2023, 11:23 PM EST", required: true, system: false, - sections: [], + sections: [ + { + name: "Application technologies 1", + order: 1, + questions: [ + { + order: 1, + text: "What is the main technology in your application?", + explanation: + "What would you describe as the main framework used to build your application.", + answers: [ + { + order: 1, + text: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: + "Gathering more information about this is required.", + risk: "unknown", + }, + { + order: 2, + text: "Quarkus", + risk: "green", + autoAnswerFor: [ + { + category: { + name: "Cat 1", + id: 23, + }, + tag: { + id: 34, + name: "Tag 1", + }, + }, + ], + applyTags: [ + { + category: { + name: "Cat 1", + id: 23, + }, + tag: { + id: 34, + name: "Tag 1", + }, + }, + ], + }, + { + order: 3, + text: "Spring Boot", + risk: "green", + }, + { + order: 4, + text: "Java EE", + rationale: + "This might not be the most cloud friendly technology.", + mitigation: + "Maybe start thinking about migrating to Quarkus or Jakarta EE.", + risk: "yellow", + }, + { + order: 5, + text: "J2EE", + rationale: "This is obsolete.", + mitigation: + "Maybe start thinking about migrating to Quarkus or Jakarta EE.", + risk: "red", + }, + ], + }, + ], + }, + ], thresholds: { red: 3, unknown: 2,