diff --git a/src/components/alert/AlertPanel.tsx b/src/components/alert/AlertPanel.tsx index b84fa95331..2caf3c788c 100644 --- a/src/components/alert/AlertPanel.tsx +++ b/src/components/alert/AlertPanel.tsx @@ -10,6 +10,7 @@ export type AlertType = { key: number; message: string; variant: AlertVariant; + description?: string; }; type AlertPanelProps = { @@ -20,21 +21,24 @@ type AlertPanelProps = { export function AlertPanel({ alerts, onCloseAlert }: AlertPanelProps) { return ( - {alerts.map(({ key, variant, message }) => ( - onCloseAlert(key)} - /> - } - /> + {alerts.map(({ key, variant, message, description }) => ( + <> + onCloseAlert(key)} + /> + } + > + {description &&

{description}

} +
+ ))}
); diff --git a/src/components/alert/Alerts.tsx b/src/components/alert/Alerts.tsx index 956b363492..7c96723860 100644 --- a/src/components/alert/Alerts.tsx +++ b/src/components/alert/Alerts.tsx @@ -3,7 +3,11 @@ import { AlertType, AlertPanel } from "./AlertPanel"; import { AlertVariant } from "@patternfly/react-core"; type AlertProps = { - addAlert: (message: string, variant?: AlertVariant) => void; + addAlert: ( + message: string, + variant?: AlertVariant, + description?: string + ) => void; }; export const AlertContext = createContext({ @@ -28,9 +32,10 @@ export const AlertProvider = ({ children }: { children: ReactNode }) => { const addAlert = ( message: string, - variant: AlertVariant = AlertVariant.default + variant: AlertVariant = AlertVariant.default, + description?: string ) => { - setAlerts([...alerts, { key: createId(), message, variant }]); + setAlerts([...alerts, { key: createId(), message, variant, description }]); }; return ( diff --git a/src/components/view-header/ViewHeader.tsx b/src/components/view-header/ViewHeader.tsx index be5ebfd01e..c8645bbe14 100644 --- a/src/components/view-header/ViewHeader.tsx +++ b/src/components/view-header/ViewHeader.tsx @@ -25,6 +25,8 @@ import { export type ViewHeaderProps = { titleKey: string; badge?: string; + badgeId?: string; + badgeIsRead?: boolean; subKey: string; actionsDropdownId?: string; subKeyLinkProps?: FormattedLinkProps; @@ -39,6 +41,8 @@ export const ViewHeader = ({ actionsDropdownId, titleKey, badge, + badgeId, + badgeIsRead, subKey, subKeyLinkProps, dropdownItems, @@ -73,7 +77,9 @@ export const ViewHeader = ({ {badge && ( - {badge} + + {badge} + )} diff --git a/src/realm-roles/AssociatedRolesTab.tsx b/src/realm-roles/AssociatedRolesTab.tsx index 8641b08d13..37807af6c4 100644 --- a/src/realm-roles/AssociatedRolesTab.tsx +++ b/src/realm-roles/AssociatedRolesTab.tsx @@ -1,28 +1,36 @@ import React, { useState } from "react"; -import { Link, useHistory, useRouteMatch } from "react-router-dom"; +import { useHistory, useParams, useRouteMatch } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { AlertVariant, Button, ButtonVariant, + Checkbox, PageSection, } from "@patternfly/react-core"; -import { IFormatter, IFormatterValueType } from "@patternfly/react-table"; - import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { formattedLinkTableCell } from "../components/external-link/FormattedLink"; import { useAlerts } from "../components/alert/Alerts"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; -import { emptyFormatter, toUpperCase } from "../util"; +import { boolFormatter, emptyFormatter } from "../util"; +import { AssociatedRolesModal } from "./AssociatedRolesModal"; +import { useAdminClient } from "../context/auth/AdminClient"; +import { RoleFormType } from "./RealmRoleTabs"; type AssociatedRolesTabProps = { additionalRoles: RoleRepresentation[]; + addComposites: (newReps: RoleRepresentation[]) => void; + parentRole: RoleFormType; + onRemove: (newReps: RoleRepresentation[]) => void; }; export const AssociatedRolesTab = ({ additionalRoles, + addComposites, + parentRole, + onRemove, }: AssociatedRolesTabProps) => { const { t } = useTranslation("roles"); const history = useHistory(); @@ -30,7 +38,11 @@ export const AssociatedRolesTab = ({ const { url } = useRouteMatch(); const tableRefresher = React.useRef<() => void>(); - const [selectedRole, setSelectedRole] = useState(); + const [selectedRows, setSelectedRows] = useState([]); + const [open, setOpen] = useState(false); + + const adminClient = useAdminClient(); + const { id } = useParams<{ id: string; clientId: string }>(); const loader = async () => { return Promise.resolve(additionalRoles); @@ -40,33 +52,47 @@ export const AssociatedRolesTab = ({ tableRefresher.current && tableRefresher.current(); }, [additionalRoles]); - const RoleDetailLink = (role: RoleRepresentation) => ( - <> - - {role.name} - - - ); + const RoleName = (role: RoleRepresentation) => <>{role.name}; - const boolFormatter = (): IFormatter => (data?: IFormatterValueType) => { - const boolVal = data?.toString(); - - return (boolVal ? toUpperCase(boolVal) : undefined) as string; - }; + const toggleModal = () => setOpen(!open); const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: "roles:roleRemoveAssociatedRoleConfirm", - messageKey: t("roles:roleRemoveAssociatedText", { - selectedRoleName: selectedRole ? selectedRole!.name : "", + messageKey: t("roles:roleRemoveAssociatedText"), + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + await adminClient.roles.delCompositeRoles({ id }, selectedRows); + setSelectedRows([]); + + addAlert(t("associatedRolesRemoved"), AlertVariant.success); + } catch (error) { + addAlert(t("roleDeleteError", { error }), AlertVariant.danger); + } + }, + }); + + const [ + toggleDeleteAssociatedRolesDialog, + DeleteAssociatedRolesConfirm, + ] = useConfirmDialog({ + titleKey: t("roles:removeAssociatedRoles") + "?", + messageKey: t("roles:removeAllAssociatedRolesConfirmDialog", { + name: parentRole?.name || t("createRole"), }), continueButtonLabel: "common:delete", continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { try { - // await adminClient.roles.delCompositeRoles({ id: compID }, compies); - - setSelectedRole(undefined); - addAlert(t("roleDeletedSuccess"), AlertVariant.success); + if (selectedRows.length === additionalRoles.length) { + onRemove(selectedRows); + const loc = url.replace(/\/AssociatedRoles/g, "/details"); + history.push(loc); + } + onRemove(selectedRows); + await adminClient.roles.delCompositeRoles({ id }, selectedRows); + addAlert(t("associatedRolesRemoved"), AlertVariant.success); } catch (error) { addAlert(`${t("roleDeleteError")} ${error}`, AlertVariant.danger); } @@ -82,23 +108,54 @@ export const AssociatedRolesTab = ({ <> + + setOpen(!open)} + /> { + setSelectedRows([...rows]); + }} isPaginated setRefresher={setRefresher} toolbarItem={ <> - + + + } actions={[ { title: t("common:remove"), onRowClick: (role) => { - setSelectedRole(role); + setSelectedRows([role]); toggleDeleteDialog(); }, }, @@ -107,12 +164,12 @@ export const AssociatedRolesTab = ({ { name: "name", displayKey: "roles:roleName", - cellRenderer: RoleDetailLink, + cellRenderer: RoleName, cellFormatters: [formattedLinkTableCell(), emptyFormatter()], }, { - name: "composite", - displayKey: "roles:composite", + name: "inherited from", + displayKey: "roles:inheritedFrom", cellFormatters: [boolFormatter(), emptyFormatter()], }, { diff --git a/src/realm-roles/RealmRoleTabs.tsx b/src/realm-roles/RealmRoleTabs.tsx index cf26c9cc7c..e4ea725922 100644 --- a/src/realm-roles/RealmRoleTabs.tsx +++ b/src/realm-roles/RealmRoleTabs.tsx @@ -213,11 +213,39 @@ export const RealmRoleTabs = () => { }, }); + const [ + toggleDeleteAllAssociatedRolesDialog, + DeleteAllAssociatedRolesConfirm, + ] = useConfirmDialog({ + titleKey: t("roles:removeAllAssociatedRoles") + "?", + messageKey: t("roles:removeAllAssociatedRolesConfirmDialog", { + name: role?.name || t("createRole"), + }), + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + await adminClient.roles.delCompositeRoles({ id }, additionalRoles); + addAlert( + t("compositeRoleOff"), + AlertVariant.success, + t("compositesRemovedAlertDescription") + ); + const loc = url.replace(/\/AssociatedRoles/g, "/details"); + history.push(loc); + refresh(); + } catch (error) { + addAlert(`${t("roleDeleteError")} ${error}`, AlertVariant.danger); + } + }, + }); + const toggleModal = () => setOpen(!open); return ( <> + { /> 0 ? t("composite") : ""} + badgeId="composite-role-badge" + badgeIsRead={true} subKey={id ? "" : "roles:roleCreateExplain"} actionsDropdownId="roles-actions-dropdown" dropdownItems={ - id + url.includes("AssociatedRoles") + ? [ + toggleDeleteAllAssociatedRolesDialog()} + > + {t("roles:removeAllAssociatedRoles")} + , + toggleDeleteDialog()} + > + {t("deleteRole")} + , + ] + : id ? [ { eventKey="AssociatedRoles" title={{t("associatedRolesText")}} > - + refresh()} + /> ) : null} Promise; paginated?: boolean; + parentRoleId?: string; }; -export const RolesList = ({ loader, paginated = true }: RolesListProps) => { +export const RolesList = ({ + loader, + paginated = true, + parentRoleId, +}: RolesListProps) => { const { t } = useTranslation("roles"); const history = useHistory(); const adminClient = useAdminClient(); @@ -47,9 +52,15 @@ export const RolesList = ({ loader, paginated = true }: RolesListProps) => { continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { try { - await adminClient.roles.delById({ - id: selectedRole!.id!, - }); + if (!parentRoleId) { + await adminClient.roles.delById({ + id: selectedRole!.id!, + }); + } else { + await adminClient.roles.delCompositeRoles({ id: parentRoleId }, [ + selectedRole!, + ]); + } setSelectedRole(undefined); addAlert(t("roleDeletedSuccess"), AlertVariant.success); } catch (error) { diff --git a/src/realm-roles/messages.json b/src/realm-roles/messages.json index a9cb6a7d15..28dfa7a3b0 100644 --- a/src/realm-roles/messages.json +++ b/src/realm-roles/messages.json @@ -8,6 +8,7 @@ "addAssociatedRolesSuccess": "Associated roles have been added", "associatedRolesModalTitle": "Add roles to {{name}}", "title": "Realm roles", + "addRole": "Add role", "createRole": "Create role", "importRole": "Import role", "roleID": "Role ID", @@ -19,6 +20,7 @@ "composite": "Composite", "deleteRole": "Delete this role", "details": "Details", + "inheritedFrom": "Inherited from", "roleList": "Role list", "searchFor": "Search role by name", "generalSettings": "General Settings", @@ -30,14 +32,21 @@ "roleDeleteConfirm": "Delete role?", "roleDeleteConfirmDialog": "This action will permanently delete the role {{selectedRoleName}} and cannot be undone.", "roleDeletedSuccess": "The role has been deleted", - "roleDeleteError": "Could not delete role:", + "roleDeleteError": "Could not delete role: {{error}}", "roleSaveSuccess": "The role has been saved", "roleSaveError": "Could not save role: {{error}}", "noRolesInThisRealm": "No roles in this realm", "noRolesInThisRealmInstructions": "You haven't created any roles in this realm. Create a role to get started.", "roleAuthentication": "Role authentication", + "removeAllAssociatedRoles": "Remove all associated roles", + "removeAssociatedRoles": "Remove associated roles", + "removeRoles": "Remove roles", + "removeAllAssociatedRolesConfirmDialog": "This action will remove the associated roles of {{name}}. Users who have permission to {{name}} will no longer have access to these roles.", "roleRemoveAssociatedRoleConfirm": "Remove associated role?", - "roleRemoveAssociatedText": "This action will remove {{role}} from {{roleName}. All the associated roles of {{role}} will also be removed." + "roleRemoveAssociatedText": "This action will remove {{role}} from {{roleName}. All the associated roles of {{role}} will also be removed.", + "compositeRoleOff": "Composite role turned off", + "associatedRolesRemoved": "Associated roles have been removed", + "compositesRemovedAlertDescription": "All the associated roles have been removed" } diff --git a/tests/cypress/integration/realm_roles_test.spec.ts b/tests/cypress/integration/realm_roles_test.spec.ts index c010852bcf..7e3df414dc 100644 --- a/tests/cypress/integration/realm_roles_test.spec.ts +++ b/tests/cypress/integration/realm_roles_test.spec.ts @@ -60,7 +60,7 @@ describe("Realm roles test", function () { listingPage.itemExist(itemId, false); }); - it("Associated roles modal test", function () { + it("Associated roles test", function () { itemId += "_" + (Math.random() + 1).toString(36).substring(7); // Create @@ -79,6 +79,10 @@ describe("Realm roles test", function () { cy.get('[type="checkbox"]').eq(1).check(); cy.get("#add-associated-roles-button").contains("Add").click(); + + cy.url().should("include", "/AssociatedRoles"); + + cy.get("#composite-role-badge").should("contain.text", "Composite"); }); }); });