From c5fc9f1299486793dc829cb8a7cbf49999b2e9bd Mon Sep 17 00:00:00 2001 From: Nkeiruka Date: Fri, 25 Oct 2024 18:13:44 +0100 Subject: [PATCH] feat: [WD-15753] Delete OIDC User Signed-off-by: Nkeiruka --- src/api/auth-identities.tsx | 24 ++++++ src/context/useSupportedFeatures.tsx | 1 + .../permissions/PermissionIdentities.tsx | 59 ++++++++++---- .../actions/BulkDeleteIdentitiesBtn.tsx | 78 +++++++++++++++++++ .../permissions/actions/DeleteIdentityBtn.tsx | 77 ++++++++++++++++++ tests/permission-identities.spec.ts | 1 + 6 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx create mode 100644 src/pages/permissions/actions/DeleteIdentityBtn.tsx diff --git a/src/api/auth-identities.tsx b/src/api/auth-identities.tsx index d0501ea819..3ba6b52c6a 100644 --- a/src/api/auth-identities.tsx +++ b/src/api/auth-identities.tsx @@ -50,3 +50,27 @@ export const updateIdentities = ( .catch(reject); }); }; + +export const deleteOIDCIdentity = (identity: LxdIdentity) => { + return new Promise((resolve, reject) => { + fetch(`/1.0/auth/identities/oidc/${identity.id}`, { + method: "DELETE", + }) + .then(handleResponse) + .then(resolve) + .catch(reject); + }); +}; + +export const deleteOIDCIdentities = ( + identities: LxdIdentity[], +): Promise => { + return new Promise((resolve, reject) => { + void Promise.allSettled( + identities.map((identity) => deleteOIDCIdentity(identity)), + ) + .then(handleSettledResult) + .then(resolve) + .catch(reject); + }); +}; diff --git a/src/context/useSupportedFeatures.tsx b/src/context/useSupportedFeatures.tsx index 49b262afed..0245f74a2f 100644 --- a/src/context/useSupportedFeatures.tsx +++ b/src/context/useSupportedFeatures.tsx @@ -24,6 +24,7 @@ export const useSupportedFeatures = () => { (!!serverVersion && serverMajor >= 5 && serverMinor >= 20) || serverMajor > 5, hasAccessManagement: apiExtensions.has("access_management"), + hasAccessManagementTLS: apiExtensions.has("access_management_tls"), hasExplicitTrustToken: apiExtensions.has("explicit_trust_token"), hasInstanceCreateStart: apiExtensions.has("instance_create_start"), hasInstanceImportConversion: apiExtensions.has( diff --git a/src/pages/permissions/PermissionIdentities.tsx b/src/pages/permissions/PermissionIdentities.tsx index 19eec377e4..b07858ea8f 100644 --- a/src/pages/permissions/PermissionIdentities.tsx +++ b/src/pages/permissions/PermissionIdentities.tsx @@ -11,7 +11,7 @@ import Loader from "components/Loader"; import ScrollableTable from "components/ScrollableTable"; import SelectableMainTable from "components/SelectableMainTable"; import SelectedTableNotification from "components/SelectedTableNotification"; -import { FC, useState } from "react"; +import { FC, useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { queryKeys } from "util/queryKeys"; import useSortTableData from "util/useSortTableData"; @@ -30,6 +30,9 @@ import HelpLink from "components/HelpLink"; import { useDocs } from "context/useDocs"; import EditIdentityGroupsPanel from "./panels/EditIdentityGroupsPanel"; import Tag from "components/Tag"; +import BulkDeleteIdentitiesBtn from "./actions/BulkDeleteIdentitiesBtn"; +import DeleteIdentityBtn from "./actions/DeleteIdentityBtn"; +import { useSupportedFeatures } from "context/useSupportedFeatures"; const PermissionIdentities: FC = () => { const notify = useNotify(); @@ -46,6 +49,21 @@ const PermissionIdentities: FC = () => { const panelParams = usePanelParams(); const [searchParams] = useSearchParams(); const [selectedIdentityIds, setSelectedIdentityIds] = useState([]); + const { hasAccessManagementTLS } = useSupportedFeatures(); + + useEffect(() => { + const validIdentities = new Set( + identities.map((identity) => identity.name), + ); + + const validSelections = selectedIdentityIds.filter((identity) => + validIdentities.has(identity), + ); + + if (validSelections.length !== selectedIdentityIds.length) { + setSelectedIdentityIds(validSelections); + } + }, [identities]); if (error) { notify.failure("Loading identities failed", error); @@ -140,20 +158,25 @@ const PermissionIdentities: FC = () => { }, { content: !isTlsIdentity && ( - + <> + + {hasAccessManagementTLS && ( + + )} + ), className: "actions u-align--right", role: "cell", @@ -234,6 +257,12 @@ const PermissionIdentities: FC = () => { className="u-no-margin--bottom" /> )} + {!!selectedIdentityIds.length && hasAccessManagementTLS && ( + + )} } diff --git a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx new file mode 100644 index 0000000000..551963d819 --- /dev/null +++ b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx @@ -0,0 +1,78 @@ +import { FC } from "react"; +import { + ButtonProps, + ConfirmationButton, + useNotify, +} from "@canonical/react-components"; +import { LxdIdentity } from "types/permissions"; +import { deleteOIDCIdentities } from "api/auth-identities"; +import { useQueryClient } from "@tanstack/react-query"; +import { useToastNotification } from "context/toastNotificationProvider"; +import { queryKeys } from "util/queryKeys"; +import { pluralize } from "util/instanceBulkActions"; + +interface Props { + identities: LxdIdentity[]; + className?: string; +} + +const BulkDeleteIdentitiesBtn: FC = ({ + identities, + className, +}) => { + const queryClient = useQueryClient(); + const notify = useNotify(); + const toastNotify = useToastNotification(); + const buttonText = `Delete ${pluralize("identity", identities.length)}`; + const successMessage = `${identities.length} ${pluralize("identity", identities.length)} successfully deleted`; + + const handleDelete = () => { + deleteOIDCIdentities(identities) + .then(() => { + void queryClient.invalidateQueries({ + predicate: (query) => { + return [queryKeys.identities, queryKeys.authGroups].includes( + query.queryKey[0] as string, + ); + }, + }); + toastNotify.success(successMessage); + close(); + }) + .catch((e) => { + notify.failure(`Identity deletion failed`, e); + }); + }; + + return ( + + This will permanently delete the following identities: +
    + {identities.map((identity) => ( +
  • {identity.name}
  • + ))} +
+ This action cannot be undone, and can result in data loss. +

+ ), + confirmButtonLabel: "Delete", + onConfirm: handleDelete, + }} + disabled={!identities.length} + shiftClickEnabled + showShiftClickHint + > + {buttonText} +
+ ); +}; + +export default BulkDeleteIdentitiesBtn; diff --git a/src/pages/permissions/actions/DeleteIdentityBtn.tsx b/src/pages/permissions/actions/DeleteIdentityBtn.tsx new file mode 100644 index 0000000000..e46de90309 --- /dev/null +++ b/src/pages/permissions/actions/DeleteIdentityBtn.tsx @@ -0,0 +1,77 @@ +import { FC } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "util/queryKeys"; +import { + ConfirmationButton, + Icon, + useNotify, +} from "@canonical/react-components"; +import { useToastNotification } from "context/toastNotificationProvider"; +import { LxdIdentity } from "types/permissions"; +import ItemName from "components/ItemName"; +import { deleteOIDCIdentity } from "api/auth-identities"; +import ResourceLabel from "components/ResourceLabel"; + +interface Props { + identity: LxdIdentity; +} + +const DeleteIdentityBtn: FC = ({ identity }) => { + const queryClient = useQueryClient(); + const notify = useNotify(); + const toastNotify = useToastNotification(); + + const handleDelete = () => { + deleteOIDCIdentity(identity) + .then(() => { + void queryClient.invalidateQueries({ + predicate: (query) => { + return [queryKeys.identities, queryKeys.authGroups].includes( + query.queryKey[0] as string, + ); + }, + }); + toastNotify.success( + <> + Identity {" "} + deleted. + , + ); + close(); + }) + .catch((e) => { + notify.failure( + `Identity deletion failed`, + e, + , + ); + }); + }; + + return ( + + This will permanently delete . +
+ This action cannot be undone, and can result in data loss. +

+ ), + confirmButtonLabel: "Delete", + onConfirm: handleDelete, + }} + shiftClickEnabled + showShiftClickHint + > + +
+ ); +}; + +export default DeleteIdentityBtn; diff --git a/tests/permission-identities.spec.ts b/tests/permission-identities.spec.ts index f288b5dd91..2a90133713 100644 --- a/tests/permission-identities.spec.ts +++ b/tests/permission-identities.spec.ts @@ -92,6 +92,7 @@ test("manage groups for many identities", async ({ page, lxdVersion }) => { ); await page.getByRole("button", { name: "Confirm changes" }).click(); await page.waitForSelector(`text=Updated groups for 2 identities`); + await selectIdentitiesToModify(page, [identityFoo, identityBar]); await page.getByLabel("Modify groups").click(); await toggleGroupsForIdentities(page, [groupOne, groupTwo]); await assertTextVisible(page, "2 groups will be modified");