From ab3fecb4d8148961d3d1acdf09757df8bacb130a Mon Sep 17 00:00:00 2001 From: Mason Hu Date: Tue, 18 Feb 2025 10:20:31 +0200 Subject: [PATCH] fix: misc fixes Signed-off-by: Mason Hu --- src/api/auth-groups.tsx | 4 +- src/api/auth-identities.tsx | 12 +- src/api/auth-idp-groups.tsx | 15 +-- src/components/SelectableMainTable.tsx | 18 +-- src/context/useIdentities.tsx | 2 +- src/pages/cluster/ClusterGroupForm.tsx | 2 +- src/pages/images/ImageList.tsx | 2 +- src/pages/instances/InstanceList.tsx | 4 +- src/pages/instances/InstanceSnapshots.tsx | 2 +- src/pages/permissions/PermissionGroups.tsx | 8 +- .../permissions/PermissionIdentities.tsx | 32 +++-- src/pages/permissions/PermissionIdpGroups.tsx | 55 +++++---- .../actions/BulkDeleteGroupsBtn.tsx | 3 +- .../actions/BulkDeleteIdentitiesBtn.tsx | 23 ++-- .../actions/BulkDeleteIdpGroupsBtn.tsx | 106 +++++++++------- .../permissions/actions/DeleteGroupModal.tsx | 60 ++++++---- .../permissions/actions/DeleteIdpGroupBtn.tsx | 63 +++++----- .../actions/DeleteIdpGroupsModal.tsx | 113 ------------------ .../actions/EditIdentityGroupsBtn.tsx | 10 +- .../panels/EditGroupIdentitiesPanel.tsx | 24 ++-- .../permissions/panels/EditGroupPanel.tsx | 4 +- .../panels/EditGroupPermissionsForm.tsx | 21 ++-- .../permissions/panels/EditIdentitiesForm.tsx | 16 +-- .../permissions/panels/GroupSelection.tsx | 2 +- src/pages/storage/StorageVolumeSnapshots.tsx | 2 +- src/sass/_selectable_main_table.scss | 2 +- src/util/entitlements/groups.tsx | 6 +- src/util/entitlements/identities.tsx | 6 +- src/util/entitlements/idp-groups.tsx | 12 +- src/util/permissionIdpGroups.tsx | 79 ++++++++++++ 30 files changed, 384 insertions(+), 324 deletions(-) delete mode 100644 src/pages/permissions/actions/DeleteIdpGroupsModal.tsx 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 ( + + ); + } + + 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 - ), + 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 ( + + ); + } + + 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) ? ( - - ) : ( - 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 ( - <> - - {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, + }; +};