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:
parent
7924080847
commit
da2fa32a69
8 changed files with 137 additions and 69 deletions
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,7 @@ export const ClientsSection = () => {
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
|
|
|
@ -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}
|
||||||
|
|
46
src/realm-roles/AliasRendererComponent.tsx
Normal file
46
src/realm-roles/AliasRendererComponent.tsx
Normal 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;
|
||||||
|
};
|
|
@ -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)}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue