From a5f08a92027acb7a4b80dbde6921e9805883ec41 Mon Sep 17 00:00:00 2001
From: Eugenia <32821331+jenny-s51@users.noreply.github.com>
Date: Wed, 17 Feb 2021 02:17:04 -0500
Subject: [PATCH] Realm roles: associated roles tab (#358)
* WIP modal
* modal WIP
* add modal
* place modal in separate file
* format
* wip implementation
* getCompositeRoles with Jeff
* add associated roles tab WIP
* addComposites function WIP
* fix post call
* additional roles fetch
* big rebase
* WIP refresh
* resolve conflicts with Erik latest -> fixes role creation
* cypress tests, bump react-hook-form to remove console warnings
* delete add
* refresh with Jeff, update cypress tests, select additional roles tab on add
* make dropdownId optional
* format
* add additionalRolesModal to associated roles tab
* add toolbar items
* add toolbaritems to associated role tab, matches mock
* rebase
* add descriptions to alert
* add badge
* fix badge logic
* fix URL when associate roles are deleted, format
* update cypress test
* format
* add associated roles refresh, PR feedback from Erik
* add associated roles refresh, PR feedback from Erik
* lint
---
src/components/alert/AlertPanel.tsx | 34 +++---
src/components/alert/Alerts.tsx | 11 +-
src/components/view-header/ViewHeader.tsx | 8 +-
src/realm-roles/AssociatedRolesTab.tsx | 115 +++++++++++++-----
src/realm-roles/RealmRoleTabs.tsx | 57 ++++++++-
src/realm-roles/RealmRolesSection.css | 6 +-
src/realm-roles/RolesList.tsx | 19 ++-
src/realm-roles/messages.json | 13 +-
.../integration/realm_roles_test.spec.ts | 6 +-
9 files changed, 211 insertions(+), 58 deletions(-)
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");
});
});
});