diff --git a/client/src/app/components/questions-table/questions-table.tsx b/client/src/app/components/questions-table/questions-table.tsx index ec5cabd59..81a6f9a10 100644 --- a/client/src/app/components/questions-table/questions-table.tsx +++ b/client/src/app/components/questions-table/questions-table.tsx @@ -45,6 +45,7 @@ const QuestionsTable: React.FC<{ section: "Section", }, isExpansionEnabled: true, + isPaginationEnabled: false, expandableVariant: "single", forceNumRenderedColumns: isAllQuestionsTab ? 3 : 2, // columns+1 for expand control }); diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index 9128b1d59..ec98ee935 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -48,7 +48,7 @@ export const getLocalTableControlDerivedState = < items: sortedItems, }); return { - totalItemCount: items.length, + totalItemCount: filteredItems.length, currentPageItems: isPaginationEnabled ? currentPageItems : sortedItems, }; }; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 256166ac2..a96829c6a 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -231,7 +231,7 @@ export type ITableControlDerivedState = { */ currentPageItems: TItem[]; /** - * The total number of items in the entire un-filtered, un-paginated table (the size of the entire API collection being tabulated). + * The total number of items after filtering but before pagination. */ totalItemCount: number; }; diff --git a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx index 12887de1d..e53940ab5 100644 --- a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx +++ b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx @@ -189,7 +189,7 @@ export const ApplicationDetailDrawer: React.FC< isCompact columnModifier={{ default: "1Col" }} horizontalTermWidthModifier={{ - default: "14ch", + default: "15ch", }} > diff --git a/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx b/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx index fccb89c46..bf118dbba 100644 --- a/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx +++ b/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx @@ -16,7 +16,7 @@ export const ApplicationReviewStatus: React.FC< > = ({ application }) => { const { t } = useTranslation(); - const { archetypes, isFetching } = useFetchArchetypes(); + const { archetypes, isFetching, error } = useFetchArchetypes(); const isAppReviewed = !!application.review; const applicationArchetypes = application.archetypes?.map((archetypeRef) => { @@ -27,6 +27,10 @@ export const ApplicationReviewStatus: React.FC< applicationArchetypes?.filter((archetype) => !!archetype?.review).length || 0; + if (error) { + return ; + } + if (isFetching) { return ; } @@ -43,9 +47,5 @@ export const ApplicationReviewStatus: React.FC< statusPreset = "NotStarted"; } - if (!applicationArchetypes || applicationArchetypes.length === 0) { - return ; - } - return ; }; diff --git a/client/src/app/pages/archetypes/archetypes-page.tsx b/client/src/app/pages/archetypes/archetypes-page.tsx index 82eefd106..117483700 100644 --- a/client/src/app/pages/archetypes/archetypes-page.tsx +++ b/client/src/app/pages/archetypes/archetypes-page.tsx @@ -59,6 +59,13 @@ import { SimplePagination } from "@app/components/SimplePagination"; import { TablePersistenceKeyPrefix } from "@app/Constants"; import { useDeleteAssessmentMutation } from "@app/queries/assessments"; import { useDeleteReviewMutation } from "@app/queries/reviews"; +import { + assessmentWriteScopes, + reviewsWriteScopes, + archetypesWriteScopes, +} from "@app/rbac"; +import { checkAccess } from "@app/utils/rbac-utils"; +import keycloak from "@app/keycloak"; const Archetypes: React.FC = () => { const { t } = useTranslation(); @@ -272,6 +279,12 @@ const Archetypes: React.FC = () => { } }; + const token = keycloak.tokenParsed; + const userScopes: string[] = token?.scope.split(" ") || [], + archetypeWriteAccess = checkAccess(userScopes, archetypesWriteScopes), + assessmentWriteAccess = checkAccess(userScopes, assessmentWriteScopes), + reviewsWriteAccess = checkAccess(userScopes, reviewsWriteScopes); + return ( <> @@ -367,26 +380,44 @@ const Archetypes: React.FC = () => { - setArchetypeToDuplicate(archetype), - }, - { - title: t("actions.assess"), - onClick: () => - assessSelectedArchetype(archetype), - }, - { - title: t("actions.review"), - onClick: () => - reviewSelectedArchetype(archetype), - }, - { - title: t("actions.edit"), - onClick: () => setArchetypeToEdit(archetype), - }, - ...(archetype?.assessments?.length + ...(archetypeWriteAccess + ? [ + { + title: t("actions.duplicate"), + onClick: () => + setArchetypeToDuplicate(archetype), + }, + ] + : []), + ...(assessmentWriteAccess + ? [ + { + title: t("actions.assess"), + onClick: () => + assessSelectedArchetype(archetype), + }, + ] + : []), + ...(reviewsWriteAccess + ? [ + { + title: t("actions.review"), + onClick: () => + reviewSelectedArchetype(archetype), + }, + ] + : []), + ...(archetypeWriteAccess + ? [ + { + title: t("actions.edit"), + onClick: () => + setArchetypeToEdit(archetype), + }, + ] + : []), + ...(archetype?.assessments?.length && + assessmentWriteAccess ? [ { title: t("actions.discardAssessment"), @@ -395,7 +426,7 @@ const Archetypes: React.FC = () => { }, ] : []), - ...(archetype?.review + ...(archetype?.review && reviewsWriteAccess ? [ { title: t("actions.discardReview"), @@ -405,11 +436,16 @@ const Archetypes: React.FC = () => { ] : []), { isSeparator: true }, - { - title: t("actions.delete"), - onClick: () => setArchetypeToDelete(archetype), - isDanger: true, - }, + ...(archetypeWriteAccess + ? [ + { + title: t("actions.delete"), + onClick: () => + setArchetypeToDelete(archetype), + isDanger: true, + }, + ] + : []), ]} /> diff --git a/client/src/app/pages/assessment/components/assessment-actions/components/dynamic-assessment-actions-row.tsx b/client/src/app/pages/assessment/components/assessment-actions/components/dynamic-assessment-actions-row.tsx index ac6f1f474..bf77aa71f 100644 --- a/client/src/app/pages/assessment/components/assessment-actions/components/dynamic-assessment-actions-row.tsx +++ b/client/src/app/pages/assessment/components/assessment-actions/components/dynamic-assessment-actions-row.tsx @@ -28,6 +28,7 @@ import { } from "@tanstack/react-query"; import { TrashIcon } from "@patternfly/react-icons"; import useIsArchetype from "@app/hooks/useIsArchetype"; +import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; enum AssessmentAction { Take = "Take", @@ -189,6 +190,7 @@ const DynamicAssessmentActionsRow: FunctionComponent< assessmentId: assessment.id, applicationName: application?.name, applicationId: application?.id, + archetypeName: archetype?.name, archetypeId: archetype?.id, }).then(() => { createAssessment(); @@ -222,7 +224,7 @@ const DynamicAssessmentActionsRow: FunctionComponent< {action} ) : ( - + Loading... )} @@ -262,6 +264,7 @@ const DynamicAssessmentActionsRow: FunctionComponent< assessmentId: assessment.id, applicationName: application?.name, applicationId: application?.id, + archetypeName: archetype?.name, archetypeId: archetype?.id, }); }} diff --git a/client/src/app/pages/assessment/components/assessment-wizard/assessment-wizard.tsx b/client/src/app/pages/assessment/components/assessment-wizard/assessment-wizard.tsx index ed8e3140a..9a06372c8 100644 --- a/client/src/app/pages/assessment/components/assessment-wizard/assessment-wizard.tsx +++ b/client/src/app/pages/assessment/components/assessment-wizard/assessment-wizard.tsx @@ -447,6 +447,7 @@ export const AssessmentWizard: React.FC = ({ assessmentId: assessment.id, applicationName: assessment.application?.name, applicationId: assessment.application?.id, + archetypeName: assessment.archetype?.name, archetypeId: assessment.archetype?.id, }); } else { @@ -455,6 +456,7 @@ export const AssessmentWizard: React.FC = ({ assessmentId: assessment.id, applicationName: assessment.application?.name, applicationId: assessment.application?.id, + archetypeName: assessment.archetype?.name, archetypeId: assessment.archetype?.id, }); } diff --git a/client/src/app/pages/assessment/components/view-archetypes/components/view-archetypes-table.tsx b/client/src/app/pages/assessment/components/view-archetypes/components/view-archetypes-table.tsx index c6b741f9e..c8235eed2 100644 --- a/client/src/app/pages/assessment/components/view-archetypes/components/view-archetypes-table.tsx +++ b/client/src/app/pages/assessment/components/view-archetypes/components/view-archetypes-table.tsx @@ -24,6 +24,24 @@ const ViewArchetypesTable: React.FC = ({ const archivedQuestionnaires = questionnaires.filter( (questionnaire) => !questionnaire.required ); + + const nonRequiredQuestionnaireIds = questionnaires + .filter((q) => !q.required) + .map((q) => q.id); + + const relevantAssessmentIds = (archetype?.assessments || []).map((a) => a.id); + + const filteredArchivedAssessments = assessments.filter( + (assessment) => + nonRequiredQuestionnaireIds.includes(assessment.questionnaire.id) && + relevantAssessmentIds.includes(assessment.id) + ); + const filteredArchivedQuestionnaires = archivedQuestionnaires.filter( + (questionnaire) => + filteredArchivedAssessments.some( + (assessment) => assessment.questionnaire.id === questionnaire.id + ) + ); return ( <> = ({ isFetching={isFetchingQuestionnaires || isFetchingAssessmentsById} tableName="Required questionnaires" /> - - + {filteredArchivedAssessments.length === 0 ? null : ( + + )} ); }; diff --git a/client/src/app/pages/reports/components/donut/donut.tsx b/client/src/app/pages/reports/components/donut/donut.tsx index 60b0ef040..6d5c72eb2 100644 --- a/client/src/app/pages/reports/components/donut/donut.tsx +++ b/client/src/app/pages/reports/components/donut/donut.tsx @@ -10,6 +10,7 @@ import { StackItem, Text, TextContent, + TextVariants, } from "@patternfly/react-core"; export interface IDonutProps { @@ -31,6 +32,7 @@ export const Donut: React.FC = ({ riskLabel, isAssessment, riskTitle, + riskDescription, }) => { const { t } = useTranslation(); @@ -63,6 +65,12 @@ export const Donut: React.FC = ({ {riskLabel} + + {riskDescription} + diff --git a/client/src/app/queries/assessments.ts b/client/src/app/queries/assessments.ts index b3796cbe0..8128ba75b 100644 --- a/client/src/app/queries/assessments.ts +++ b/client/src/app/queries/assessments.ts @@ -107,7 +107,7 @@ export const useUpdateAssessmentMutation = ( }; export const useDeleteAssessmentMutation = ( - onSuccess?: (applicationName: string) => void, + onSuccess?: (name: string) => void, onError?: (err: AxiosError) => void ) => { const queryClient = useQueryClient(); @@ -117,6 +117,7 @@ export const useDeleteAssessmentMutation = ( assessmentId: number; applicationName?: string; applicationId?: number; + archetypeName?: string; archetypeId?: number; }) => { const deletedAssessment = deleteAssessment(args.assessmentId); @@ -138,7 +139,8 @@ export const useDeleteAssessmentMutation = ( return deletedAssessment; }, onSuccess: (_, args) => { - onSuccess && onSuccess(args?.applicationName || "Unknown"); + onSuccess && + onSuccess(args?.applicationName || args?.archetypeName || "Unknown"); }, onError: onError, }); diff --git a/client/src/app/rbac.ts b/client/src/app/rbac.ts index aa4845c7d..cf136ff19 100644 --- a/client/src/app/rbac.ts +++ b/client/src/app/rbac.ts @@ -104,6 +104,12 @@ export const applicationsWriteScopes = [ "applications:delete", ]; +export const archetypesWriteScopes = [ + "archetypes:put", + "archetypes:post", + "archetypes:delete", +]; + export const analysisWriteScopes = [ "applications.analysis:put", "applications.analysis:post",