From e37085969069352f76e0a712b002ab82ca972f56 Mon Sep 17 00:00:00 2001 From: Eugenia <32821331+jenny-s51@users.noreply.github.com> Date: Tue, 23 Feb 2021 08:36:37 -0500 Subject: [PATCH] Modal with clients (#380) * 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 WIP modal with client roles add clients to modal WIP label labels WIP promises wip wip add clients to modal with labels modal with Clients format * rebase * fix check types * PR feedback from Erik --- cypress/integration/realm_roles_test.spec.ts | 19 ++ .../table-toolbar/KeycloakDataTable.tsx | 4 + .../table-toolbar/PaginatingTableToolbar.tsx | 3 + src/components/table-toolbar/TableToolbar.tsx | 3 + src/realm-roles/AssociatedRolesModal.tsx | 182 +++++++++++++++++- src/realm-roles/AssociatedRolesTab.tsx | 1 + src/realm-roles/RealmRolesSection.tsx | 1 + src/realm-roles/messages.json | 2 + 8 files changed, 205 insertions(+), 10 deletions(-) diff --git a/cypress/integration/realm_roles_test.spec.ts b/cypress/integration/realm_roles_test.spec.ts index 7e3df414dc..52ae73c177 100644 --- a/cypress/integration/realm_roles_test.spec.ts +++ b/cypress/integration/realm_roles_test.spec.ts @@ -70,6 +70,7 @@ describe("Realm roles test", function () { masthead.checkNotificationMessage("Role created"); + // Add associated realm role cy.get("#roles-actions-dropdown").last().click(); cy.get("#add-roles").click(); @@ -83,6 +84,24 @@ describe("Realm roles test", function () { cy.url().should("include", "/AssociatedRoles"); cy.get("#composite-role-badge").should("contain.text", "Composite"); + + // Add associated client role + + cy.get('[data-cy=add-role-button]').click(); + + cy.wait(100); + + cy.get('[data-cy=filter-type-dropdown]').click() + + cy.get('[data-cy=filter-type-dropdown-item]').click() + + cy.wait(2500); + + cy.get('[type="checkbox"]').eq(4).check({force: true}); + + cy.get("#add-associated-roles-button").contains("Add").click(); + + cy.wait(2500); }); }); }); diff --git a/src/components/table-toolbar/KeycloakDataTable.tsx b/src/components/table-toolbar/KeycloakDataTable.tsx index 3bd7cb76dc..60bec32546 100644 --- a/src/components/table-toolbar/KeycloakDataTable.tsx +++ b/src/components/table-toolbar/KeycloakDataTable.tsx @@ -89,6 +89,7 @@ export type DataListProps = { columns: Field[]; actions?: Action[]; actionResolver?: IActionsResolver; + searchTypeComponent?: ReactNode; toolbarItem?: ReactNode; emptyState?: ReactNode; }; @@ -126,6 +127,7 @@ export function KeycloakDataTable({ columns, actions, actionResolver, + searchTypeComponent, toolbarItem, emptyState, }: DataListProps) { @@ -262,6 +264,7 @@ export function KeycloakDataTable({ inputGroupOnChange={searchOnChange} inputGroupOnClick={refresh} inputGroupPlaceholder={t(searchPlaceholderKey)} + searchTypeComponent={searchTypeComponent} toolbarItem={toolbarItem} > {!loading && (emptyState === undefined || rows.length !== 0) && ( @@ -286,6 +289,7 @@ export function KeycloakDataTable({ inputGroupOnClick={() => {}} inputGroupPlaceholder={t(searchPlaceholderKey)} toolbarItem={toolbarItem} + searchTypeComponent={searchTypeComponent} > {(emptyState === undefined || rows.length !== 0) && ( void; onPreviousClick: (page: number) => void; onPerPageSelect: (max: number, first: number) => void; + searchTypeComponent?: React.ReactNode; toolbarItem?: React.ReactNode; children: React.ReactNode; inputGroupName?: string; @@ -31,6 +32,7 @@ export const PaginatingTableToolbar = ({ onNextClick, onPreviousClick, onPerPageSelect, + searchTypeComponent, toolbarItem, children, inputGroupName, @@ -59,6 +61,7 @@ export const PaginatingTableToolbar = ({ return ( {toolbarItem} diff --git a/src/components/table-toolbar/TableToolbar.tsx b/src/components/table-toolbar/TableToolbar.tsx index eaf0e0ec2b..9e03ccf78a 100644 --- a/src/components/table-toolbar/TableToolbar.tsx +++ b/src/components/table-toolbar/TableToolbar.tsx @@ -18,6 +18,7 @@ import { SearchIcon } from "@patternfly/react-icons"; import { useTranslation } from "react-i18next"; type TableToolbarProps = { + filterToolbarDropdown?: ReactNode; toolbarItem?: ReactNode; toolbarItemFooter?: ReactNode; children: ReactNode; @@ -32,6 +33,7 @@ type TableToolbarProps = { }; export const TableToolbar = ({ + filterToolbarDropdown, toolbarItem, toolbarItemFooter, children, @@ -50,6 +52,7 @@ export const TableToolbar = ({ {inputGroupName && ( + {filterToolbarDropdown} {searchTypeComponent} { + const [containerName, setContainerName] = useState(""); + + useEffect(() => { + adminClient.clients + .findOne({ id: containerId! }) + .then((client) => setContainerName(client.clientId as string)); + }, [containerId]); + + if (filterType === "roles") { + return <>{name}; + } + + if (filterType === "clients") { + return ( + <> + {containerId && ( + + )}{" "} + {name} + + ); + } + + return null; +}; export type AssociatedRolesModalProps = { open: boolean; @@ -37,16 +89,38 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { const [name, setName] = useState(""); const adminClient = useAdminClient(); const [selectedRows, setSelectedRows] = useState([]); + const [allClientRoles, setAllClientRoles] = useState( + [] + ); + + const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); + const [filterType, setFilterType] = useState("roles"); + const tableRefresher = React.useRef<() => void>(); const { id } = useParams<{ id: string }>(); + const alphabetize = (rolesList: RoleRepresentation[]) => { + return rolesList.sort((r1, r2) => { + const r1Name = r1.name?.toUpperCase(); + const r2Name = r2.name?.toUpperCase(); + if (r1Name! < r2Name!) { + return -1; + } + if (r1Name! > r2Name!) { + return 1; + } + + return 0; + }); + }; + const loader = async () => { const allRoles = await adminClient.roles.find(); const existingAdditionalRoles = await adminClient.roles.getCompositeRoles({ id, }); - return allRoles.filter((role: RoleRepresentation) => { + return alphabetize(allRoles).filter((role: RoleRepresentation) => { return ( existingAdditionalRoles.find( (existing: RoleRepresentation) => existing.name === role.name @@ -55,6 +129,52 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { }); }; + const AliasRenderer = (role: RoleRepresentation) => { + return ( + <> + + + ); + }; + + const clientRolesLoader = async () => { + const clients = await adminClient.clients.find(); + + const clientIdArray = clients.map((client) => client.id); + + let rolesList: RoleRepresentation[] = []; + for (const id of clientIdArray) { + const clientRolesList = await adminClient.clients.listRoles({ + id: id as string, + }); + rolesList = [...rolesList, ...clientRolesList]; + } + const existingAdditionalRoles = await adminClient.roles.getCompositeRoles({ + id, + }); + + setAllClientRoles(rolesList); + console.log(allClientRoles); + + return alphabetize(rolesList).filter((role: RoleRepresentation) => { + return ( + existingAdditionalRoles.find( + (existing: RoleRepresentation) => existing.name === role.name + ) === undefined && role.name !== name + ); + }); + }; + + React.useEffect(() => { + tableRefresher.current && tableRefresher.current(); + }, [filterType]); + useEffect(() => { (async () => { if (id) { @@ -77,6 +197,24 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { }); }; + const onFilterDropdownToggle = () => { + setIsFilterDropdownOpen(!isFilterDropdownOpen); + }; + + const onFilterDropdownSelect = (filterType: string) => { + if (filterType == "roles") { + setFilterType("clients"); + } + if (filterType == "clients") { + setFilterType("roles"); + } + setIsFilterDropdownOpen(!isFilterDropdownOpen); + }; + + const setRefresher = (refresher: () => void) => { + tableRefresher.current = refresher; + }; + return ( { > onFilterDropdownSelect(filterType)} + data-cy="filter-type-dropdown" + toggle={ + } + > + Filter by {filterType} + + } + isOpen={isFilterDropdownOpen} + dropdownItems={[ + + {filterType == "roles" + ? t("filterByClients") + : t("filterByRoles")}{" "} + , + ]} + /> + } canSelectAll // isPaginated onSelect={(rows) => { @@ -121,15 +287,11 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { { name: "name", displayKey: "roles:roleName", - }, - { - name: "composite", - displayKey: "roles:composite", - cellFormatters: [boolFormatter()], + cellRenderer: AliasRenderer, }, { name: "description", - displayKey: "roles:description", + displayKey: "common:description", }, ]} emptyState={ diff --git a/src/realm-roles/AssociatedRolesTab.tsx b/src/realm-roles/AssociatedRolesTab.tsx index 37807af6c4..6eb2f046eb 100644 --- a/src/realm-roles/AssociatedRolesTab.tsx +++ b/src/realm-roles/AssociatedRolesTab.tsx @@ -136,6 +136,7 @@ export const AssociatedRolesTab = ({ className="kc-add-role-button" key="add-role-button" onClick={() => toggleModal()} + data-cy="add-role-button" > {t("addRole")} diff --git a/src/realm-roles/RealmRolesSection.tsx b/src/realm-roles/RealmRolesSection.tsx index dbddad6227..0878b3fcca 100644 --- a/src/realm-roles/RealmRolesSection.tsx +++ b/src/realm-roles/RealmRolesSection.tsx @@ -6,6 +6,7 @@ import { RolesList } from "./RolesList"; export const RealmRolesSection = () => { const adminClient = useAdminClient(); + const loader = async (first?: number, max?: number, search?: string) => { const params: { [name: string]: string | number } = { first: first!, diff --git a/src/realm-roles/messages.json b/src/realm-roles/messages.json index 28dfa7a3b0..f68c61cab6 100644 --- a/src/realm-roles/messages.json +++ b/src/realm-roles/messages.json @@ -13,6 +13,8 @@ "importRole": "Import role", "roleID": "Role ID", "homeURL": "Home URL", + "filterByClients": "Filter by clients", + "filterByRoles": "Filter by roles", "roleExplain": "Realm-level roles are a global namespace to define your roles.", "roleCreateExplain": "This is some description", "roleName": "Role name",