diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index e917fbdf85..ee02060da8 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -11,6 +11,7 @@ "analysisDetails": "Analysis details", "analyze": "Analyze", "assess": "Assess", + "accept": "Accept", "back": "Back", "cancel": "Cancel", "checkDocumentation": "Check documentation", @@ -110,6 +111,7 @@ "import": "Import {{what}}", "leavePage": "Leave page", "new": "New {{what}}", + "newAssessment": "New assessment", "newApplication": "New application", "newBusinessService": "New business service", "newJobFunction": "New job function", @@ -156,6 +158,7 @@ "noResultsFoundBody": "No results match the filter criteria. Remove all filters or clear all filters to show results.", "noResultsFoundTitle": "No results found", "overrideAssessmentConfirmation": "This application has already been assessed. Do you want to continue?", + "overrideArchetypeConfirmation": "The archetype for this application already has an assessment. Do you want to create a dedicated assessment for this application?", "overrideReviewConfirmation": "This application has already been reviewed. Do you want to continue?", "reasonForError": "The reported reason for the error:", "reviewInstructions": "Use this section to provide your assessment of the possible migration/modernization plan and effort estimation.", diff --git a/client/src/app/Paths.ts b/client/src/app/Paths.ts index 6a004a8532..e3018ac2b6 100644 --- a/client/src/app/Paths.ts +++ b/client/src/app/Paths.ts @@ -19,6 +19,7 @@ export enum Paths { applicationsImports = "/applications/application-imports", applicationsImportsDetails = "/applications/application-imports/:importId", applicationsAssessment = "/applications/assessment/:assessmentId", + assessmentActions = "/applications/assessment-actions/:applicationId", applicationsReview = "/applications/application/:applicationId/review", applicationsAnalysis = "/applications/analysis", controls = "/controls", @@ -56,6 +57,10 @@ export interface AssessmentRoute { assessmentId: string; } +export interface AssessmentActionsRoute { + applicationId: string; +} + export interface ReviewRoute { applicationId: string; } diff --git a/client/src/app/Routes.tsx b/client/src/app/Routes.tsx index 7851a2f1bb..09b7743be6 100644 --- a/client/src/app/Routes.tsx +++ b/client/src/app/Routes.tsx @@ -43,6 +43,10 @@ const AssessmentSettings = lazy( const Questionnaire = lazy( () => import("./pages/assessment-management/questionnaire/questionnaire-page") ); +const AssessmentActions = lazy( + () => + import("./pages/applications/assessment-actions/assessment-actions-page") +); export interface IRoute { path: string; comp: React.ComponentType; @@ -66,6 +70,12 @@ export const devRoutes: IRoute[] = [ comp: ApplicationAssessment, exact: false, }, + { + path: Paths.assessmentActions, + comp: AssessmentActions, + exact: false, + }, + { path: Paths.applicationsReview, comp: Reviews, diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 12ebaf7dc6..a9fd6da255 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -121,6 +121,7 @@ export interface Application { repository?: Repository; binary?: string; migrationWave: Ref | null; + assessments?: Questionnaire[]; } export interface Review { @@ -707,13 +708,19 @@ export type HubFile = { export interface Questionnaire { id: number; - required: boolean; name: string; + description: string; + revision: number; questions: number; rating: string; dateImported: string; + required: boolean; system: boolean; + sections: Section[]; + thresholds: Thresholds; + riskMessages: RiskMessages; } + export interface RiskMessages { green: string; red: string; @@ -728,7 +735,7 @@ export interface Section { // TODO: Rename after removing pathfinder export interface CustomYamlAssessmentQuestion { answers: Answer[]; - explanation: string; + explanation?: string; formulation: string; include_if_tags_present?: Tag[]; skip_if_tags_present?: Tag[]; @@ -736,10 +743,11 @@ export interface CustomYamlAssessmentQuestion { export interface Answer { choice: string; - mitigation: string; - rationale: string; + mitigation?: string; + rationale?: string; risk: string; autoanswer_if_tags_present?: Tag[]; + autotag?: Tag[]; } export interface Thresholds { red: string; diff --git a/client/src/app/data/mock-questionnaire.ts b/client/src/app/data/mock-questionnaire.ts new file mode 100644 index 0000000000..97d0f607d9 --- /dev/null +++ b/client/src/app/data/mock-questionnaire.ts @@ -0,0 +1,466 @@ +export const mockQuestionnaire = { + id: 1, + name: "Q1", + description: "Questionnaire 1 ", + revision: 1, + questions: 42, + rating: "5% Red, 25% Yellow", + dateImported: "8 Aug. 2023, 10:20 AM EST", + required: false, + system: true, + sections: [ + { + name: "Application technologies 1", + questions: [ + { + formulation: "What is the main technology in your application?", + explanation: + "What would you describe as the main framework used to build your application.", + answers: [ + { + choice: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: "Gathering more information about this is required.", + risk: "unknown", + }, + { + choice: "Quarkus", + risk: "green", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + autotag: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + choice: "Spring Boot", + risk: "green", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + autotag: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + choice: "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", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + autotag: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + choice: "J2EE", + rationale: "This is obsolete.", + mitigation: + "Maybe start thinking about migrating to Quarkus or Jakarta EE.", + risk: "red", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + autotag: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + ], + }, + { + formulation: "What version of Java EE does the application use?", + explanation: + "What version of the Java EE specification is your application using?", + answers: [ + { + choice: "Below 5.", + rationale: "This technology stack is obsolete.", + mitigation: "Consider migrating to at least Java EE 7.", + risk: "red", + }, + { + choice: "5 or 6", + rationale: "This is a mostly outdated stack.", + mitigation: "Consider migrating to at least Java EE 7.", + risk: "yellow", + }, + { + choice: "7", + risk: "green", + }, + ], + include_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + formulation: "Does your application use any caching mechanism?", + answers: [ + { + choice: "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", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + choice: "No", + risk: "green", + }, + { + choice: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: "Gathering more information about this is required.", + risk: "unknown", + }, + ], + }, + { + formulation: + "What implementation of JAX-WS does your application use?", + answers: [ + { + choice: "Apache Axis", + rationale: "This version is obsolete", + mitigation: "Consider migrating to Apache CXF", + risk: "red", + }, + { + choice: "Apache CXF", + risk: "green", + }, + { + choice: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: "Gathering more information about this is required.", + risk: "unknown", + }, + ], + skip_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + ], + }, + { + name: "Application technologies", + questions: [ + { + formulation: "What is the main technology in your application?", + explanation: + "What would you describe as the main framework used to build your application.", + answers: [ + { + choice: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: "Gathering more information about this is required.", + risk: "unknown", + }, + { + choice: "Quarkus", + risk: "green", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + autotag: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + choice: "Spring Boot", + risk: "green", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + autotag: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + choice: "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", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + autotag: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + choice: "J2EE", + rationale: "This is obsolete.", + mitigation: + "Maybe start thinking about migrating to Quarkus or Jakarta EE.", + risk: "red", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + autotag: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + ], + }, + { + formulation: "What version of Java EE does the application use?", + explanation: + "What version of the Java EE specification is your application using?", + answers: [ + { + choice: "Below 5.", + rationale: "This technology stack is obsolete.", + mitigation: "Consider migrating to at least Java EE 7.", + risk: "red", + }, + { + choice: "5 or 6", + rationale: "This is a mostly outdated stack.", + mitigation: "Consider migrating to at least Java EE 7.", + risk: "yellow", + }, + { + choice: "7", + risk: "green", + }, + ], + include_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + formulation: "Does your application use any caching mechanism?", + answers: [ + { + choice: "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", + autoanswer_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + ], + }, + { + choice: "No", + risk: "green", + }, + { + choice: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: "Gathering more information about this is required.", + risk: "unknown", + }, + ], + }, + { + formulation: + "What implementation of JAX-WS does your application use?", + answers: [ + { + choice: "Apache Axis", + rationale: "This version is obsolete", + mitigation: "Consider migrating to Apache CXF", + risk: "red", + }, + { + choice: "Apache CXF", + risk: "green", + }, + { + choice: "Unknown", + rationale: "This is a problem because of the uncertainty.", + mitigation: "Gathering more information about this is required.", + risk: "unknown", + }, + ], + skip_if_tags_present: [ + { + category: { + name: "Cat 1", + id: 23, + }, + id: 34, + name: "Tag 1", + }, + { + category: { + name: "Cat 2", + id: 23, + }, + id: 34, + name: "Tag 2", + }, + ], + }, + ], + }, + ], + thresholds: { red: "5", yellow: "25", unknown: "70" }, + riskMessages: { + green: "Low Risk", + red: "High Risk", + yellow: "Medium Risk", + unknown: "Low Risk", + }, +}; 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 c1a51da30f..28f0592256 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 @@ -1,110 +1,137 @@ -import React, { useState } from "react"; -import { useHistory } from "react-router-dom"; +// External libraries +import * as React from "react"; +import { useState } from "react"; import { AxiosError } from "axios"; -import { useTranslation, Trans } from "react-i18next"; -import { IconedStatus } from "@app/components/IconedStatus"; +import { useHistory, useLocation } from "react-router-dom"; +import { Trans, useTranslation } from "react-i18next"; + +// @patternfly import { + Toolbar, + ToolbarContent, + ToolbarItem, Button, + ToolbarGroup, ButtonVariant, + DropdownItem, + Dropdown, + MenuToggle, + MenuToggleElement, Modal, - ToolbarGroup, - ToolbarItem, - TooltipPosition, } from "@patternfly/react-core"; -import { DropdownItem } from "@patternfly/react-core/deprecated"; import { - cellWidth, - IAction, - ICell, - IExtraData, - IRow, - IRowData, - ISeparator, - nowrap, - sortable, + PencilAltIcon, + TagIcon, + EllipsisVIcon, + CubesIcon, +} from "@patternfly/react-icons"; +import { + Table, + Thead, + Tr, + Th, + Tbody, + Td, TableText, } from "@patternfly/react-table"; -import TagIcon from "@patternfly/react-icons/dist/esm/icons/tag-icon"; -import PencilAltIcon from "@patternfly/react-icons/dist/esm/icons/pencil-alt-icon"; +// @app components and utilities +import { AppPlaceholder } from "@app/components/AppPlaceholder"; +import { + FilterType, + FilterToolbar, + FilterCategory, +} from "@app/components/FilterToolbar/FilterToolbar"; +import { SimplePagination } from "@app/components/SimplePagination"; +import { + TableHeaderContentWithControls, + ConditionalTableBody, + TableRowContentWithControls, +} from "@app/components/TableControls"; +import { IconedStatus } from "@app/components/IconedStatus"; +import { ToolbarBulkSelector } from "@app/components/ToolbarBulkSelector"; import { ApplicationDependenciesFormContainer } from "@app/components/ApplicationDependenciesFormContainer"; -import { formatPath, Paths } from "@app/Paths"; -import { Application, Assessment, Task } from "@app/api/models"; -import { ApplicationForm } from "../components/application-form"; -import { ApplicationAssessmentStatus } from "../components/application-assessment-status"; -import { ApplicationBusinessService } from "../components/application-business-service"; -import { ImportApplicationsForm } from "../components/import-applications-form"; -import { BulkCopyAssessmentReviewForm } from "../components/bulk-copy-assessment-review-form"; +import { ConfirmDialog } from "@app/components/ConfirmDialog"; +import { NotificationsContext } from "@app/components/NotificationsContext"; +import { dedupeFunction, getAxiosErrorMessage } from "@app/utils/utils"; +import { Paths, formatPath } from "@app/Paths"; +import keycloak from "@app/keycloak"; import { - applicationsWriteScopes, - dependenciesWriteScopes, - importsWriteScopes, - modifiedPathfinderWriteScopes, RBAC, RBAC_TYPE, + applicationsWriteScopes, + importsWriteScopes, } from "@app/rbac"; -import { checkAccess, checkAccessAll } from "@app/utils/rbac-utils"; -import keycloak from "@app/keycloak"; +import { checkAccess } from "@app/utils/rbac-utils"; + +// Hooks +import { useQueryClient } from "@tanstack/react-query"; +import { + handlePropagatedRowClick, + useLocalTableControls, +} from "@app/hooks/table-controls"; +import { useAssessApplication } from "@app/hooks"; + +// Queries +import { Application, Assessment, Task } from "@app/api/models"; import { ApplicationsQueryKey, useBulkDeleteApplicationMutation, useFetchApplications, } from "@app/queries/applications"; -import { - ApplicationTableType, - useApplicationsFilterValues, -} from "../applicationsFilter"; -import { FilterToolbar } from "@app/components/FilterToolbar/FilterToolbar"; -import { useDeleteReviewMutation, useFetchReviews } from "@app/queries/reviews"; +import { useFetchTasks } from "@app/queries/tasks"; import { useDeleteAssessmentMutation, useFetchApplicationAssessments, } from "@app/queries/assessments"; -import { useQueryClient } from "@tanstack/react-query"; -import { useAssessApplication } from "@app/hooks/useAssessApplication"; -import { NotificationsContext } from "@app/components/NotificationsContext"; +import { useDeleteReviewMutation, useFetchReviews } from "@app/queries/reviews"; +import { useFetchIdentities } from "@app/queries/identities"; +import { useFetchTagCategories } from "@app/queries/tags"; import { useCreateBulkCopyMutation } from "@app/queries/bulkcopy"; + +// Relative components +import { ApplicationAssessmentStatus } from "../components/application-assessment-status"; +import { ApplicationBusinessService } from "../components/application-business-service"; import { ApplicationDetailDrawerAssessment } from "../components/application-detail-drawer"; -import { useSetting } from "@app/queries/settings"; -import { useFetchTasks } from "@app/queries/tasks"; -import { getAxiosErrorMessage } from "@app/utils/utils"; -import { ConditionalTooltip } from "@app/components/ConditionalTooltip"; -import { StatusIcon } from "@app/components/StatusIcon"; +import { ApplicationForm } from "../components/application-form"; +import { BulkCopyAssessmentReviewForm } from "../components/bulk-copy-assessment-review-form"; +import { ImportApplicationsForm } from "../components/import-applications-form"; import { ConditionalRender } from "@app/components/ConditionalRender"; -import { AppPlaceholder } from "@app/components/AppPlaceholder"; -import { AppTableWithControls } from "@app/components/AppTableWithControls"; -import { ToolbarBulkSelector } from "@app/components/ToolbarBulkSelector"; -import { KebabDropdown } from "@app/components/KebabDropdown"; import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; -import { ConfirmDialog } from "@app/components/ConfirmDialog"; - -const ENTITY_FIELD = "entity"; - -const getRow = (rowData: IRowData): Application => { - return rowData[ENTITY_FIELD]; -}; +import { ConditionalTooltip } from "@app/components/ConditionalTooltip"; export const ApplicationsTable: React.FC = () => { - //RBAC - const token = keycloak.tokenParsed; const { t } = useTranslation(); + const history = useHistory(); + const token = keycloak.tokenParsed; + const { pushNotification } = React.useContext(NotificationsContext); - const [saveApplicationsModalState, setSavesApplicationsModalState] = + const [isKebabOpen, setIsKebabOpen] = React.useState(null); + const [isToolbarKebabOpen, setIsToolbarKebabOpen] = + React.useState(false); + + const [saveApplicationModalState, setSaveApplicationModalState] = React.useState<"create" | Application | null>(null); + const isCreateUpdateApplicationsModalOpen = - saveApplicationsModalState !== null; + saveApplicationModalState !== null; + const createUpdateApplications = - saveApplicationsModalState !== "create" ? saveApplicationsModalState : null; + saveApplicationModalState !== "create" ? saveApplicationModalState : null; - const [applicationToCopyAssessmentFrom, setAapplicationToCopyAssessmentFrom] = + const [applicationToCopyAssessmentFrom, setApplicationToCopyAssessmentFrom] = React.useState(null); + const isCopyAssessmentModalOpen = applicationToCopyAssessmentFrom !== null; + const [isAssessModalOpen, setAssessModalOpen] = React.useState(false); + const [ applicationToCopyAssessmentAndReviewFrom, setCopyAssessmentAndReviewModalState, ] = React.useState(null); + const isCopyAssessmentAndReviewModalOpen = applicationToCopyAssessmentAndReviewFrom !== null; @@ -112,6 +139,9 @@ export const ApplicationsTable: React.FC = () => { React.useState(null); const isDependenciesModalOpen = applicationDependenciesToManage !== null; + const [applicationToAssess, setApplicationToAssess] = + React.useState(null); + const [assessmentToEdit, setAssessmentToEdit] = React.useState(null); @@ -126,46 +156,21 @@ export const ApplicationsTable: React.FC = () => { const [isSubmittingBulkCopy, setIsSubmittingBulkCopy] = useState(false); - // Router - const history = useHistory(); - - // Table data - const { - data: applications, - isFetching, - error: fetchError, - refetch: fetchApplications, - } = useFetchApplications(); + const getTask = (application: Application) => + tasks.find((task: Task) => task.application?.id === application.id); const { tasks } = useFetchTasks({ addon: "analyzer" }); - const getTask = (application: Application) => - tasks.find((task: Task) => task.application?.id === application.id); + const { tagCategories: tagCategories } = useFetchTagCategories(); - const queryClient = useQueryClient(); + const { identities } = useFetchIdentities(); const { - paginationProps, - sortBy, - onSort, - filterCategories, - filterValues, - setFilterValues, - handleOnClearAllFilters, - currentPageItems, - isRowSelected, - toggleRowSelected, - selectAll, - selectMultiple, - areAllSelected, - selectedRows, - openDetailDrawer, - closeDetailDrawer, - activeAppInDetailDrawer, - } = useApplicationsFilterValues( - ApplicationTableType.Assessment, - applications - ); + data: applications, + isFetching: isFetchingApplications, + error: applicationsFetchError, + refetch: fetchApplications, + } = useFetchApplications(); const onDeleteApplicationSuccess = (appIDCount: number) => { pushNotification({ @@ -174,7 +179,7 @@ export const ApplicationsTable: React.FC = () => { }), variant: "success", }); - activeAppInDetailDrawer && closeDetailDrawer(); + clearActiveRow(); setApplicationsToDelete([]); }; @@ -186,28 +191,6 @@ export const ApplicationsTable: React.FC = () => { setApplicationsToDelete([]); }; - const { - reviews, - isFetching: isFetchingReviews, - fetchError: fetchErrorReviews, - } = useFetchReviews(); - - const appReview = reviews?.find( - (review) => - review.id === applicationToCopyAssessmentAndReviewFrom?.review?.id - ); - - // Application import modal - const [isApplicationImportModalOpen, setIsApplicationImportModalOpen] = - useState(false); - - // Table's assessments - const { - getApplicationAssessment, - isLoadingApplicationAssessment, - fetchErrorApplicationAssessment, - } = useFetchApplicationAssessments(applications); - const { mutate: bulkDeleteApplication } = useBulkDeleteApplicationMutation( onDeleteApplicationSuccess, onDeleteApplicationError @@ -230,202 +213,11 @@ export const ApplicationsTable: React.FC = () => { }); fetchApplications(); }; - const { mutate: createCopy, isCopying } = useCreateBulkCopyMutation({ onSuccess: onHandleCopySuccess, onError: onHandleCopyError, }); - // Create assessment - const { assessApplication, inProgress: isApplicationAssessInProgress } = - useAssessApplication(); - - // Table - const columns: ICell[] = [ - { - title: t("terms.name"), - transforms: [sortable, cellWidth(20)], - }, - { title: t("terms.description"), transforms: [cellWidth(25)] }, - { - title: t("terms.businessService"), - transforms: [sortable, cellWidth(20)], - }, - { - title: t("terms.assessment"), - transforms: [cellWidth(10)], - cellTransforms: [nowrap], - }, - { - title: t("terms.review"), - transforms: [cellWidth(10)], - cellTransforms: [nowrap], - }, - { - title: t("terms.tagCount"), - transforms: [sortable, cellWidth(10)], - cellTransforms: [nowrap], - }, - { - title: "", - props: { - className: "pf-v5-c-table__inline-edit-action", - }, - }, - ]; - - const rows: IRow[] = []; - currentPageItems?.forEach((item) => { - const isSelected = isRowSelected(item); - - rows.push({ - [ENTITY_FIELD]: item, - selected: isSelected, - isClickable: true, - isRowSelected: activeAppInDetailDrawer?.id === item.id, - cells: [ - { - title: {item.name}, - }, - { - title: ( - {item.description} - ), - }, - { - title: ( - - {item.businessService && ( - - )} - - ), - }, - { - title: ( - - ), - }, - { - title: ( - - ), - }, - { - title: ( - <> - {item.tags ? item.tags.length : 0} - - ), - }, - { - title: ( - - - - ), - }, - ], - }); - }); - - const actionResolver = (rowData: IRowData): (IAction | ISeparator)[] => { - const row: Application = getRow(rowData); - if (!row) { - return []; - } - - const applicationAssessment = getApplicationAssessment(row.id!); - - const userScopes: string[] = token?.scope.split(" ") || [], - dependenciesWriteAccess = checkAccess( - userScopes, - dependenciesWriteScopes - ), - applicationWriteAccess = checkAccess(userScopes, applicationsWriteScopes); - - const actions: (IAction | ISeparator)[] = []; - if ( - applicationAssessment?.status === "COMPLETE" && - checkAccess(userScopes, ["assessments:patch"]) - ) { - actions.push({ - title: t("actions.copyAssessment"), - onClick: () => setAapplicationToCopyAssessmentFrom(row), - }); - } - if ( - row.review && - applicationAssessment?.status === "COMPLETE" && - checkAccessAll(userScopes, ["assessments:patch", "reviews:post"]) - ) { - actions.push({ - title: t("actions.copyAssessmentAndReview"), - onClick: () => setCopyAssessmentAndReviewModalState(row), - }); - } - if ( - (applicationAssessment?.status || row.review) && - checkAccess(userScopes, ["assessments:delete"]) - ) { - actions.push({ - title: t("actions.discardAssessment"), - onClick: () => { - setAssessmentOrReviewToDiscard(row); - // setIsDiscardAssessmentConfirmDialogOpen(true); - fetchApplications(); - }, - }); - } - if (applicationWriteAccess) { - actions.push({ - title: t("actions.delete"), - ...(row.migrationWave !== null && { - isAriaDisabled: true, - tooltipProps: { - position: TooltipPosition.top, - content: "Cannot delete application assigned to a migration wave.", - }, - }), - onClick: () => setApplicationsToDelete([row]), - }); - } - - if (dependenciesWriteAccess) { - actions.push({ - title: t("actions.manageDependencies"), - onClick: () => setApplicationDependenciesToManage(row), - }); - } - - return actions; - }; - - // Row actions - const selectRow = ( - event: React.FormEvent, - isSelected: boolean, - rowIndex: number, - rowData: IRowData, - extraData: IExtraData - ) => { - const row = getRow(rowData); - toggleRowSelected(row); - }; - const onDeleteReviewSuccess = (name: string) => { pushNotification({ title: t("toastr.success.reviewDiscarded", { @@ -473,71 +265,185 @@ export const ApplicationsTable: React.FC = () => { } }; - // Toolbar actions - const assessSelectedRows = () => { - if (selectedRows.length !== 1) { - const msg = "The number of applications to be assessed must be 1"; - pushNotification({ - title: msg, - variant: "danger", - }); - return; - } + const { + getApplicationAssessment, + isLoadingApplicationAssessment, + fetchErrorApplicationAssessment, + } = useFetchApplicationAssessments(applications); - const row = selectedRows[0]; - assessApplication( - row, - (assessment: Assessment) => { - if (assessment.status === "COMPLETE") { - setAssessmentToEdit(assessment); - } else { - history.push( - formatPath(Paths.applicationsAssessment, { - assessmentId: assessment.id, - }) - ); - } + const { assessApplication, inProgress: isApplicationAssessInProgress } = + useAssessApplication(); + + const tableControls = useLocalTableControls({ + idProperty: "id", + items: applications || [], + columnNames: { + name: "Name", + description: "Description", + businessService: "Business Service", + assessment: "Assessment", + review: "Review", + tags: "Tags", + }, + sortableColumns: ["name", "description", "businessService", "tags"], + initialSort: { columnKey: "name", direction: "asc" }, + filterCategories: [ + { + key: "name", + title: t("terms.name"), + type: FilterType.search, + placeholderText: + t("actions.filterBy", { + what: t("terms.name").toLowerCase(), + }) + "...", + getItemValue: (item) => item?.name || "", }, - (error: AxiosError) => { - pushNotification({ - title: getAxiosErrorMessage(error), - variant: "danger", - }); - } - ); - }; + { + key: "description", + title: t("terms.description"), + type: FilterType.search, + placeholderText: + t("actions.filterBy", { + what: t("terms.description").toLowerCase(), + }) + "...", + getItemValue: (item) => item.description || "", + }, + { + key: "businessService", + title: t("terms.businessService"), + placeholderText: + t("actions.filterBy", { + what: t("terms.businessService").toLowerCase(), + }) + "...", + type: FilterType.select, + selectOptions: dedupeFunction( + applications + .filter((app) => !!app.businessService?.name) + .map((app) => app.businessService?.name) + .map((name) => ({ key: name, value: name })) + ), + getItemValue: (item) => item.businessService?.name || "", + }, + { + key: "identities", + title: t("terms.credentialType"), + placeholderText: + t("actions.filterBy", { + what: t("terms.credentialType").toLowerCase(), + }) + "...", + type: FilterType.multiselect, + selectOptions: [ + { key: "source", value: "Source" }, + { key: "maven", value: "Maven" }, + { key: "proxy", value: "Proxy" }, + ], + getItemValue: (item) => { + const searchStringArr: string[] = []; + item.identities?.forEach((appIdentity) => { + const matchingIdentity = identities.find( + (identity) => identity.id === appIdentity.id + ); + searchStringArr.push(matchingIdentity?.kind || ""); + }); + const searchString = searchStringArr.join(""); + return searchString; + }, + }, + { + key: "repository", + title: t("terms.repositoryType"), + placeholderText: + t("actions.filterBy", { + what: t("terms.repositoryType").toLowerCase(), + }) + "...", + type: FilterType.select, + selectOptions: [ + { key: "git", value: "Git" }, + { key: "subversion", value: "Subversion" }, + ], + getItemValue: (item) => item?.repository?.kind || "", + }, + { + key: "binary", + title: t("terms.artifact"), + placeholderText: + t("actions.filterBy", { + what: t("terms.artifact").toLowerCase(), + }) + "...", + type: FilterType.select, + selectOptions: [ + { key: "binary", value: t("terms.artifactAssociated") }, + { key: "none", value: t("terms.artifactNotAssociated") }, + ], + getItemValue: (item) => { + const hasBinary = + item.binary !== "::" && item.binary?.match(/.+:.+:.+/) + ? "binary" + : "none"; + + return hasBinary; + }, + }, + { + key: "tags", + title: t("terms.tags"), + type: FilterType.multiselect, + placeholderText: + t("actions.filterBy", { + what: t("terms.tagName").toLowerCase(), + }) + "...", + getItemValue: (item) => { + let tagNames = item?.tags?.map((tag) => tag.name).join(""); + return tagNames || ""; + }, + selectOptions: dedupeFunction( + tagCategories + ?.map((tagCategory) => tagCategory?.tags) + .flat() + .filter((tag) => tag && tag.name) + .map((tag) => ({ key: tag?.name, value: tag?.name })) + ), + }, + ], + initialItemsPerPage: 10, + hasActionsColumn: true, + isSelectable: true, + }); - const { data: reviewAssessmentSetting } = useSetting( - "review.assessment.required" - ); + const queryClient = useQueryClient(); - const reviewSelectedRows = () => { - if (selectedRows.length !== 1) { - const msg = "The number of applications to be reviewed must be 1"; - pushNotification({ - title: msg, - variant: "danger", - }); - return; - } + const { + currentPageItems, + numRenderedColumns, + propHelpers: { + toolbarProps, + filterToolbarProps, + paginationToolbarItemProps, + paginationProps, + tableProps, + getThProps, + getTdProps, + toolbarBulkSelectorProps, + getClickableTrProps, + }, + activeRowDerivedState: { activeRowItem, clearActiveRow }, - const row = selectedRows[0]; - if (row.review) setReviewToEdit(row.id); - else { - history.push( - formatPath(Paths.applicationsReview, { - applicationId: row.id, - }) - ); - } - }; + selectionState: { selectedItems: selectedRows }, + } = tableControls; - // Flags - const isReviewBtnDisabled = (row: Application) => { - if (reviewAssessmentSetting) return false; - const assessment = getApplicationAssessment(row.id!); - return assessment === undefined || assessment.status !== "COMPLETE"; - }; + const { + reviews, + isFetching: isFetchingReviews, + fetchError: fetchErrorReviews, + } = useFetchReviews(); + + const appReview = reviews?.find( + (review) => + review.id === applicationToCopyAssessmentAndReviewFrom?.review?.id + ); + + const [isApplicationImportModalOpen, setIsApplicationImportModalOpen] = + React.useState(false); const userScopes: string[] = token?.scope.split(" ") || [], importWriteAccess = checkAccess(userScopes, importsWriteScopes), @@ -589,350 +495,527 @@ export const ApplicationsTable: React.FC = () => { : []; const dropdownItems = [...importDropdownItems, ...applicationDeleteDropdown]; + const assessSelectedApp = (application: Application) => { + // if application/archetype has an assessment, ask if user wants to override it + setAssessModalOpen(true); + setApplicationToAssess(application); + // if() + // assessApplication( + // application, + // (assessment: Assessment) => { + // if (assessment.status === "COMPLETE") { + // setAssessmentToEdit(assessment); + // } else { + // history.push( + // formatPath(Paths.applicationsAssessment, { + // assessmentId: assessment.id, + // }) + // ); + // } + // }, + // (error: AxiosError) => { + // pushNotification({ + // title: getAxiosErrorMessage(error), + // variant: "danger", + // }); + // } + // ); + }; + const reviewSelectedApp = (application: Application) => { + if (application.review) { + setReviewToEdit(application.id); + } else { + history.push( + formatPath(Paths.applicationsReview, { + applicationId: application.id, + }) + ); + } + }; + return ( - <> - } + } + > +
- { - if (activeAppInDetailDrawer === application) { - closeDetailDrawer(); - } else { - openDetailDrawer(application); - } - }} - toolbarToggle={ - - } - toolbarBulkSelector={ - - } - toolbarClearAllFilters={handleOnClearAllFilters} - toolbarActions={ - <> - - - - - - + + + + + + - - - + - - -
+
); }; diff --git a/client/src/app/pages/applications/assessment-actions/assessment-actions-page.tsx b/client/src/app/pages/applications/assessment-actions/assessment-actions-page.tsx new file mode 100644 index 0000000000..f7848eb55c --- /dev/null +++ b/client/src/app/pages/applications/assessment-actions/assessment-actions-page.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { + Text, + TextContent, + PageSection, + PageSectionVariants, + Breadcrumb, + BreadcrumbItem, +} from "@patternfly/react-core"; +import { Link, useParams } from "react-router-dom"; +import { AssessmentActionsRoute, Paths } from "@app/Paths"; +import { ConditionalRender } from "@app/components/ConditionalRender"; +import { AppPlaceholder } from "@app/components/AppPlaceholder"; +import { useTranslation } from "react-i18next"; +import { useFetchApplicationByID } from "@app/queries/applications"; +import AssessmentActionsTable from "./components/assessment-actions-table"; + +const AssessmentActions: React.FC = () => { + const { applicationId } = useParams(); + const { application } = useFetchApplicationByID(applicationId || ""); + return ( + <> + + + Assessment Actions + + + + Applications + + + Assessment + + + + + }> + + + + + + + ); +}; + +export default AssessmentActions; 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 new file mode 100644 index 0000000000..3e4d1a3df6 --- /dev/null +++ b/client/src/app/pages/applications/assessment-actions/components/assessment-actions-table.tsx @@ -0,0 +1,95 @@ +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"; +import { + ConditionalTableBody, + TableHeaderContentWithControls, + TableRowContentWithControls, +} from "@app/components/TableControls"; +import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; +import { Questionnaire } from "@app/api/models"; +import { Button } from "@patternfly/react-core"; +export interface AssessmentActionsTableProps { + questionnaires: Questionnaire[]; +} + +const AssessmentActionsTable: React.FC = ({ + questionnaires, +}) => { + const tableControls = useLocalTableControls({ + idProperty: "id", + items: questionnaires, + columnNames: { + questionnaires: "Required questionnaires", + }, + hasActionsColumn: false, + hasPagination: false, + variant: "compact", + }); + const { + currentPageItems, + numRenderedColumns, + propHelpers: { tableProps, getThProps, getTdProps }, + } = tableControls; + + return ( + <> + + + + + + + + + + } + > + + {currentPageItems?.map((questionnaire, rowIndex) => ( + <> + + + + + + + + ))} + + +
+ +
+ {questionnaire.name} + + +
+ + ); +}; + +export default AssessmentActionsTable; 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 83e7b7fa7a..e81726a2d0 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 @@ -296,7 +296,7 @@ const AssessmentSettings: React.FC = () => { setIsKebabOpen(null)} onOpenChange={(_isOpen) => setIsKebabOpen(null)} toggle={( @@ -306,7 +306,11 @@ const AssessmentSettings: React.FC = () => { ref={toggleRef} aria-label="kebab dropdown toggle" variant="plain" - onClick={() => setIsKebabOpen(rowIndex)} + onClick={() => { + isKebabOpen + ? setIsKebabOpen(null) + : setIsKebabOpen(questionnaire.id); + }} isExpanded={isKebabOpen === rowIndex} > diff --git a/client/src/app/pages/assessment-management/questionnaire/components/answer-table.tsx b/client/src/app/pages/assessment-management/questionnaire/components/answer-table.tsx index 6957676d05..3a7a26f773 100644 --- a/client/src/app/pages/assessment-management/questionnaire/components/answer-table.tsx +++ b/client/src/app/pages/assessment-management/questionnaire/components/answer-table.tsx @@ -12,14 +12,14 @@ import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { Answer } from "@app/api/models"; import { Label, Text } from "@patternfly/react-core"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { IconedStatus, IconedStatusPreset } from "@app/components/IconedStatus"; +import { IconedStatus } from "@app/components/IconedStatus"; import { TimesCircleIcon } from "@patternfly/react-icons"; import { WarningTriangleIcon } from "@patternfly/react-icons"; -export interface IWaveStatusTableProps { +export interface IAnswerTableProps { answers: Answer[]; } -const AnswerTable: React.FC = ({ answers }) => { +const AnswerTable: React.FC = ({ answers }) => { const { t } = useTranslation(); const tableControls = useLocalTableControls({ diff --git a/client/src/app/queries/applications.ts b/client/src/app/queries/applications.ts index 067fd75b64..7453b4c3ef 100644 --- a/client/src/app/queries/applications.ts +++ b/client/src/app/queries/applications.ts @@ -5,6 +5,7 @@ import { createApplication, deleteApplication, deleteBulkApplications, + getApplicationById, getApplications, updateAllApplications, updateApplication, @@ -12,6 +13,7 @@ import { import { reviewsQueryKey } from "./reviews"; import { assessmentsQueryKey } from "./assessments"; import { AxiosError } from "axios"; +import { mockQuestionnaire } from "@app/data/mock-questionnaire"; export interface IApplicationDependencyFetchState { applicationDependencies: ApplicationDependency[]; @@ -34,6 +36,14 @@ export const useFetchApplications = () => { queryClient.invalidateQueries([reviewsQueryKey]); queryClient.invalidateQueries([assessmentsQueryKey]); }, + select: (apps) => + apps.map( + (app: Application): Application => ({ + ...app, + //TODO: remove this mock data and replace with real data + assessments: [mockQuestionnaire], + }) + ), onError: (error: AxiosError) => console.log(error), }); return { @@ -44,6 +54,28 @@ export const useFetchApplications = () => { }; }; +export const ApplicationQueryKey = "application"; + +export const useFetchApplicationByID = (id: number | string) => { + const { data, isLoading, error } = useQuery({ + queryKey: [ApplicationQueryKey, id], + queryFn: () => getApplicationById(id), + onError: (error: AxiosError) => console.log("error, ", error), + select: (app): Application => { + return { + ...app.data, + //TODO: remove this mock data and replace with real data + assessments: [mockQuestionnaire, mockQuestionnaire], + }; + }, + }); + return { + application: data, + isFetching: isLoading, + fetchError: error, + }; +}; + export const useUpdateApplicationMutation = ( onSuccess: () => void, onError: (err: AxiosError) => void diff --git a/client/src/mocks/stub-new-work/questionnaires.ts b/client/src/mocks/stub-new-work/questionnaires.ts index ec9ed8fa28..91c827eaff 100644 --- a/client/src/mocks/stub-new-work/questionnaires.ts +++ b/client/src/mocks/stub-new-work/questionnaires.ts @@ -67,31 +67,60 @@ const data: Record = { 1: { id: 1, name: "System questionnaire", + description: "This is a custom questionnaire", + revision: 1, questions: 42, rating: "5% Red, 25% Yellow", dateImported: "8 Aug. 2023, 10:20 AM EST", required: false, system: true, + sections: [], + thresholds: { red: "5", yellow: "25", unknown: "70" }, + riskMessages: { + green: "Low Risk", + red: "High Risk", + yellow: "Medium Risk", + unknown: "Low Risk", + }, }, - 2: { id: 2, name: "Custom questionnaire", + description: "This is a custom questionnaire", + revision: 1, questions: 24, rating: "15% Red, 35% Yellow", dateImported: "9 Aug. 2023, 03:32 PM EST", required: true, system: false, + sections: [], + thresholds: { red: "5", yellow: "25", unknown: "70" }, + riskMessages: { + green: "Low Risk", + red: "High Risk", + yellow: "Medium Risk", + unknown: "Low Risk", + }, }, 3: { id: 3, name: "Ruby questionnaire", + description: "This is a ruby questionnaire", + revision: 1, questions: 34, rating: "7% Red, 25% Yellow", dateImported: "10 Aug. 2023, 11:23 PM EST", required: true, system: false, + sections: [], + thresholds: { red: "5", yellow: "25", unknown: "70" }, + riskMessages: { + green: "Low Risk", + red: "High Risk", + yellow: "Medium Risk", + unknown: "Low Risk", + }, }, };