Realm roles: adds "Inherited From" column to Associated Roles tab (#391)

* 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

* add inherited roles with Jeff

* hide inherited roles

* clean up

* rebase

* clean up modal file

* remove filter dropdown

* remove log stmts

* fix types error with Erik

* format after rebase

* fix lint

* fix cypress test

* PR feedback from Erik

* PR feedback from Erik

* remove comment

* remove client hook

* remove unused declaration
This commit is contained in:
Eugenia 2021-02-25 04:10:28 -05:00 committed by GitHub
parent 7924080847
commit da2fa32a69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 137 additions and 69 deletions

View file

@ -85,6 +85,8 @@ describe("Realm roles test", function () {
cy.get("#composite-role-badge").should("contain.text", "Composite"); cy.get("#composite-role-badge").should("contain.text", "Composite");
cy.wait(100);
// Add associated client role // Add associated client role
cy.get('[data-cy=add-role-button]').click(); cy.get('[data-cy=add-role-button]').click();
@ -97,7 +99,7 @@ describe("Realm roles test", function () {
cy.wait(2500); cy.wait(2500);
cy.get('[type="checkbox"]').eq(4).check({force: true}); cy.get('[type="checkbox"]').eq(40).check({force: true});
cy.get("#add-associated-roles-button").contains("Add").click(); cy.get("#add-associated-roles-button").contains("Add").click();

View file

@ -75,6 +75,7 @@ export const ClientsSection = () => {
</Link> </Link>
</> </>
); );
return ( return (
<> <>
<ViewHeader <ViewHeader

View file

@ -18,7 +18,6 @@ import { SearchIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
type TableToolbarProps = { type TableToolbarProps = {
filterToolbarDropdown?: ReactNode;
toolbarItem?: ReactNode; toolbarItem?: ReactNode;
toolbarItemFooter?: ReactNode; toolbarItemFooter?: ReactNode;
children: ReactNode; children: ReactNode;
@ -33,7 +32,6 @@ type TableToolbarProps = {
}; };
export const TableToolbar = ({ export const TableToolbar = ({
filterToolbarDropdown,
toolbarItem, toolbarItem,
toolbarItemFooter, toolbarItemFooter,
children, children,
@ -52,7 +50,6 @@ export const TableToolbar = ({
{inputGroupName && ( {inputGroupName && (
<ToolbarItem> <ToolbarItem>
<InputGroup> <InputGroup>
{filterToolbarDropdown}
{searchTypeComponent} {searchTypeComponent}
<TextInput <TextInput
name={inputGroupName} name={inputGroupName}

View file

@ -0,0 +1,46 @@
import React, { useEffect, useState } from "react";
import KeycloakAdminClient from "keycloak-admin";
import { Label } from "@patternfly/react-core";
export type AliasRendererComponentProps = {
name?: string;
containerId?: string;
filterType?: string;
adminClient: KeycloakAdminClient;
id: string;
};
export 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" || !containerName) {
return <>{name}</>;
}
if (filterType === "clients" || containerName) {
return (
<>
{containerId && (
<Label color="blue" key={`label-${id}`}>
{containerName}
</Label>
)}{" "}
{name}
</>
);
}
return null;
};

View file

@ -5,7 +5,6 @@ import {
Dropdown, Dropdown,
DropdownItem, DropdownItem,
DropdownToggle, DropdownToggle,
Label,
Modal, Modal,
ModalVariant, ModalVariant,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
@ -16,50 +15,7 @@ import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons"; import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons";
import KeycloakAdminClient from "keycloak-admin"; import { AliasRendererComponent } from "./AliasRendererComponent";
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 = { export type AssociatedRolesModalProps = {
open: boolean; open: boolean;
@ -95,7 +51,8 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
const [filterType, setFilterType] = useState("roles"); const [filterType, setFilterType] = useState("roles");
const tableRefresher = React.useRef<() => void>(); const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@ -171,8 +128,8 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
}); });
}; };
React.useEffect(() => { useEffect(() => {
tableRefresher.current && tableRefresher.current(); refresh();
}, [filterType]); }, [filterType]);
useEffect(() => { useEffect(() => {
@ -211,10 +168,6 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
setIsFilterDropdownOpen(!isFilterDropdownOpen); setIsFilterDropdownOpen(!isFilterDropdownOpen);
}; };
const setRefresher = (refresher: () => void) => {
tableRefresher.current = refresher;
};
return ( return (
<Modal <Modal
title={t("roles:associatedRolesModalTitle", { name })} title={t("roles:associatedRolesModalTitle", { name })}
@ -246,11 +199,10 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
]} ]}
> >
<KeycloakDataTable <KeycloakDataTable
key="role-list-modal" key={key}
loader={filterType == "roles" ? loader : clientRolesLoader} loader={filterType == "roles" ? loader : clientRolesLoader}
ariaLabelKey="roles:roleList" ariaLabelKey="roles:roleList"
searchPlaceholderKey="roles:searchFor" searchPlaceholderKey="roles:searchFor"
setRefresher={setRefresher}
searchTypeComponent={ searchTypeComponent={
<Dropdown <Dropdown
onSelect={() => onFilterDropdownSelect(filterType)} onSelect={() => onFilterDropdownSelect(filterType)}

View file

@ -14,16 +14,19 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
import { formattedLinkTableCell } from "../components/external-link/FormattedLink"; import { formattedLinkTableCell } from "../components/external-link/FormattedLink";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { boolFormatter, emptyFormatter } from "../util"; import { emptyFormatter } from "../util";
import { AssociatedRolesModal } from "./AssociatedRolesModal"; import { AssociatedRolesModal } from "./AssociatedRolesModal";
import { useAdminClient } from "../context/auth/AdminClient"; import { useAdminClient } from "../context/auth/AdminClient";
import { RoleFormType } from "./RealmRoleTabs"; import { RoleFormType } from "./RealmRoleTabs";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { AliasRendererComponent } from "./AliasRendererComponent";
type AssociatedRolesTabProps = { type AssociatedRolesTabProps = {
additionalRoles: RoleRepresentation[]; additionalRoles: RoleRepresentation[];
addComposites: (newReps: RoleRepresentation[]) => void; addComposites: (newReps: RoleRepresentation[]) => void;
parentRole: RoleFormType; parentRole: RoleFormType;
onRemove: (newReps: RoleRepresentation[]) => void; onRemove: (newReps: RoleRepresentation[]) => void;
client?: ClientRepresentation;
}; };
export const AssociatedRolesTab = ({ export const AssociatedRolesTab = ({
@ -40,20 +43,86 @@ export const AssociatedRolesTab = ({
const refresh = () => setKey(new Date().getTime()); const refresh = () => setKey(new Date().getTime());
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]); const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
const [isInheritedHidden, setIsInheritedHidden] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { id } = useParams<{ id: string; clientId: string }>(); const { id } = useParams<{ id: string }>();
const inheritanceMap = React.useRef<{ [key: string]: string }>({});
const getSubRoles = async (
role: RoleRepresentation,
allRoles: RoleRepresentation[]
): Promise<RoleRepresentation[]> => {
// Fetch all composite roles
const allCompositeRoles = await adminClient.roles.getCompositeRoles({
id: role.id!,
});
// Need to ensure we don't get into an infinite loop, do not add any role that is already there or the starting role
const newRoles: Promise<RoleRepresentation[]> = allCompositeRoles.reduce(
async (acc: Promise<RoleRepresentation[]>, newRole) => {
const resolvedRoles = await acc;
if (!allRoles.find((ar) => ar.id === newRole.id)) {
inheritanceMap.current[newRole.id!] = role.name!;
resolvedRoles.push(newRole);
const subRoles = await getSubRoles(newRole, [
...allRoles,
...resolvedRoles,
]);
resolvedRoles.push(...subRoles);
}
return acc;
},
Promise.resolve([] as RoleRepresentation[])
);
return newRoles;
};
const loader = async () => { const loader = async () => {
return Promise.resolve(additionalRoles); if (isInheritedHidden) {
return additionalRoles;
}
const allRoles: Promise<RoleRepresentation[]> = additionalRoles.reduce(
async (acc: Promise<RoleRepresentation[]>, role) => {
const resolvedRoles = await acc;
resolvedRoles.push(role);
const subRoles = await getSubRoles(role, resolvedRoles);
resolvedRoles.push(...subRoles);
return acc;
},
Promise.resolve([] as RoleRepresentation[])
);
return allRoles;
}; };
useEffect(() => { useEffect(() => {
refresh(); refresh();
}, [additionalRoles]); }, [additionalRoles, isInheritedHidden]);
const RoleName = (role: RoleRepresentation) => <>{role.name}</>; const InheritedRoleName = (role: RoleRepresentation) => {
return <>{inheritanceMap.current[role.id!]}</>;
};
const AliasRenderer = (role: RoleRepresentation) => {
return (
<>
<AliasRendererComponent
id={id}
name={role.name}
adminClient={adminClient}
containerId={role.containerId}
/>
</>
);
};
console.log(inheritanceMap);
const toggleModal = () => setOpen(!open); const toggleModal = () => setOpen(!open);
@ -128,6 +197,8 @@ export const AssociatedRolesTab = ({
label="Hide inherited roles" label="Hide inherited roles"
key="associated-roles-check" key="associated-roles-check"
id="kc-hide-inherited-roles-checkbox" id="kc-hide-inherited-roles-checkbox"
onChange={() => setIsInheritedHidden(!isInheritedHidden)}
isChecked={isInheritedHidden}
/> />
<Button <Button
className="kc-add-role-button" className="kc-add-role-button"
@ -162,13 +233,14 @@ export const AssociatedRolesTab = ({
{ {
name: "name", name: "name",
displayKey: "roles:roleName", displayKey: "roles:roleName",
cellRenderer: RoleName, cellRenderer: AliasRenderer,
cellFormatters: [formattedLinkTableCell(), emptyFormatter()], cellFormatters: [formattedLinkTableCell(), emptyFormatter()],
}, },
{ {
name: "inherited from", name: "containerId",
displayKey: "roles:inheritedFrom", displayKey: "roles:inheritedFrom",
cellFormatters: [boolFormatter(), emptyFormatter()], cellRenderer: InheritedRoleName,
cellFormatters: [emptyFormatter()],
}, },
{ {
name: "description", name: "description",

View file

@ -52,7 +52,6 @@ export const RealmRoleTabs = () => {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
const form = useForm<RoleFormType>({ mode: "onChange" }); const form = useForm<RoleFormType>({ mode: "onChange" });
const history = useHistory(); const history = useHistory();
// const [name, setName] = useState("");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const [role, setRole] = useState<RoleFormType>(); const [role, setRole] = useState<RoleFormType>();
@ -322,6 +321,7 @@ export const RealmRoleTabs = () => {
addComposites={addComposites} addComposites={addComposites}
parentRole={role!} parentRole={role!}
onRemove={() => refresh()} onRemove={() => refresh()}
// client={client!}
/> />
</Tab> </Tab>
) : null} ) : null}

View file

@ -49,7 +49,5 @@
"compositeRoleOff": "Composite role turned off", "compositeRoleOff": "Composite role turned off",
"associatedRolesRemoved": "Associated roles have been removed", "associatedRolesRemoved": "Associated roles have been removed",
"compositesRemovedAlertDescription": "All the associated roles have been removed" "compositesRemovedAlertDescription": "All the associated roles have been removed"
} }
} }