Skip to content

Commit 0379c59

Browse files
✨ Add SBOM|Advisory Delete action (guacsec#497)
Signed-off-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com>
1 parent c8a970d commit 0379c59

File tree

7 files changed

+378
-67
lines changed

7 files changed

+378
-67
lines changed

client/src/app/Constants.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { AxiosError } from "axios";
2+
import type { AdvisorySummary, SbomSummary } from "./client";
13
import ENV from "./env";
24

35
export const FILTER_TEXT_CATEGORY_KEY = "";
@@ -46,3 +48,27 @@ export const ANSICOLOR = {
4648
lightBlue: "\x1b[94m",
4749
red: "\x1b[31m",
4850
};
51+
52+
export const sbomDeleteDialogProps = (sbom?: SbomSummary | null) => ({
53+
title: "Permanently delete SBOM?",
54+
message: `This action permanently deletes the ${sbom?.name} SBOM.`,
55+
});
56+
57+
export const advisoryDeleteDialogProps = (
58+
advisory?: AdvisorySummary | null,
59+
) => ({
60+
title: "Permanently delete Advisory?",
61+
message: `This action permanently deletes the ${advisory?.document_id} Advisory.`,
62+
});
63+
64+
export const sbomDeletedSuccessMessage = (sbom: SbomSummary) =>
65+
`The SBOM ${sbom.name} was deleted`;
66+
67+
export const sbomDeletedErrorMessage = (_error: AxiosError) =>
68+
"Error occurred while deleting the SBOM";
69+
70+
export const advisoryDeletedSuccessMessage = (sbom: AdvisorySummary) =>
71+
`The Advisory ${sbom.document_id} was deleted`;
72+
73+
export const advisoryDeletedErrorMessage = (_error: AxiosError) =>
74+
"Error occurred while deleting the Advisory";

client/src/app/pages/advisory-details/advisory-details.tsx

Lines changed: 124 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,91 @@
11
import React from "react";
2-
import { Link } from "react-router-dom";
2+
import { Link, useNavigate } from "react-router-dom";
3+
4+
import type { AxiosError } from "axios";
35

46
import {
57
Breadcrumb,
68
BreadcrumbItem,
7-
Button,
9+
ButtonVariant,
810
Content,
11+
Divider,
12+
Dropdown,
13+
DropdownItem,
14+
DropdownList,
915
Flex,
1016
FlexItem,
1117
Label,
18+
MenuToggle,
19+
type MenuToggleElement,
1220
PageSection,
1321
Split,
1422
SplitItem,
1523
Tab,
1624
TabContent,
17-
TabTitleText,
1825
Tabs,
26+
TabTitleText,
1927
} from "@patternfly/react-core";
20-
import DownloadIcon from "@patternfly/react-icons/dist/esm/icons/download-icon";
2128

29+
import {
30+
advisoryDeletedErrorMessage,
31+
advisoryDeleteDialogProps,
32+
advisoryDeletedSuccessMessage,
33+
} from "@app/Constants";
2234
import { PathParam, Paths, useRouteParams } from "@app/Routes";
23-
35+
import type { AdvisorySummary } from "@app/client";
36+
import { ConfirmDialog } from "@app/components/ConfirmDialog";
2437
import { LoadingWrapper } from "@app/components/LoadingWrapper";
38+
import { NotificationsContext } from "@app/components/NotificationsContext";
2539
import { useDownload } from "@app/hooks/domain-controls/useDownload";
26-
import { useFetchAdvisoryById } from "@app/queries/advisories";
40+
import {
41+
useDeleteAdvisoryMutation,
42+
useFetchAdvisoryById,
43+
} from "@app/queries/advisories";
2744

2845
import { Overview } from "./overview";
2946
import { VulnerabilitiesByAdvisory } from "./vulnerabilities-by-advisory";
3047

3148
export const AdvisoryDetails: React.FC = () => {
49+
const navigate = useNavigate();
50+
const { pushNotification } = React.useContext(NotificationsContext);
51+
52+
const advisoryId = useRouteParams(PathParam.ADVISORY_ID);
53+
const { advisory, isFetching, fetchError } = useFetchAdvisoryById(advisoryId);
54+
55+
// Actions Dropdown
56+
const [isActionsDropdownOpen, setIsActionsDropdownOpen] =
57+
React.useState(false);
58+
59+
const handleActionsDropdownToggle = () => {
60+
setIsActionsDropdownOpen(!isActionsDropdownOpen);
61+
};
62+
63+
// Download action
64+
const { downloadAdvisory } = useDownload();
65+
66+
// Delete action
67+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false);
68+
69+
const onDeleteAdvisorySuccess = (advisory: AdvisorySummary) => {
70+
setIsDeleteDialogOpen(false);
71+
pushNotification({
72+
title: advisoryDeletedSuccessMessage(advisory),
73+
variant: "success",
74+
});
75+
navigate("/advisories");
76+
};
77+
78+
const onDeleteAdvisoryError = (error: AxiosError) => {
79+
pushNotification({
80+
title: advisoryDeletedErrorMessage(error),
81+
variant: "danger",
82+
});
83+
};
84+
85+
const { mutate: deleteAdvisory, isPending: isDeleting } =
86+
useDeleteAdvisoryMutation(onDeleteAdvisorySuccess, onDeleteAdvisoryError);
87+
88+
// Tabs
3289
const [activeTabKey, setActiveTabKey] = React.useState<string | number>(0);
3390

3491
const handleTabClick = (
@@ -41,13 +98,6 @@ export const AdvisoryDetails: React.FC = () => {
4198
const infoTabRef = React.createRef<HTMLElement>();
4299
const vulnerabilitiesTabRef = React.createRef<HTMLElement>();
43100

44-
//
45-
46-
const advisoryId = useRouteParams(PathParam.ADVISORY_ID);
47-
const { advisory, isFetching, fetchError } = useFetchAdvisoryById(advisoryId);
48-
49-
const { downloadAdvisory } = useDownload();
50-
51101
return (
52102
<>
53103
<PageSection type="breadcrumb">
@@ -78,23 +128,49 @@ export const AdvisoryDetails: React.FC = () => {
78128
</Flex>
79129
</SplitItem>
80130
<SplitItem>
81-
{!isFetching && (
82-
<Button
83-
variant="secondary"
84-
icon={<DownloadIcon />}
85-
onClick={() => {
86-
if (advisoryId) {
87-
downloadAdvisory(
88-
advisoryId,
89-
advisory?.identifier
90-
? `${advisory?.identifier}.json`
91-
: `${advisoryId}.json`,
92-
);
93-
}
94-
}}
131+
{advisory && (
132+
<Dropdown
133+
isOpen={isActionsDropdownOpen}
134+
onSelect={() => setIsActionsDropdownOpen(false)}
135+
onOpenChange={(isOpen) => setIsActionsDropdownOpen(isOpen)}
136+
popperProps={{ position: "right" }}
137+
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
138+
<MenuToggle
139+
ref={toggleRef}
140+
onClick={handleActionsDropdownToggle}
141+
isExpanded={isActionsDropdownOpen}
142+
>
143+
Actions
144+
</MenuToggle>
145+
)}
146+
ouiaId="BasicDropdown"
147+
shouldFocusToggleOnSelect
95148
>
96-
Download
97-
</Button>
149+
<DropdownList>
150+
<DropdownItem
151+
key="advisory"
152+
onClick={() => {
153+
if (advisoryId) {
154+
downloadAdvisory(
155+
advisoryId,
156+
advisory?.identifier
157+
? `${advisory?.identifier}.json`
158+
: `${advisoryId}.json`,
159+
);
160+
}
161+
}}
162+
>
163+
Download Advisory
164+
</DropdownItem>
165+
<Divider component="li" key="separator" />
166+
<DropdownItem
167+
key="delete"
168+
onClick={() => setIsDeleteDialogOpen(true)}
169+
>
170+
Delete
171+
</DropdownItem>
172+
</DropdownList>
173+
</Dropdown>
98174
)}
99175
</SplitItem>
100176
</Split>
@@ -104,7 +180,7 @@ export const AdvisoryDetails: React.FC = () => {
104180
mountOnEnter
105181
activeKey={activeTabKey}
106182
onSelect={handleTabClick}
107-
aria-label="Tabs that contain the SBOM information"
183+
aria-label="Tabs that contain the Advisory information"
108184
role="region"
109185
>
110186
<Tab
@@ -136,7 +212,7 @@ export const AdvisoryDetails: React.FC = () => {
136212
eventKey={1}
137213
id="refVulnerabilitiesSection"
138214
ref={vulnerabilitiesTabRef}
139-
aria-label="Vulnerabilities within the SBOM"
215+
aria-label="Vulnerabilities within the Advisory"
140216
hidden
141217
>
142218
<VulnerabilitiesByAdvisory
@@ -146,6 +222,23 @@ export const AdvisoryDetails: React.FC = () => {
146222
/>
147223
</TabContent>
148224
</PageSection>
225+
226+
<ConfirmDialog
227+
{...advisoryDeleteDialogProps(advisory)}
228+
inProgress={isDeleting}
229+
titleIconVariant="warning"
230+
isOpen={isDeleteDialogOpen}
231+
confirmBtnVariant={ButtonVariant.danger}
232+
confirmBtnLabel="Delete"
233+
cancelBtnLabel="Cancel"
234+
onCancel={() => setIsDeleteDialogOpen(false)}
235+
onClose={() => setIsDeleteDialogOpen(false)}
236+
onConfirm={() => {
237+
if (advisory) {
238+
deleteAdvisory(advisory.uuid);
239+
}
240+
}}
241+
/>
149242
</>
150243
);
151244
};

client/src/app/pages/advisory-list/advisory-table.tsx

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import React from "react";
22
import { generatePath, NavLink } from "react-router-dom";
33

4-
import { Modal, ModalBody, ModalHeader } from "@patternfly/react-core";
4+
import type { AxiosError } from "axios";
5+
6+
import {
7+
ButtonVariant,
8+
Modal,
9+
ModalBody,
10+
ModalHeader,
11+
} from "@patternfly/react-core";
512
import {
613
ActionsColumn,
714
Table,
@@ -12,30 +19,34 @@ import {
1219
Tr,
1320
} from "@patternfly/react-table";
1421

22+
import { joinKeyValueAsString } from "@app/api/model-utils.ts";
23+
import {
24+
type ExtendedSeverity,
25+
extendedSeverityFromSeverity,
26+
} from "@app/api/models";
1527
import type { AdvisorySummary } from "@app/client";
28+
import { ConfirmDialog } from "@app/components/ConfirmDialog.tsx";
29+
import { LabelsAsList } from "@app/components/LabelsAsList.tsx";
30+
import { NotificationsContext } from "@app/components/NotificationsContext.tsx";
1631
import { SimplePagination } from "@app/components/SimplePagination";
1732
import {
1833
ConditionalTableBody,
1934
TableHeaderContentWithControls,
2035
TableRowContentWithControls,
2136
} from "@app/components/TableControls";
22-
import { useDownload } from "@app/hooks/domain-controls/useDownload";
23-
24-
import {
25-
type ExtendedSeverity,
26-
extendedSeverityFromSeverity,
27-
} from "@app/api/models";
2837
import { VulnerabilityGallery } from "@app/components/VulnerabilityGallery";
29-
import { formatDate } from "@app/utils/utils";
30-
31-
import { joinKeyValueAsString } from "@app/api/model-utils";
32-
import { LabelsAsList } from "@app/components/LabelsAsList";
38+
import { useDownload } from "@app/hooks/domain-controls/useDownload";
39+
import { useDeleteAdvisoryMutation } from "@app/queries/advisories.ts";
3340
import { Paths } from "@app/Routes";
41+
import { formatDate } from "@app/utils/utils";
3442

3543
import { AdvisorySearchContext } from "./advisory-context";
3644
import { AdvisoryEditLabelsForm } from "./components/AdvisoryEditLabelsForm";
45+
import { advisoryDeleteDialogProps } from "@app/Constants";
3746

3847
export const AdvisoryTable: React.FC = () => {
48+
const { pushNotification } = React.useContext(NotificationsContext);
49+
3950
const { isFetching, fetchError, totalItemCount, tableControls } =
4051
React.useContext(AdvisorySearchContext);
4152

@@ -64,6 +75,29 @@ export const AdvisoryTable: React.FC = () => {
6475
setEditLabelsModalState(null);
6576
};
6677

78+
// Delete action
79+
80+
const [advisoryToDelete, setAdvisoryToDelete] =
81+
React.useState<AdvisorySummary | null>(null);
82+
83+
const onDeleteAdvisorySuccess = (advisory: AdvisorySummary) => {
84+
setAdvisoryToDelete(null);
85+
pushNotification({
86+
title: `The Advisory ${advisory.identifier} was deleted`,
87+
variant: "success",
88+
});
89+
};
90+
91+
const onDeleteAdvisoryError = (_error: AxiosError) => {
92+
pushNotification({
93+
title: "Error occurred while deleting the Advisory",
94+
variant: "danger",
95+
});
96+
};
97+
98+
const { mutate: deleteAdvisory, isPending: isDeletingAdvisory } =
99+
useDeleteAdvisoryMutation(onDeleteAdvisorySuccess, onDeleteAdvisoryError);
100+
67101
return (
68102
<>
69103
<Table {...tableProps} aria-label="advisory-table">
@@ -185,9 +219,6 @@ export const AdvisoryTable: React.FC = () => {
185219
setEditLabelsModalState(item);
186220
},
187221
},
188-
{
189-
isSeparator: true,
190-
},
191222
{
192223
title: "Download",
193224
onClick: () => {
@@ -197,6 +228,13 @@ export const AdvisoryTable: React.FC = () => {
197228
);
198229
},
199230
},
231+
{ isSeparator: true },
232+
{
233+
title: "Delete",
234+
onClick: () => {
235+
setAdvisoryToDelete(item);
236+
},
237+
},
200238
]}
201239
/>
202240
</Td>
@@ -228,6 +266,23 @@ export const AdvisoryTable: React.FC = () => {
228266
)}
229267
</ModalBody>
230268
</Modal>
269+
270+
<ConfirmDialog
271+
{...advisoryDeleteDialogProps(advisoryToDelete)}
272+
inProgress={isDeletingAdvisory}
273+
titleIconVariant="warning"
274+
isOpen={!!advisoryToDelete}
275+
confirmBtnVariant={ButtonVariant.danger}
276+
confirmBtnLabel="Delete"
277+
cancelBtnLabel="Cancel"
278+
onCancel={() => setAdvisoryToDelete(null)}
279+
onClose={() => setAdvisoryToDelete(null)}
280+
onConfirm={() => {
281+
if (advisoryToDelete) {
282+
deleteAdvisory(advisoryToDelete.uuid);
283+
}
284+
}}
285+
/>
231286
</>
232287
);
233288
};

0 commit comments

Comments
 (0)