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
This commit is contained in:
Eugenia 2021-02-23 08:36:37 -05:00 committed by GitHub
parent a83ae98175
commit e370859690
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 205 additions and 10 deletions

View file

@ -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);
});
});
});

View file

@ -89,6 +89,7 @@ export type DataListProps<T> = {
columns: Field<T>[];
actions?: Action<T>[];
actionResolver?: IActionsResolver;
searchTypeComponent?: ReactNode;
toolbarItem?: ReactNode;
emptyState?: ReactNode;
};
@ -126,6 +127,7 @@ export function KeycloakDataTable<T>({
columns,
actions,
actionResolver,
searchTypeComponent,
toolbarItem,
emptyState,
}: DataListProps<T>) {
@ -262,6 +264,7 @@ export function KeycloakDataTable<T>({
inputGroupOnChange={searchOnChange}
inputGroupOnClick={refresh}
inputGroupPlaceholder={t(searchPlaceholderKey)}
searchTypeComponent={searchTypeComponent}
toolbarItem={toolbarItem}
>
{!loading && (emptyState === undefined || rows.length !== 0) && (
@ -286,6 +289,7 @@ export function KeycloakDataTable<T>({
inputGroupOnClick={() => {}}
inputGroupPlaceholder={t(searchPlaceholderKey)}
toolbarItem={toolbarItem}
searchTypeComponent={searchTypeComponent}
>
{(emptyState === undefined || rows.length !== 0) && (
<DataTable

View file

@ -13,6 +13,7 @@ type TableToolbarProps = {
onNextClick: (page: number) => 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 (
<TableToolbar
searchTypeComponent={searchTypeComponent}
toolbarItem={
<>
{toolbarItem}

View file

@ -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 && (
<ToolbarItem>
<InputGroup>
{filterToolbarDropdown}
{searchTypeComponent}
<TextInput
name={inputGroupName}

View file

@ -1,13 +1,65 @@
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { Button, Modal, ModalVariant } from "@patternfly/react-core";
import {
Button,
Dropdown,
DropdownItem,
DropdownToggle,
Label,
Modal,
ModalVariant,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { useAdminClient } from "../context/auth/AdminClient";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { boolFormatter } from "../util";
import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons";
import KeycloakAdminClient from "keycloak-admin";
type AliasRendererComponentProps = {
name?: string;
containerId?: string;
filterType: string;
adminClient: KeycloakAdminClient;
id: string;
};
const AliasRendererComponent = ({
name,
containerId,
filterType,
adminClient,
id,
}: AliasRendererComponentProps) => {
const [containerName, setContainerName] = useState<string>("");
useEffect(() => {
adminClient.clients
.findOne({ id: containerId! })
.then((client) => setContainerName(client.clientId as string));
}, [containerId]);
if (filterType === "roles") {
return <>{name}</>;
}
if (filterType === "clients") {
return (
<>
{containerId && (
<Label color="blue" key={`label-${id}`}>
{containerName}
</Label>
)}{" "}
{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<RoleRepresentation[]>([]);
const [allClientRoles, setAllClientRoles] = useState<RoleRepresentation[]>(
[]
);
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 (
<>
<AliasRendererComponent
id={id}
name={role.name}
adminClient={adminClient}
filterType={filterType}
containerId={role.containerId}
/>
</>
);
};
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 (
<Modal
title={t("roles:associatedRolesModalTitle", { name })}
@ -109,9 +247,37 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
>
<KeycloakDataTable
key="role-list-modal"
loader={loader}
loader={filterType == "roles" ? loader : clientRolesLoader}
ariaLabelKey="roles:roleList"
searchPlaceholderKey="roles:searchFor"
setRefresher={setRefresher}
searchTypeComponent={
<Dropdown
onSelect={() => onFilterDropdownSelect(filterType)}
data-cy="filter-type-dropdown"
toggle={
<DropdownToggle
id="toggle-id-9"
onToggle={onFilterDropdownToggle}
toggleIndicator={CaretDownIcon}
icon={<FilterIcon />}
>
Filter by {filterType}
</DropdownToggle>
}
isOpen={isFilterDropdownOpen}
dropdownItems={[
<DropdownItem
data-cy="filter-type-dropdown-item"
key="filter-type"
>
{filterType == "roles"
? t("filterByClients")
: t("filterByRoles")}{" "}
</DropdownItem>,
]}
/>
}
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={

View file

@ -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")}
</Button>

View file

@ -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!,

View file

@ -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",