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:
Eugenia 2021-04-09 11:08:40 -04:00 committed by GitHub
parent f37802bd01
commit dd1e1f511e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 77 additions and 34 deletions

View file

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

View file

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

View file

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