diff --git a/src/api/auth-groups.tsx b/src/api/auth-groups.tsx
index d664f4c5cc..de0dacbbb8 100644
--- a/src/api/auth-groups.tsx
+++ b/src/api/auth-groups.tsx
@@ -3,12 +3,12 @@ import type { LxdApiResponse } from "types/apiResponse";
import type { LxdGroup } from "types/permissions";
import { withEntitlementsQuery } from "util/entitlements/api";
-export const groupEntitlements = ["can_edit", "can_delete"];
+export const groupEntitlements = ["can_delete", "can_edit"];
export const fetchGroups = (
isFineGrained: boolean | null,
): Promise => {
- const entitlements = `&${withEntitlementsQuery(isFineGrained, groupEntitlements)}`;
+ const entitlements = withEntitlementsQuery(isFineGrained, groupEntitlements);
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/groups?recursion=1${entitlements}`)
.then(handleResponse)
diff --git a/src/api/auth-identities.tsx b/src/api/auth-identities.tsx
index 4244f0e922..59dfbd3634 100644
--- a/src/api/auth-identities.tsx
+++ b/src/api/auth-identities.tsx
@@ -3,12 +3,15 @@ import type { LxdApiResponse } from "types/apiResponse";
import type { LxdIdentity, TlsIdentityTokenDetail } from "types/permissions";
import { withEntitlementsQuery } from "util/entitlements/api";
-export const identitiesEntitlements = ["can_edit", "can_delete"];
+export const identitiesEntitlements = ["can_delete", "can_edit"];
export const fetchIdentities = (
isFineGrained: boolean | null,
): Promise => {
- const entitlements = `&${withEntitlementsQuery(isFineGrained, identitiesEntitlements)}`;
+ const entitlements = withEntitlementsQuery(
+ isFineGrained,
+ identitiesEntitlements,
+ );
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identities?recursion=1${entitlements}`)
.then(handleResponse)
@@ -31,7 +34,10 @@ export const fetchIdentity = (
authMethod: string,
isFineGrained: boolean | null,
): Promise => {
- const entitlements = `&${withEntitlementsQuery(isFineGrained, identitiesEntitlements)}`;
+ const entitlements = withEntitlementsQuery(
+ isFineGrained,
+ identitiesEntitlements,
+ );
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identities/${authMethod}/${id}?recursion=1${entitlements}`)
.then(handleResponse)
diff --git a/src/api/auth-idp-groups.tsx b/src/api/auth-idp-groups.tsx
index a4b5934bca..7a76cd848e 100644
--- a/src/api/auth-idp-groups.tsx
+++ b/src/api/auth-idp-groups.tsx
@@ -3,12 +3,15 @@ import type { LxdApiResponse } from "types/apiResponse";
import type { IdpGroup } from "types/permissions";
import { withEntitlementsQuery } from "util/entitlements/api";
-const idpGroupEntitlements = ["can_edit", "can_delete"];
+const idpGroupEntitlements = ["can_delete", "can_edit"];
export const fetchIdpGroups = (
isFineGrained: boolean | null,
): Promise => {
- const entitlements = `&${withEntitlementsQuery(isFineGrained, idpGroupEntitlements)}`;
+ const entitlements = withEntitlementsQuery(
+ isFineGrained,
+ idpGroupEntitlements,
+ );
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identity-provider-groups?recursion=1${entitlements}`)
.then(handleResponse)
@@ -21,14 +24,8 @@ export const createIdpGroup = (group: Partial): Promise => {
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identity-provider-groups`, {
method: "POST",
- body: JSON.stringify({ name: group.name }),
+ body: JSON.stringify({ name: group.name, groups: group.groups }),
})
- .then(() =>
- fetch(`/1.0/auth/identity-provider-groups/${group.name}`, {
- method: "PUT",
- body: JSON.stringify(group),
- }),
- )
.then(handleResponse)
.then(resolve)
.catch(reject);
diff --git a/src/components/SelectableMainTable.tsx b/src/components/SelectableMainTable.tsx
index b8fdd2f42e..215574d428 100644
--- a/src/components/SelectableMainTable.tsx
+++ b/src/components/SelectableMainTable.tsx
@@ -19,14 +19,14 @@ interface SelectableMainTableProps {
parentName: string;
selectedNames: string[];
setSelectedNames: (val: string[], isUnselectAll?: boolean) => void;
- processingNames: string[];
+ disabledNames: string[];
rows: MainTableRow[];
indeterminateNames?: string[];
disableSelect?: boolean;
onToggleRow?: (rowName: string) => void;
hideContextualMenu?: boolean;
defaultSortKey?: string;
- disableHeaderCheckbox?: boolean;
+ disableSelectAll?: boolean;
}
type Props = SelectableMainTableProps & MainTableProps;
@@ -37,7 +37,7 @@ const SelectableMainTable: FC = ({
parentName,
selectedNames,
setSelectedNames,
- processingNames,
+ disabledNames,
rows,
headers,
indeterminateNames = [],
@@ -45,7 +45,7 @@ const SelectableMainTable: FC = ({
onToggleRow,
hideContextualMenu,
defaultSortKey,
- disableHeaderCheckbox,
+ disableSelectAll,
...props
}: Props) => {
const [currentSelectedIndex, setCurrentSelectedIndex] = useState();
@@ -93,7 +93,7 @@ const SelectableMainTable: FC = ({
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
onChange={isSomeSelected ? selectNone : selectPage}
- disabled={disableSelect || disableHeaderCheckbox}
+ disabled={disableSelect || disableSelectAll}
/>
{!hideContextualMenu && (
= ({
];
const selectedNamesLookup = new Set(selectedNames);
- const processingNamesLookup = new Set(processingNames);
+ const disabledNamesLookup = new Set(disabledNames);
const indeterminateNamesLookup = new Set(indeterminateNames);
const rowsWithCheckbox = rows.map((row, rowIndex) => {
const isRowSelected = selectedNamesLookup.has(row.name ?? "");
- const isRowProcessing = processingNamesLookup.has(row.name ?? "");
+ const isRowDisabled = disabledNamesLookup.has(row.name ?? "");
const isRowIndeterminate = indeterminateNamesLookup.has(row.name ?? "");
const toggleRow = (event: PointerEvent) => {
@@ -185,7 +185,7 @@ const SelectableMainTable: FC = ({
labelClassName="u-no-margin--bottom"
checked={isRowSelected}
onChange={toggleRow}
- disabled={isRowProcessing || !row.name || disableSelect}
+ disabled={isRowDisabled || !row.name || disableSelect}
indeterminate={isRowIndeterminate && !isRowSelected}
/>
),
@@ -197,7 +197,7 @@ const SelectableMainTable: FC = ({
const className = classnames(row.className, {
"selected-row": isRowSelected,
- "processing-row": isRowProcessing,
+ "disabled-row": isRowDisabled,
});
const key = row.key ?? row.name;
diff --git a/src/context/useIdentities.tsx b/src/context/useIdentities.tsx
index e523226a19..615756013d 100644
--- a/src/context/useIdentities.tsx
+++ b/src/context/useIdentities.tsx
@@ -21,7 +21,7 @@ export const useIdentity = (
): UseQueryResult => {
const { isFineGrained } = useAuth();
return useQuery({
- queryKey: [queryKeys.identities, id],
+ queryKey: [queryKeys.identities, authMethod, id],
queryFn: () => fetchIdentity(id, authMethod, isFineGrained),
enabled: (enabled ?? true) && isFineGrained !== null,
});
diff --git a/src/pages/cluster/ClusterGroupForm.tsx b/src/pages/cluster/ClusterGroupForm.tsx
index 595cda1760..cf52d243ae 100644
--- a/src/pages/cluster/ClusterGroupForm.tsx
+++ b/src/pages/cluster/ClusterGroupForm.tsx
@@ -170,7 +170,7 @@ const ClusterGroupForm: FC = ({ group }) => {
setSelectedNames={(newMembers: string[]) =>
void formik.setFieldValue("members", newMembers)
}
- processingNames={[]}
+ disabledNames={[]}
/>
diff --git a/src/pages/images/ImageList.tsx b/src/pages/images/ImageList.tsx
index dcfc204dd1..73b6e0c4dd 100644
--- a/src/pages/images/ImageList.tsx
+++ b/src/pages/images/ImageList.tsx
@@ -288,7 +288,7 @@ const ImageList: FC = () => {
itemName="image"
parentName="project"
filteredNames={filteredImages.map((item) => item.fingerprint)}
- processingNames={processingNames}
+ disabledNames={processingNames}
rows={[]}
disableSelect={!deletableImages.length}
/>
diff --git a/src/pages/instances/InstanceList.tsx b/src/pages/instances/InstanceList.tsx
index 7fcf01d86c..8f73764bb6 100644
--- a/src/pages/instances/InstanceList.tsx
+++ b/src/pages/instances/InstanceList.tsx
@@ -699,7 +699,7 @@ const InstanceList: FC = () => {
parentName="project"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
- processingNames={processingNames}
+ disabledNames={processingNames}
filteredNames={filteredInstances.map(
(instance) => instance.name,
)}
@@ -716,7 +716,7 @@ const InstanceList: FC = () => {
parentName="project"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
- processingNames={processingNames}
+ disabledNames={processingNames}
filteredNames={filteredInstances.map(
(instance) => instance.name,
)}
diff --git a/src/pages/instances/InstanceSnapshots.tsx b/src/pages/instances/InstanceSnapshots.tsx
index 13744285f7..6eba368e5f 100644
--- a/src/pages/instances/InstanceSnapshots.tsx
+++ b/src/pages/instances/InstanceSnapshots.tsx
@@ -278,7 +278,7 @@ const InstanceSnapshots = (props: Props) => {
parentName="instance"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
- processingNames={processingNames}
+ disabledNames={processingNames}
filteredNames={filteredSnapshots.map(
(snapshot) => snapshot.name,
)}
diff --git a/src/pages/permissions/PermissionGroups.tsx b/src/pages/permissions/PermissionGroups.tsx
index be37c60e1e..fa910b9ae3 100644
--- a/src/pages/permissions/PermissionGroups.tsx
+++ b/src/pages/permissions/PermissionGroups.tsx
@@ -209,7 +209,7 @@ const PermissionGroups: FC = () => {
parentName=""
selectedNames={selectedGroupNames}
setSelectedNames={setSelectedGroupNames}
- processingNames={[]}
+ disabledNames={[]}
filteredNames={filteredGroups.map((item) => item.name)}
disableSelect={!!panelParams.panel}
/>
@@ -275,14 +275,14 @@ const PermissionGroups: FC = () => {
)}
{selectedGroupNames.length > 0 && !panelParams.panel && (
<>
- setSelectedGroupNames([])}
/>
- setSelectedGroupNames([])}
/>
>
)}
diff --git a/src/pages/permissions/PermissionIdentities.tsx b/src/pages/permissions/PermissionIdentities.tsx
index a5b4d4a67e..09613e51c3 100644
--- a/src/pages/permissions/PermissionIdentities.tsx
+++ b/src/pages/permissions/PermissionIdentities.tsx
@@ -35,6 +35,7 @@ import IdentityResource from "components/IdentityResource";
import CreateTlsIdentityBtn from "./CreateTlsIdentityBtn";
import { useIdentities } from "context/useIdentities";
import { useIdentityEntitlements } from "util/entitlements/identities";
+import { pluralize } from "util/instanceBulkActions";
const PermissionIdentities: FC = () => {
const notify = useNotify();
@@ -113,6 +114,25 @@ const PermissionIdentities: FC = () => {
setSelectedIdentityIds([identity.id]);
};
+ const getGroupLink = () => {
+ if (canEditIdentity(identity)) {
+ return (
+
+ {identity.groups?.length || 0}
+
+ );
+ }
+
+ const groupsText = pluralize("group", identity.groups?.length ?? 0);
+ const groupsList = identity.groups?.join("\n- ");
+ const groupsTitle = `Assigned ${groupsText}:\n- ${groupsList}`;
+ return (
+
+ {identity.groups?.length || 0}
+
+ );
+ };
+
return {
key: identity.id,
name: isUnrestricted(identity) ? "" : identity.id,
@@ -149,13 +169,7 @@ const PermissionIdentities: FC = () => {
className: "u-truncate",
},
{
- content: canEditIdentity(identity) ? (
-
- {identity.groups?.length || 0}
-
- ) : (
- identity.groups?.length || 0
- ),
+ content: getGroupLink(),
role: "cell",
className: "u-align--right group-count",
"aria-label": "Groups for this identity",
@@ -265,7 +279,7 @@ const PermissionIdentities: FC = () => {
{!!selectedIdentityIds.length && hasAccessManagementTLS && (
)}
@@ -302,7 +316,7 @@ const PermissionIdentities: FC = () => {
parentName=""
selectedNames={selectedIdentityIds}
setSelectedNames={setSelectedIdentityIds}
- processingNames={[]}
+ disabledNames={[]}
filteredNames={fineGrainedIdentities.map(
(identity) => identity.id,
)}
diff --git a/src/pages/permissions/PermissionIdpGroups.tsx b/src/pages/permissions/PermissionIdpGroups.tsx
index 6036c36b1e..9caf4a156e 100644
--- a/src/pages/permissions/PermissionIdpGroups.tsx
+++ b/src/pages/permissions/PermissionIdpGroups.tsx
@@ -30,6 +30,7 @@ import { Link } from "react-router-dom";
import { useIdpGroups } from "context/useIdpGroups";
import { useServerEntitlements } from "util/entitlements/server";
import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
+import { pluralize } from "util/instanceBulkActions";
const PermissionIdpGroups: FC = () => {
const notify = useNotify();
@@ -41,7 +42,7 @@ const PermissionIdpGroups: FC = () => {
const { data: settings } = useSettings();
const hasCustomClaim = settings?.config?.["oidc.groups.claim"];
const { canCreateIdpGroups } = useServerEntitlements();
- const { canEditGroup } = useIdpGroupEntitlements();
+ const { canEditIdpGroup } = useIdpGroupEntitlements();
if (error) {
notify.failure("Loading provider groups failed", error);
@@ -81,6 +82,29 @@ const PermissionIdpGroups: FC = () => {
);
const rows = filteredGroups.map((idpGroup) => {
+ const getGroupLink = () => {
+ if (canEditIdpGroup(idpGroup)) {
+ return (
+ panelParams.openEditIdpGroup(idpGroup.name)}
+ >
+ {idpGroup.groups.length}
+
+ );
+ }
+
+ const groupsText = pluralize("group", idpGroup.groups?.length ?? 0);
+ const groupsList = idpGroup.groups?.join("\n- ");
+ const groupsTitle = `Assigned ${groupsText}:\n- ${groupsList}`;
+ return (
+
+ {idpGroup.groups?.length || 0}
+
+ );
+ };
+
return {
key: idpGroup.name,
name: idpGroup.name,
@@ -94,17 +118,7 @@ const PermissionIdpGroups: FC = () => {
title: idpGroup.name,
},
{
- content: canEditGroup(idpGroup) ? (
- panelParams.openEditIdpGroup(idpGroup.name)}
- >
- {idpGroup.groups.length}
-
- ) : (
- idpGroup.groups.length
- ),
+ content: getGroupLink(),
role: "cell",
className: "u-align--right",
"aria-label": "Number of mapped groups",
@@ -125,11 +139,11 @@ const PermissionIdpGroups: FC = () => {
type="button"
aria-label="Edit IDP group details"
title={
- canEditGroup(idpGroup)
+ canEditIdpGroup(idpGroup)
? "Edit details"
- : "You do not have permission to edit this IDP group"
+ : "You do not have permission to modify this IDP group"
}
- disabled={!canEditGroup(idpGroup)}
+ disabled={!canEditIdpGroup(idpGroup)}
>
,
@@ -232,7 +246,7 @@ const PermissionIdpGroups: FC = () => {
parentName=""
selectedNames={selectedGroupNames}
setSelectedNames={setSelectedGroupNames}
- processingNames={[]}
+ disabledNames={[]}
filteredNames={filteredGroups.map((item) => item.name)}
disableSelect={!!panelParams.panel}
/>
@@ -277,7 +291,7 @@ const PermissionIdpGroups: FC = () => {
Identity provider groups
- {!selectedGroupNames.length && hasGroups && (
+ {!selectedGroupNames.length && hasGroups ? (
{
disabled={!!panelParams.idpGroup}
/>
- )}
+ ) : null}
{selectedGroupNames.length > 0 && !panelParams.panel && (
<>
-
+
>
)}
diff --git a/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx b/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx
index fa7b16f1ce..7bee3a6863 100644
--- a/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx
@@ -36,11 +36,10 @@ const BulkDeleteGroupsBtn: FC = ({ groups, className, onDelete }) => {
: `You do not have permission to delete the selected ${pluralize("group", groups.length)}`
}
className={className}
- appearance="negative"
hasIcon
disabled={!deletableGroups.length}
>
-
+
{`Delete ${groups.length} ${pluralize("group", groups.length)}`}
{confirming && (
diff --git a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
index eb57c22a18..1b41eae16f 100644
--- a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
@@ -2,6 +2,7 @@ import { FC, useState } from "react";
import {
ButtonProps,
ConfirmationButton,
+ Icon,
useNotify,
} from "@canonical/react-components";
import type { LxdIdentity } from "types/permissions";
@@ -67,7 +68,6 @@ const BulkDeleteIdentitiesBtn: FC = ({
? buttonText
: `You do not have permission to delete the selected ${pluralize("identity", identities.length)}`
}
- appearance=""
aria-label="Delete identities"
className={className}
loading={isLoading}
@@ -82,13 +82,17 @@ const BulkDeleteIdentitiesBtn: FC = ({
{identity.name}
))}
- You do not have permission to delete the following{" "}
- {pluralize("identity", deletableIdentities.length)}:
-
- {restrictedIdentities.map((identity) => (
- {identity.name}
- ))}
-
+ {restrictedIdentities.length ? (
+ <>
+ You do not have permission to delete the following{" "}
+ {pluralize("identity", restrictedIdentities.length)}:
+
+ {restrictedIdentities.map((identity) => (
+ {identity.name}
+ ))}
+
+ >
+ ) : null}
This action cannot be undone, and can result in data loss.
),
@@ -99,7 +103,8 @@ const BulkDeleteIdentitiesBtn: FC = ({
shiftClickEnabled
showShiftClickHint
>
- {buttonText}
+
+ {buttonText}
);
};
diff --git a/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx b/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx
index 053c511bb1..d48624951f 100644
--- a/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx
@@ -1,53 +1,79 @@
-import { FC, useState } from "react";
-import { Button, Icon } from "@canonical/react-components";
+import { FC } from "react";
+import {
+ ConfirmationButton,
+ Icon,
+ Notification,
+} from "@canonical/react-components";
import type { IdpGroup } from "types/permissions";
import { pluralize } from "util/instanceBulkActions";
-import DeleteIdpGroupsModal from "./DeleteIdpGroupsModal";
-import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
+import { useDeleteIdpGroups } from "util/permissionIdpGroups";
interface Props {
idpGroups: IdpGroup[];
- className?: string;
}
-const BulkDeleteIdpGroupsBtn: FC = ({ idpGroups, className }) => {
- const [confirming, setConfirming] = useState(false);
- const { canDeleteGroup } = useIdpGroupEntitlements();
- const deletableGroups = idpGroups.filter(canDeleteGroup);
+const BulkDeleteIdpGroupsBtn: FC = ({ idpGroups }) => {
+ const {
+ submitting,
+ deletableGroups,
+ restrictedGroups,
+ handleDeleteIdpGroups,
+ } = useDeleteIdpGroups({ idpGroups });
- const handleConfirmDelete = () => {
- setConfirming(true);
- };
-
- const handleCloseConfirm = () => {
- setConfirming(false);
- };
+ const hasOneGroup = deletableGroups.length === 1;
return (
- <>
-
-
- {`Delete ${idpGroups.length} ${pluralize("IDP group", idpGroups.length)}`}
-
- {confirming && (
-
- )}
- >
+
+
+ Are you sure you want to delete{" "}
+
+ {hasOneGroup
+ ? `"${deletableGroups[0].name}"`
+ : `${deletableGroups.length} IDP groups`}
+
+ ? This action is permanent and can not be undone.
+
+ {restrictedGroups.length ? (
+
+
+ You do not have permission to delete the following IDP groups:
+
+
+ {restrictedGroups.map((group) => (
+ {group.name}
+ ))}
+
+
+ ) : null}
+ >
+ ),
+ }}
+ >
+
+ {`Delete ${idpGroups.length} ${pluralize("IDP group", idpGroups.length)}`}
+
);
};
diff --git a/src/pages/permissions/actions/DeleteGroupModal.tsx b/src/pages/permissions/actions/DeleteGroupModal.tsx
index 470c90c7bd..e03ee31975 100644
--- a/src/pages/permissions/actions/DeleteGroupModal.tsx
+++ b/src/pages/permissions/actions/DeleteGroupModal.tsx
@@ -6,7 +6,7 @@ import {
useNotify,
} from "@canonical/react-components";
import { useQueryClient } from "@tanstack/react-query";
-import { deleteGroup, deleteGroups } from "api/auth-groups";
+import { deleteGroups } from "api/auth-groups";
import ResourceLabel from "components/ResourceLabel";
import { useToastNotification } from "context/toastNotificationProvider";
import { ChangeEvent, FC, useState } from "react";
@@ -14,6 +14,8 @@ import type { LxdGroup } from "types/permissions";
import { useGroupEntitlements } from "util/entitlements/groups";
import { pluralize } from "util/instanceBulkActions";
import { queryKeys } from "util/queryKeys";
+import LoggedInUserNotification from "../panels/LoggedInUserNotification";
+import { useSettings } from "context/useSettings";
interface Props {
groups: LxdGroup[];
@@ -29,15 +31,26 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
const [submitting, setSubmitting] = useState(false);
const confirmText = "confirm-delete-group";
const { canDeleteGroup } = useGroupEntitlements();
+ const { data: settings } = useSettings();
+ const loggedInIdentityID = settings?.auth_user_name ?? "";
const restrictedGroups: LxdGroup[] = [];
const deletableGroups: LxdGroup[] = [];
+ let hasGroupsForLoggedInUser = false;
groups.forEach((group) => {
if (canDeleteGroup(group)) {
deletableGroups.push(group);
} else {
restrictedGroups.push(group);
}
+
+ if (group.identities?.oidc?.includes(loggedInIdentityID)) {
+ hasGroupsForLoggedInUser = true;
+ }
+
+ if (group.identities?.tls?.includes(loggedInIdentityID)) {
+ hasGroupsForLoggedInUser = true;
+ }
});
const hasOneGroup = deletableGroups.length === 1;
@@ -55,9 +68,6 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
const handleDeleteGroups = () => {
setSubmitting(true);
const hasSingleGroup = deletableGroups.length === 1;
- const mutationPromise = hasSingleGroup
- ? deleteGroup(deletableGroups[0].name)
- : deleteGroups(deletableGroups.map((group) => group.name));
const successMessage = hasSingleGroup ? (
<>
@@ -69,7 +79,7 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
`${deletableGroups.length} groups deleted.`
);
- mutationPromise
+ deleteGroups(deletableGroups.map((group) => group.name))
.then(() => {
void queryClient.invalidateQueries({
predicate: (query) => {
@@ -83,7 +93,7 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
})
.catch((e) => {
notify.failure(
- `${pluralize("group", deletableGroups.length)} deletion failed`,
+ `Failed deleting ${deletableGroups.length} ${pluralize("group", deletableGroups.length)}.`,
e,
);
})
@@ -104,22 +114,30 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
?
- {restrictedGroups.length && (
-
-
- You do not have permission to delete the following groups:
-
-
- {restrictedGroups.map((group) => (
- {group.name}
- ))}
-
-
+ {hasGroupsForLoggedInUser && (
+
+
+
)}
+ {restrictedGroups.length ? (
+
+
+
+ You do not have permission to delete the following groups:
+
+
+ {restrictedGroups.map((group) => (
+ {group.name}
+ ))}
+
+
+
+ ) : null}
This action cannot be undone and may result in users losing access to
LXD, including the possibility that all users lose admin access.
diff --git a/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx b/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx
index a7066923bd..4fcc8456be 100644
--- a/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx
+++ b/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx
@@ -1,42 +1,49 @@
import { FC } from "react";
-import { Button, Icon } from "@canonical/react-components";
+import { ConfirmationButton, Icon } from "@canonical/react-components";
import type { IdpGroup } from "types/permissions";
-import DeleteIdpGroupsModal from "./DeleteIdpGroupsModal";
-import { usePortal } from "@canonical/react-components";
import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
+import { useDeleteIdpGroups } from "util/permissionIdpGroups";
interface Props {
idpGroup: IdpGroup;
}
const DeleteIdpGroupBtn: FC = ({ idpGroup }) => {
- const { openPortal, closePortal, isOpen, Portal } = usePortal();
- const { canDeleteGroup } = useIdpGroupEntitlements();
+ const { canDeleteIdpGroup } = useIdpGroupEntitlements();
+ const { submitting, deletableGroups, handleDeleteIdpGroups } =
+ useDeleteIdpGroups({ idpGroups: [idpGroup] });
return (
- <>
-
-
-
- {isOpen && (
-
-
-
- )}
- >
+
+ Are you sure you want to delete{" "}
+ {deletableGroups[0]?.name} ? This action is
+ permanent and can not be undone.
+
+ ),
+ }}
+ >
+
+
);
};
diff --git a/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx b/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
deleted file mode 100644
index d5dba19756..0000000000
--- a/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import {
- ConfirmationModal,
- Notification,
- useNotify,
-} from "@canonical/react-components";
-import { useQueryClient } from "@tanstack/react-query";
-import { deleteIdpGroup, deleteIdpGroups } from "api/auth-idp-groups";
-import ResourceLabel from "components/ResourceLabel";
-import { useToastNotification } from "context/toastNotificationProvider";
-import { FC, useState } from "react";
-import type { IdpGroup } from "types/permissions";
-import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
-import { pluralize } from "util/instanceBulkActions";
-import { queryKeys } from "util/queryKeys";
-
-interface Props {
- idpGroups: IdpGroup[];
- close: () => void;
-}
-
-const DeleteIdpGroupsModal: FC = ({ idpGroups, close }) => {
- const queryClient = useQueryClient();
- const notify = useNotify();
- const toastNotify = useToastNotification();
- const [submitting, setSubmitting] = useState(false);
- const { canDeleteGroup } = useIdpGroupEntitlements();
-
- const restrictedGroups: IdpGroup[] = [];
- const deletableGroups: IdpGroup[] = [];
- idpGroups.forEach((group) => {
- if (canDeleteGroup(group)) {
- deletableGroups.push(group);
- } else {
- restrictedGroups.push(group);
- }
- });
-
- const hasOneGroup = deletableGroups.length === 1;
-
- const handleDeleteIdpGroups = () => {
- setSubmitting(true);
- const mutationPromise = hasOneGroup
- ? deleteIdpGroup(deletableGroups[0].name)
- : deleteIdpGroups(deletableGroups.map((group) => group.name));
-
- const successMessage = hasOneGroup ? (
- <>
- IDP group{" "}
- {" "}
- deleted.
- >
- ) : (
- `${deletableGroups.length} IDP groups deleted.`
- );
-
- mutationPromise
- .then(() => {
- void queryClient.invalidateQueries({
- queryKey: [queryKeys.idpGroups],
- });
- toastNotify.success(successMessage);
- close();
- })
- .catch((e) => {
- notify.failure(
- `${pluralize("IDP group", deletableGroups.length)} deletion failed`,
- e,
- );
- })
- .finally(() => {
- setSubmitting(false);
- });
- };
-
- return (
-
-
- Are you sure you want to delete{" "}
-
- {hasOneGroup
- ? `"${deletableGroups[0].name}"`
- : `${deletableGroups.length} IDP groups`}
-
- ? This action is permanent and can not be undone.
-
- {restrictedGroups.length && (
-
-
- You do not have permission to delete the following IDP groups:
-
-
- {restrictedGroups.map((group) => (
- {group.name}
- ))}
-
-
- )}
-
- );
-};
-
-export default DeleteIdpGroupsModal;
diff --git a/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx b/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx
index 10e3da0e62..122861a2ca 100644
--- a/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx
+++ b/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx
@@ -1,5 +1,5 @@
import { FC } from "react";
-import { Button, ButtonProps } from "@canonical/react-components";
+import { Button, ButtonProps, Icon } from "@canonical/react-components";
import type { LxdIdentity } from "types/permissions";
import usePanelParams from "util/usePanelParams";
import { useIdentityEntitlements } from "util/entitlements/identities";
@@ -27,10 +27,10 @@ const EditIdentityGroupsBtn: FC = ({
);
const getRestrictedWarning = () => {
- const test = restrictedIdentities
+ const restrictedList = restrictedIdentities
.map((identity) => `\n- ${identity.name}`)
.join("");
- return `You do not have permission to modify ${restrictedIdentities.length > 1 ? "some of the selected" : "the selected"} ${pluralize("identity", restrictedIdentities.length)}:${test}`;
+ return `You do not have permission to modify ${restrictedIdentities.length > 1 ? "some of the selected" : "the selected"} ${pluralize("identity", restrictedIdentities.length)}:${restrictedList}`;
};
return (
@@ -47,9 +47,11 @@ const EditIdentityGroupsBtn: FC = ({
!identities.length ||
!!panelParams.panel
}
+ hasIcon
{...buttonProps}
>
- {buttonText}
+
+ {buttonText}
>
);
diff --git a/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx b/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
index d5caee4f11..808d73d2fe 100644
--- a/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
+++ b/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
@@ -229,7 +229,7 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
"aria-label": "Identity",
title: canEditIdentity(identity)
? identity.name
- : "You do not have permission to allocate this identity to groups",
+ : "You do not have permission to manage this identity",
},
{
content: modifiedIdentities.has(identity.id) && (
@@ -252,15 +252,6 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
defaultSort: "name",
});
- const confirmButtonText = modifiedIdentities.size
- ? `Apply ${modifiedIdentities.size} identity ${pluralize("change", modifiedIdentities.size)}`
- : "Modify identities";
-
- const panelTitle =
- groups.length > 1
- ? `Change identities for ${groups.length} groups`
- : `Change identities for ${groups[0]?.name}`;
-
const content = (
= ({ groups }) => {
parentName="server"
selectedNames={Array.from(selectedIdentities)}
setSelectedNames={modifyIdentities}
- processingNames={restrictedIdentities.map((identity) => identity.id)}
+ disabledNames={restrictedIdentities.map((identity) => identity.id)}
filteredNames={fineGrainedIdentities.map((identity) => identity.id)}
indeterminateNames={Array.from(indeterminateIdentities)}
onToggleRow={toggleRow}
hideContextualMenu
- disableHeaderCheckbox={!!restrictedIdentities.length}
+ disableSelectAll={!!restrictedIdentities.length}
/>
);
+ const confirmButtonText = modifiedIdentities.size
+ ? `Apply ${modifiedIdentities.size} identity ${pluralize("change", modifiedIdentities.size)}`
+ : "Modify identities";
+
+ const panelTitle =
+ groups.length > 1
+ ? `Change identities for ${groups.length} groups`
+ : `Change identities for ${groups[0]?.name}`;
+
return (
<>
diff --git a/src/pages/permissions/panels/EditGroupPanel.tsx b/src/pages/permissions/panels/EditGroupPanel.tsx
index a05f0ea7ef..f4a92b7faa 100644
--- a/src/pages/permissions/panels/EditGroupPanel.tsx
+++ b/src/pages/permissions/panels/EditGroupPanel.tsx
@@ -167,7 +167,7 @@ const EditGroupPanel: FC = ({ group, onClose }) => {
permissions: permissions.filter((p) => !p.isRemoved),
};
- const getMutationPromise = () => {
+ const mutation = () => {
if (!canEditGroup(group)) {
return saveIdentities();
}
@@ -179,7 +179,7 @@ const EditGroupPanel: FC = ({ group, onClose }) => {
: updateGroup(groupPayload).then(saveIdentities);
};
- getMutationPromise()
+ mutation()
.then(() => {
closePanel();
toastNotify.success(
diff --git a/src/pages/permissions/panels/EditGroupPermissionsForm.tsx b/src/pages/permissions/panels/EditGroupPermissionsForm.tsx
index 1f18f20f2a..1c9201bd80 100644
--- a/src/pages/permissions/panels/EditGroupPermissionsForm.tsx
+++ b/src/pages/permissions/panels/EditGroupPermissionsForm.tsx
@@ -42,10 +42,17 @@ const EditGroupPermissionsForm: FC = ({
const [search, setSearch] = useState("");
const { canViewPermissions } = useServerEntitlements();
const { canEditGroup } = useGroupEntitlements();
- const permissionEditRestriction =
- !canViewPermissions() || !canEditGroup(group)
- ? "You do not have permission to edit entitlements for this group"
- : "";
+ const getEditRestriction = () => {
+ if (!canEditGroup(group)) {
+ return "You do not have permission to edit this group";
+ }
+
+ if (!canViewPermissions()) {
+ return "You are not allowed to view permissions";
+ }
+
+ return "";
+ };
const addPermission = (newPermission: FormPermission) => {
const permissionExists = permissions.find(
@@ -178,9 +185,9 @@ const EditGroupPermissionsForm: FC = ({
onClick={() => deletePermission(permission.id ?? "")}
type="button"
aria-label="Delete permission"
- title={permissionEditRestriction ?? "Delete permission"}
+ title={getEditRestriction() ?? "Delete permission"}
className="u-no-margin--right"
- disabled={!!permissionEditRestriction}
+ disabled={!!getEditRestriction()}
>
@@ -223,7 +230,7 @@ const EditGroupPermissionsForm: FC = ({
diff --git a/src/pages/permissions/panels/EditIdentitiesForm.tsx b/src/pages/permissions/panels/EditIdentitiesForm.tsx
index b59d0ac628..3e1799a488 100644
--- a/src/pages/permissions/panels/EditIdentitiesForm.tsx
+++ b/src/pages/permissions/panels/EditIdentitiesForm.tsx
@@ -29,9 +29,9 @@ const EditIdentitiesForm: FC = ({
const { data: identities = [], error } = useIdentities();
const { canEditIdentity } = useIdentityEntitlements();
- const restrictedIdentities = identities.filter(
- (identity) => !canEditIdentity(identity),
- );
+ const restrictedIdentityNames = identities
+ .filter((identity) => !canEditIdentity(identity))
+ .map((identity) => identity.id);
if (error) {
notify.failure("Loading details failed", error);
@@ -42,6 +42,10 @@ const EditIdentitiesForm: FC = ({
);
const toggleRow = (id: string) => {
+ if (restrictedIdentityNames.includes(id)) {
+ return;
+ }
+
const existing = selected.find((identity) => identity.id === id);
if (existing) {
const filtered = selected.filter((identity) => identity.id !== id);
@@ -171,14 +175,12 @@ const EditIdentitiesForm: FC = ({
.filter((id) => !id.isRemoved)
.map((identity) => identity.id)}
setSelectedNames={bulkSelect}
- processingNames={restrictedIdentities.map(
- (identity) => identity.name,
- )}
+ disabledNames={restrictedIdentityNames}
filteredNames={fineGrainedIdentities.map((identity) => identity.id)}
indeterminateNames={[]}
onToggleRow={toggleRow}
hideContextualMenu
- disableHeaderCheckbox={!!restrictedIdentities.length}
+ disableSelectAll={!!restrictedIdentityNames.length}
/>
>
diff --git a/src/pages/permissions/panels/GroupSelection.tsx b/src/pages/permissions/panels/GroupSelection.tsx
index 822f21c54c..dabb2c48b7 100644
--- a/src/pages/permissions/panels/GroupSelection.tsx
+++ b/src/pages/permissions/panels/GroupSelection.tsx
@@ -145,7 +145,7 @@ const GroupSelection: FC = ({
parentName=""
selectedNames={Array.from(selectedGroups)}
setSelectedNames={setSelectedGroups}
- processingNames={[]}
+ disabledNames={[]}
filteredNames={groups.map((group) => group.name)}
indeterminateNames={Array.from(indeterminateGroups ?? new Set())}
onToggleRow={toggleGroup}
diff --git a/src/pages/storage/StorageVolumeSnapshots.tsx b/src/pages/storage/StorageVolumeSnapshots.tsx
index 5fbd64a09e..a1e769e6f7 100644
--- a/src/pages/storage/StorageVolumeSnapshots.tsx
+++ b/src/pages/storage/StorageVolumeSnapshots.tsx
@@ -276,7 +276,7 @@ const StorageVolumeSnapshots: FC = ({ volume }) => {
parentName="instance"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
- processingNames={processingNames}
+ disabledNames={processingNames}
filteredNames={filteredSnapshots.map(
(snapshot) => snapshot.name,
)}
diff --git a/src/sass/_selectable_main_table.scss b/src/sass/_selectable_main_table.scss
index 8472068236..f80a8eb8ae 100644
--- a/src/sass/_selectable_main_table.scss
+++ b/src/sass/_selectable_main_table.scss
@@ -38,7 +38,7 @@
}
}
-.processing-row {
+.disabled-row {
opacity: 0.5;
.actions > * {
diff --git a/src/util/entitlements/groups.tsx b/src/util/entitlements/groups.tsx
index 1556ddf873..46f7d5e5be 100644
--- a/src/util/entitlements/groups.tsx
+++ b/src/util/entitlements/groups.tsx
@@ -5,12 +5,12 @@ import { LxdGroup } from "types/permissions";
export const useGroupEntitlements = () => {
const { isFineGrained } = useAuth();
- const canEditGroup = (group?: LxdGroup) =>
- hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
-
const canDeleteGroup = (group?: LxdGroup) =>
hasEntitlement(isFineGrained, "can_delete", group?.access_entitlements);
+ const canEditGroup = (group?: LxdGroup) =>
+ hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
+
return {
canDeleteGroup,
canEditGroup,
diff --git a/src/util/entitlements/identities.tsx b/src/util/entitlements/identities.tsx
index a7394ab4f3..56eaa4b1d6 100644
--- a/src/util/entitlements/identities.tsx
+++ b/src/util/entitlements/identities.tsx
@@ -5,12 +5,12 @@ import { LxdIdentity } from "types/permissions";
export const useIdentityEntitlements = () => {
const { isFineGrained } = useAuth();
- const canEditIdentity = (identity?: LxdIdentity) =>
- hasEntitlement(isFineGrained, "can_edit", identity?.access_entitlements);
-
const canDeleteIdentity = (identity?: LxdIdentity) =>
hasEntitlement(isFineGrained, "can_delete", identity?.access_entitlements);
+ const canEditIdentity = (identity?: LxdIdentity) =>
+ hasEntitlement(isFineGrained, "can_edit", identity?.access_entitlements);
+
return {
canDeleteIdentity,
canEditIdentity,
diff --git a/src/util/entitlements/idp-groups.tsx b/src/util/entitlements/idp-groups.tsx
index 0a637ffb2d..a55d5e614a 100644
--- a/src/util/entitlements/idp-groups.tsx
+++ b/src/util/entitlements/idp-groups.tsx
@@ -5,14 +5,14 @@ import { IdpGroup } from "types/permissions";
export const useIdpGroupEntitlements = () => {
const { isFineGrained } = useAuth();
- const canEditGroup = (group?: IdpGroup) =>
- hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
-
- const canDeleteGroup = (group?: IdpGroup) =>
+ const canDeleteIdpGroup = (group?: IdpGroup) =>
hasEntitlement(isFineGrained, "can_delete", group?.access_entitlements);
+ const canEditIdpGroup = (group?: IdpGroup) =>
+ hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
+
return {
- canDeleteGroup,
- canEditGroup,
+ canDeleteIdpGroup,
+ canEditIdpGroup,
};
};
diff --git a/src/util/permissionIdpGroups.tsx b/src/util/permissionIdpGroups.tsx
index f6ead0f851..7109b7e50e 100644
--- a/src/util/permissionIdpGroups.tsx
+++ b/src/util/permissionIdpGroups.tsx
@@ -1,5 +1,15 @@
import { AbortControllerState, checkDuplicateName } from "./helpers";
import * as Yup from "yup";
+import { deleteIdpGroups } from "api/auth-idp-groups";
+import { useNotify } from "@canonical/react-components";
+import { useState } from "react";
+import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
+import ResourceLabel from "components/ResourceLabel";
+import { pluralize } from "util/instanceBulkActions";
+import { useQueryClient } from "@tanstack/react-query";
+import { useToastNotification } from "context/toastNotificationProvider";
+import { IdpGroup } from "types/permissions";
+import { queryKeys } from "./queryKeys";
export const testDuplicateIdpGroupName = (
controllerState: AbortControllerState,
@@ -21,3 +31,72 @@ export const testDuplicateIdpGroupName = (
},
];
};
+
+type DeleteIdpGroupsProps = {
+ idpGroups: IdpGroup[];
+ onSuccess?: () => void;
+};
+
+export const useDeleteIdpGroups = ({
+ idpGroups,
+ onSuccess,
+}: DeleteIdpGroupsProps) => {
+ const queryClient = useQueryClient();
+ const notify = useNotify();
+ const toastNotify = useToastNotification();
+ const [submitting, setSubmitting] = useState(false);
+ const { canDeleteIdpGroup } = useIdpGroupEntitlements();
+
+ const restrictedGroups: IdpGroup[] = [];
+ const deletableGroups: IdpGroup[] = [];
+ idpGroups.forEach((group) => {
+ if (canDeleteIdpGroup(group)) {
+ deletableGroups.push(group);
+ } else {
+ restrictedGroups.push(group);
+ }
+ });
+
+ const hasOneGroup = deletableGroups.length === 1;
+
+ const handleDeleteIdpGroups = () => {
+ setSubmitting(true);
+
+ const successMessage = hasOneGroup ? (
+ <>
+ IDP group{" "}
+ {" "}
+ deleted.
+ >
+ ) : (
+ `${deletableGroups.length} IDP groups deleted.`
+ );
+
+ deleteIdpGroups(deletableGroups.map((group) => group.name))
+ .then(() => {
+ void queryClient.invalidateQueries({
+ queryKey: [queryKeys.idpGroups],
+ });
+ toastNotify.success(successMessage);
+ if (onSuccess) {
+ onSuccess();
+ }
+ })
+ .catch((e) => {
+ notify.failure(
+ `${pluralize("IDP group", deletableGroups.length)} deletion failed`,
+ e,
+ );
+ })
+ .finally(() => {
+ setSubmitting(false);
+ });
+ };
+
+ return {
+ handleDeleteIdpGroups,
+ submitting,
+ restrictedGroups,
+ deletableGroups,
+ };
+};