diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index 3e66f825c1..b5bbf95c94 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -29,7 +29,9 @@ "createTags": "Create tags", "cancelAnalysis": "Cancel analysis", "delete": "Delete", - "discardAssessment": "Discard assessment/review", + "discardAssessment": "Discard assessment(s)", + "discardReview": "Discard review", + "downloadCsvTemplate": "Download CSV template", "download": "Download {{what}}", "duplicate": "Duplicate", @@ -360,6 +362,7 @@ "reports": "Reports", "repositoryType": "Repository type", "review": "Review", + "reviewedArchetype": "Archetype reviewed", "reviews": "Reviews", "reviewComments": "Review comments", "risk": "Risk", diff --git a/client/src/app/pages/applications/applications-table/applications-table.tsx b/client/src/app/pages/applications/applications-table/applications-table.tsx index 87427ed8ae..96b7d58184 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -17,6 +17,9 @@ import { MenuToggle, MenuToggleElement, Modal, + Tooltip, + Grid, + GridItem, } from "@patternfly/react-core"; import { PencilAltIcon, TagIcon, EllipsisVIcon } from "@patternfly/react-icons"; import { @@ -28,6 +31,7 @@ import { ActionsColumn, Tbody, } from "@patternfly/react-table"; +import { QuestionCircleIcon } from "@patternfly/react-icons/dist/esm/icons/question-circle-icon"; // @app components and utilities import { AppPlaceholder } from "@app/components/AppPlaceholder"; @@ -184,7 +188,10 @@ export const ApplicationsTable: React.FC = () => { Application[] >([]); - const [assessmentOrReviewToDiscard, setAssessmentOrReviewToDiscard] = + const [assessmentToDiscard, setAssessmentToDiscard] = + React.useState(null); + + const [reviewToDiscard, setReviewToDiscard] = React.useState(null); const { @@ -257,15 +264,8 @@ export const ApplicationsTable: React.FC = () => { onDeleteError ); - const discardAssessmentAndReview = async (application: Application) => { + const discardAssessment = async (application: Application) => { try { - if (application.review?.id) { - await deleteReview({ - id: application.review.id, - name: application.name, - }); - } - if (application.assessments) { await Promise.all( application.assessments.map(async (assessment) => { @@ -277,7 +277,20 @@ export const ApplicationsTable: React.FC = () => { ); } } catch (error) { - console.error("Error while deleting assessments and/or reviews:", error); + console.error("Error while deleting assessments:", error); + } + }; + + const discardReview = async (application: Application) => { + try { + if (application.review?.id) { + await deleteReview({ + id: application.review.id, + name: application.name, + }); + } + } catch (error) { + console.error("Error while deleting review:", error); } }; @@ -827,13 +840,27 @@ export const ApplicationsTable: React.FC = () => { modifier="truncate" {...getTdProps({ columnKey: "review" })} > - + + + + + + {hasReviewedArchetype ? ( + + + + ) : null} + + { title: t("actions.review"), onClick: () => reviewSelectedApp(application), }, - ...(application?.review + ...(application?.assessments?.length ? [ { title: t("actions.discardAssessment"), onClick: () => - setAssessmentOrReviewToDiscard( - application - ), + setAssessmentToDiscard(application), + }, + ] + : []), + ...(application?.review + ? [ + { + title: t("actions.discardReview"), + onClick: () => + setReviewToDiscard(application), }, ] : []), @@ -1071,16 +1105,46 @@ export const ApplicationsTable: React.FC = () => { what: t("terms.assessment").toLowerCase(), })} titleIconVariant={"warning"} - isOpen={assessmentOrReviewToDiscard !== null} + isOpen={assessmentToDiscard !== null} message={ + The assessment(s) for{" "} + {assessmentToDiscard?.name} will be discarded. + Do you wish to continue? + + + } + confirmBtnVariant={ButtonVariant.primary} + confirmBtnLabel={t("actions.continue")} + cancelBtnLabel={t("actions.cancel")} + onCancel={() => setAssessmentToDiscard(null)} + onClose={() => setAssessmentToDiscard(null)} + onConfirm={() => { + discardAssessment(assessmentToDiscard!); + setAssessmentToDiscard(null); + }} + /> + + - The assessment for applicationName will be + The review for {reviewToDiscard?.name} will be discarded, as well as the review result. Do you wish to continue? @@ -1089,11 +1153,11 @@ export const ApplicationsTable: React.FC = () => { confirmBtnVariant={ButtonVariant.primary} confirmBtnLabel={t("actions.continue")} cancelBtnLabel={t("actions.cancel")} - onCancel={() => setAssessmentOrReviewToDiscard(null)} - onClose={() => setAssessmentOrReviewToDiscard(null)} + onCancel={() => setReviewToDiscard(null)} + onClose={() => setReviewToDiscard(null)} onConfirm={() => { - discardAssessmentAndReview(assessmentOrReviewToDiscard!); - setAssessmentOrReviewToDiscard(null); + discardReview(reviewToDiscard!); + setReviewToDiscard(null); }} /> { const { t } = useTranslation(); @@ -71,6 +73,11 @@ const Archetypes: React.FC = () => { const [archetypeToEdit, setArchetypeToEdit] = useState( null ); + const [assessmentToDiscard, setAssessmentToDiscard] = + React.useState(null); + + const [reviewToDiscard, setReviewToDiscard] = + React.useState(null); const [archetypeToDuplicate, setArchetypeToDuplicate] = useState(null); @@ -97,6 +104,70 @@ const Archetypes: React.FC = () => { }), onError ); + const onDeleteAssessmentSuccess = (name: string) => { + pushNotification({ + title: t("toastr.success.assessmentDiscarded", { + application: name, + }), + variant: "success", + }); + }; + + const onDeleteError = (error: AxiosError) => { + pushNotification({ + title: getAxiosErrorMessage(error), + variant: "danger", + }); + }; + + const { mutate: deleteAssessment } = useDeleteAssessmentMutation( + onDeleteAssessmentSuccess, + onDeleteError + ); + + const discardAssessment = async (archetype: Archetype) => { + try { + if (archetype.assessments) { + await Promise.all( + archetype.assessments.map(async (assessment) => { + await deleteAssessment({ + assessmentId: assessment.id, + archetypeId: archetype.id, + }); + }) + ); + } + } catch (error) { + console.error("Error while deleting assessments:", error); + } + }; + + const onDeleteReviewSuccess = (name: string) => { + pushNotification({ + title: t("toastr.success.reviewDiscarded", { + application: name, + }), + variant: "success", + }); + }; + + const { mutate: deleteReview } = useDeleteReviewMutation( + onDeleteReviewSuccess, + onDeleteError + ); + + const discardReview = async (archetype: Archetype) => { + try { + if (archetype.review?.id) { + await deleteReview({ + id: archetype.review.id, + name: archetype.name, + }); + } + } catch (error) { + console.error("Error while deleting review:", error); + } + }; const tableControls = useLocalTableControls({ persistTo: "urlParams", @@ -319,6 +390,24 @@ const Archetypes: React.FC = () => { title: t("actions.edit"), onClick: () => setArchetypeToEdit(archetype), }, + ...(archetype?.assessments?.length + ? [ + { + title: t("actions.discardAssessment"), + onClick: () => + setAssessmentToDiscard(archetype), + }, + ] + : []), + ...(archetype?.review + ? [ + { + title: t("actions.discardReview"), + onClick: () => + setReviewToDiscard(archetype), + }, + ] + : []), { isSeparator: true }, { title: t("actions.delete"), @@ -386,6 +475,64 @@ const Archetypes: React.FC = () => { onClose={() => setArchetypeToDuplicate(null)} /> + + + The assessment(s) for {assessmentToDiscard?.name}{" "} + will be discarded. Do you wish to continue? + + + } + confirmBtnVariant={ButtonVariant.primary} + confirmBtnLabel={t("actions.continue")} + cancelBtnLabel={t("actions.cancel")} + onCancel={() => setAssessmentToDiscard(null)} + onClose={() => setAssessmentToDiscard(null)} + onConfirm={() => { + discardAssessment(assessmentToDiscard!); + setAssessmentToDiscard(null); + }} + /> + + + The review for {reviewToDiscard?.name} will be + discarded, as well as the review result. Do you wish to continue? + + + } + confirmBtnVariant={ButtonVariant.primary} + confirmBtnLabel={t("actions.continue")} + cancelBtnLabel={t("actions.cancel")} + onCancel={() => setReviewToDiscard(null)} + onClose={() => setReviewToDiscard(null)} + onConfirm={() => { + discardReview(reviewToDiscard!); + setReviewToDiscard(null); + }} + /> {/* Delete confirm modal */} { onSuccess: () => { queryClient.invalidateQueries([reviewsQueryKey]); queryClient.invalidateQueries([assessmentsQueryKey]); + queryClient.invalidateQueries([assessmentsByItemIdQueryKey]); }, onError: (error: AxiosError) => console.log(error), }); diff --git a/client/src/app/queries/archetypes.ts b/client/src/app/queries/archetypes.ts index 5bdad16aec..7b0de34b64 100644 --- a/client/src/app/queries/archetypes.ts +++ b/client/src/app/queries/archetypes.ts @@ -9,16 +9,28 @@ import { getArchetypes, updateArchetype, } from "@app/api/rest"; +import { + assessmentsQueryKey, + assessmentsByItemIdQueryKey, +} from "./assessments"; +import { reviewsQueryKey } from "./reviews"; export const ARCHETYPES_QUERY_KEY = "archetypes"; export const ARCHETYPE_QUERY_KEY = "archetype"; export const useFetchArchetypes = () => { + const queryClient = useQueryClient(); const { isLoading, isSuccess, error, refetch, data } = useQuery({ initialData: [], queryKey: [ARCHETYPES_QUERY_KEY], queryFn: getArchetypes, refetchInterval: 5000, + onSuccess: () => { + queryClient.invalidateQueries([reviewsQueryKey]); + queryClient.invalidateQueries([assessmentsQueryKey]); + queryClient.invalidateQueries([assessmentsByItemIdQueryKey]); + }, + onError: (error: AxiosError) => console.log(error), });