Realm roles(associated roles): fix search filtering bug (#497)
* realm roles UX review progress wip * filter realm roles on Enter key press, add filter functionality * clean up * filterChips logic now in table toolbar * fix lint and format * save with erik * remove filter chips functionality * fix check-types * fix realm roles cypress test * format * wip pagination * rebase * fix roles pagination * format * add back save * remove duplicates in associated roles table, can now paginate modal * remove logs * rebase and fix pagination/search * remove slice * pagination in modal and associated roles tab * show client roles * lint and format * fix ts error in AliasRenderer * fix lint * add filterType * wip search in component * fix associated roles tab pagination * revert KDT changes * fix text * add promise resolve type * format * remove comment * add alphabetize function * fix search * remove log stmt * clean up * address PR feedback from Erik and render clientId badge in associated roles * remove comment * fix type * format * make checkboxes selectable * address PR feedbak from Erik * changes from rebase
This commit is contained in:
parent
f37802bd01
commit
dd1e1f511e
3 changed files with 77 additions and 34 deletions
|
@ -268,17 +268,23 @@ export function KeycloakDataTable<T>({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data![rowIndex].selected = isSelected;
|
data![rowIndex].selected = isSelected;
|
||||||
|
|
||||||
setRows([...rows!]);
|
setRows([...rows!]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keeps selected items when paginating
|
||||||
const difference = _.differenceBy(
|
const difference = _.differenceBy(
|
||||||
selected,
|
selected,
|
||||||
data!.map((row) => row.data),
|
data!.map((row) => row.data),
|
||||||
"id"
|
"id"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Selected rows are any rows previously selected from a different page, plus current page selections
|
||||||
const selectedRows = [
|
const selectedRows = [
|
||||||
...difference,
|
...difference,
|
||||||
...data!.filter((row) => row.selected).map((row) => row.data),
|
...data!.filter((row) => row.selected).map((row) => row.data),
|
||||||
];
|
];
|
||||||
|
|
||||||
setSelected(selectedRows);
|
setSelected(selectedRows);
|
||||||
onSelect!(selectedRows);
|
onSelect!(selectedRows);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
|
Label,
|
||||||
Modal,
|
Modal,
|
||||||
ModalVariant,
|
ModalVariant,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
@ -14,10 +15,13 @@ 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 { AliasRendererComponent } from "./AliasRendererComponent";
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useErrorHandler } from "react-error-boundary";
|
import { useErrorHandler } from "react-error-boundary";
|
||||||
|
|
||||||
|
type Role = RoleRepresentation & {
|
||||||
|
clientId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AssociatedRolesModalProps = {
|
export type AssociatedRolesModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
toggleDialog: () => void;
|
toggleDialog: () => void;
|
||||||
|
@ -50,11 +54,21 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
});
|
});
|
||||||
const allRoles = [...roles, ...existingAdditionalRoles];
|
const allRoles = [...roles, ...existingAdditionalRoles];
|
||||||
|
|
||||||
const filterDupes = allRoles.filter(
|
const filterDupes: Role[] = allRoles.filter(
|
||||||
(thing, index, self) =>
|
(thing, index, self) =>
|
||||||
index === self.findIndex((t) => t.name === thing.name)
|
index === self.findIndex((t) => t.name === thing.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const clients = await adminClient.clients.find();
|
||||||
|
filterDupes
|
||||||
|
.filter((role) => role.clientRole)
|
||||||
|
.map(
|
||||||
|
(role) =>
|
||||||
|
(role.clientId = clients.find(
|
||||||
|
(client) => client.id === role.containerId
|
||||||
|
)!.clientId!)
|
||||||
|
);
|
||||||
|
|
||||||
return alphabetize(filterDupes).filter((role: RoleRepresentation) => {
|
return alphabetize(filterDupes).filter((role: RoleRepresentation) => {
|
||||||
return (
|
return (
|
||||||
props.existingCompositeRoles.find(
|
props.existingCompositeRoles.find(
|
||||||
|
@ -64,16 +78,15 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const AliasRenderer = (role: RoleRepresentation) => {
|
const AliasRenderer = ({ id, name, clientId }: Role) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AliasRendererComponent
|
{clientId && (
|
||||||
id={id}
|
<Label color="blue" key={`label-${id}`}>
|
||||||
name={role.name}
|
{clientId}
|
||||||
adminClient={adminClient}
|
</Label>
|
||||||
filterType={filterType}
|
)}{" "}
|
||||||
containerId={role.containerId}
|
{name}
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -83,7 +96,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
|
|
||||||
const clientIdArray = clients.map((client) => client.id);
|
const clientIdArray = clients.map((client) => client.id);
|
||||||
|
|
||||||
let rolesList: RoleRepresentation[] = [];
|
let rolesList: Role[] = [];
|
||||||
for (const id of clientIdArray) {
|
for (const id of clientIdArray) {
|
||||||
const clientRolesList = await adminClient.clients.listRoles({
|
const clientRolesList = await adminClient.clients.listRoles({
|
||||||
id: id as string,
|
id: id as string,
|
||||||
|
@ -94,6 +107,15 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
rolesList
|
||||||
|
.filter((role) => role.clientRole)
|
||||||
|
.map(
|
||||||
|
(role) =>
|
||||||
|
(role.clientId = clients.find(
|
||||||
|
(client) => client.id === role.containerId
|
||||||
|
)!.clientId!)
|
||||||
|
);
|
||||||
|
|
||||||
return alphabetize(rolesList).filter((role: RoleRepresentation) => {
|
return alphabetize(rolesList).filter((role: RoleRepresentation) => {
|
||||||
return (
|
return (
|
||||||
existingAdditionalRoles.find(
|
existingAdditionalRoles.find(
|
||||||
|
@ -213,8 +235,8 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
emptyState={
|
emptyState={
|
||||||
<ListEmptyState
|
<ListEmptyState
|
||||||
hasIcon={true}
|
hasIcon={true}
|
||||||
message={t("noRolesInThisRealm")}
|
message={t("noRoles")}
|
||||||
instructions={t("noRolesInThisRealmInstructions")}
|
instructions={t("noRolesInstructions")}
|
||||||
primaryActionText={t("createRole")}
|
primaryActionText={t("createRole")}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
Button,
|
Button,
|
||||||
ButtonVariant,
|
ButtonVariant,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Label,
|
||||||
PageSection,
|
PageSection,
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
@ -20,17 +21,20 @@ 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 ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||||
import { AliasRendererComponent } from "./AliasRendererComponent";
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
type AssociatedRolesTabProps = {
|
type AssociatedRolesTabProps = {
|
||||||
additionalRoles: RoleRepresentation[];
|
additionalRoles: Role[];
|
||||||
addComposites: (newReps: RoleRepresentation[]) => void;
|
addComposites: (newReps: RoleRepresentation[]) => void;
|
||||||
parentRole: RoleFormType;
|
parentRole: RoleFormType;
|
||||||
onRemove: (newReps: RoleRepresentation[]) => void;
|
onRemove: (newReps: RoleRepresentation[]) => void;
|
||||||
client?: ClientRepresentation;
|
client?: ClientRepresentation;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Role = RoleRepresentation & {
|
||||||
|
clientId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const AssociatedRolesTab = ({
|
export const AssociatedRolesTab = ({
|
||||||
additionalRoles,
|
additionalRoles,
|
||||||
addComposites,
|
addComposites,
|
||||||
|
@ -54,10 +58,7 @@ export const AssociatedRolesTab = ({
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const inheritanceMap = React.useRef<{ [key: string]: string }>({});
|
const inheritanceMap = React.useRef<{ [key: string]: string }>({});
|
||||||
|
|
||||||
const getSubRoles = async (
|
const getSubRoles = async (role: Role, allRoles: Role[]): Promise<Role[]> => {
|
||||||
role: RoleRepresentation,
|
|
||||||
allRoles: RoleRepresentation[]
|
|
||||||
): Promise<RoleRepresentation[]> => {
|
|
||||||
// Fetch all composite roles
|
// Fetch all composite roles
|
||||||
const allCompositeRoles = await adminClient.roles.getCompositeRoles({
|
const allCompositeRoles = await adminClient.roles.getCompositeRoles({
|
||||||
id: role.id!,
|
id: role.id!,
|
||||||
|
@ -86,33 +87,47 @@ export const AssociatedRolesTab = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const alphabetize = (rolesList: RoleRepresentation[]) => {
|
const alphabetize = (rolesList: Role[]) => {
|
||||||
return _.sortBy(rolesList, (role) => role.name?.toUpperCase());
|
return _.sortBy(rolesList, (role) => role.name?.toUpperCase());
|
||||||
};
|
};
|
||||||
|
const clients = await adminClient.clients.find();
|
||||||
|
|
||||||
if (isInheritedHidden) {
|
if (isInheritedHidden) {
|
||||||
setAllRoles(additionalRoles);
|
setAllRoles(additionalRoles);
|
||||||
return alphabetize(additionalRoles);
|
return alphabetize(
|
||||||
|
additionalRoles.filter(
|
||||||
|
(role) =>
|
||||||
|
role.containerId === "master" && !inheritanceMap.current[role.id!]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchedRoles: Promise<RoleRepresentation[]> = additionalRoles.reduce(
|
const fetchedRoles: Promise<Role[]> = additionalRoles.reduce(
|
||||||
async (acc: Promise<RoleRepresentation[]>, role) => {
|
async (acc: Promise<Role[]>, role) => {
|
||||||
const resolvedRoles = await acc;
|
const resolvedRoles = await acc;
|
||||||
resolvedRoles.push(role);
|
resolvedRoles.push(role);
|
||||||
const subRoles = await getSubRoles(role, resolvedRoles);
|
const subRoles = await getSubRoles(role, resolvedRoles);
|
||||||
resolvedRoles.push(...subRoles);
|
resolvedRoles.push(...subRoles);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
Promise.resolve([] as RoleRepresentation[])
|
Promise.resolve([] as Role[])
|
||||||
);
|
);
|
||||||
|
|
||||||
return fetchedRoles.then((results: RoleRepresentation[]) => {
|
return fetchedRoles.then((results: Role[]) => {
|
||||||
const filterDupes = results.filter(
|
const filterDupes = results.filter(
|
||||||
(thing, index, self) =>
|
(thing, index, self) =>
|
||||||
index === self.findIndex((t) => t.name === thing.name)
|
index === self.findIndex((t) => t.name === thing.name)
|
||||||
);
|
);
|
||||||
setAllRoles(filterDupes);
|
filterDupes
|
||||||
|
.filter((role) => role.clientRole)
|
||||||
|
.map(
|
||||||
|
(role) =>
|
||||||
|
(role.clientId = clients.find(
|
||||||
|
(client) => client.id === role.containerId
|
||||||
|
)!.clientId!)
|
||||||
|
);
|
||||||
|
|
||||||
return alphabetize(filterDupes);
|
return alphabetize(additionalRoles);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,15 +139,15 @@ export const AssociatedRolesTab = ({
|
||||||
return <>{inheritanceMap.current[role.id!]}</>;
|
return <>{inheritanceMap.current[role.id!]}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AliasRenderer = (role: RoleRepresentation) => {
|
const AliasRenderer = ({ id, name, clientId }: Role) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AliasRendererComponent
|
{clientId && (
|
||||||
id={id}
|
<Label color="blue" key={`label-${id}`}>
|
||||||
name={role.name}
|
{clientId}
|
||||||
adminClient={adminClient}
|
</Label>
|
||||||
containerId={role.containerId}
|
)}{" "}
|
||||||
/>
|
{name}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue