added permission search (#1890)

This commit is contained in:
Erik Jan de Wit 2022-01-24 15:17:32 +01:00 committed by GitHub
parent 92c73a7fcc
commit c4724b21c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 186 additions and 106 deletions

View file

@ -30,12 +30,13 @@ import { useAlerts } from "../../components/alert/Alerts";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import useToggle from "../../utils/useToggle";
import { useRealm } from "../../context/realm-context/RealmContext";
import { SearchDropdown } from "./SearchDropdown";
import { SearchDropdown, SearchForm } from "./SearchDropdown";
import { MoreLabel } from "./MoreLabel";
import { DetailDescription } from "./DetailDescription";
import { EmptyPermissionsState } from "./EmptyPermissionsState";
import { toNewPermission } from "../routes/NewPermission";
import { toPermissionDetails } from "../routes/PermissionDetails";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import "./permissions.css";
@ -64,6 +65,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
const [disabledCreate, setDisabledCreate] =
useState<{ resources: boolean; scopes: boolean }>();
const [createOpen, toggleCreate] = useToggle();
const [search, setSearch] = useState<SearchForm>({});
const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1);
@ -90,6 +92,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
first,
max,
id: clientId,
...search,
});
return await Promise.all(
@ -109,7 +112,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
);
},
setPermissions,
[key]
[key, search]
);
useFetch(
@ -166,10 +169,12 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
return <KeycloakSpinner />;
}
const noData = permissions.length === 0;
const searching = Object.keys(search).length !== 0;
return (
<PageSection variant="light" className="pf-u-p-0">
<DeleteConfirm />
{permissions.length > 0 && (
{(!noData || searching) && (
<PaginatingTableToolbar
count={permissions.length}
first={first}
@ -183,7 +188,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
toolbarItem={
<>
<ToolbarItem>
<SearchDropdown types={policyProviders} />
<SearchDropdown types={policyProviders} onSearch={setSearch} />
</ToolbarItem>
<ToolbarItem>
<Dropdown
@ -238,104 +243,113 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
</>
}
>
<TableComposable aria-label={t("resources")} variant="compact">
<Thead>
<Tr>
<Th />
<Th>{t("common:name")}</Th>
<Th>{t("common:type")}</Th>
<Th>{t("associatedPolicy")}</Th>
<Th>{t("common:description")}</Th>
<Th />
</Tr>
</Thead>
{permissions.map((permission, rowIndex) => (
<Tbody key={permission.id} isExpanded={permission.isExpanded}>
{!noData && (
<TableComposable aria-label={t("resources")} variant="compact">
<Thead>
<Tr>
<Td
expand={{
rowIndex,
isExpanded: permission.isExpanded,
onToggle: (_, rowIndex) => {
const rows = permissions.map((p, index) =>
index === rowIndex
? { ...p, isExpanded: !p.isExpanded }
: p
);
setPermissions(rows);
},
}}
/>
<Td data-testid={`name-column-${permission.name}`}>
<Link
to={toPermissionDetails({
realm,
id: clientId,
permissionType: permission.type!,
permissionId: permission.id!,
})}
>
{permission.name}
</Link>
</Td>
<Td>
{
policyProviders?.find((p) => p.type === permission.type)
?.name
}
</Td>
<Td>
<AssociatedPoliciesRenderer row={permission} />
</Td>
<Td>{permission.description}</Td>
<Td
actions={{
items: [
{
title: t("common:delete"),
onClick: async () => {
setSelectedPermission(permission);
toggleDeleteDialog();
},
<Th />
<Th>{t("common:name")}</Th>
<Th>{t("common:type")}</Th>
<Th>{t("associatedPolicy")}</Th>
<Th>{t("common:description")}</Th>
<Th />
</Tr>
</Thead>
{permissions.map((permission, rowIndex) => (
<Tbody key={permission.id} isExpanded={permission.isExpanded}>
<Tr>
<Td
expand={{
rowIndex,
isExpanded: permission.isExpanded,
onToggle: (_, rowIndex) => {
const rows = permissions.map((p, index) =>
index === rowIndex
? { ...p, isExpanded: !p.isExpanded }
: p
);
setPermissions(rows);
},
],
}}
></Td>
</Tr>
<Tr
key={`child-${permission.id}`}
isExpanded={permission.isExpanded}
>
<Td />
<Td colSpan={5}>
<ExpandableRowContent>
{permission.isExpanded && (
<DescriptionList
isHorizontal
className="keycloak_resource_details"
>
<DetailDescription
name="associatedPolicy"
array={permission.associatedPolicies}
convert={(p) => p.name!}
/>
</DescriptionList>
)}
</ExpandableRowContent>
</Td>
</Tr>
</Tbody>
))}
</TableComposable>
}}
/>
<Td data-testid={`name-column-${permission.name}`}>
<Link
to={toPermissionDetails({
realm,
id: clientId,
permissionType: permission.type!,
permissionId: permission.id!,
})}
>
{permission.name}
</Link>
</Td>
<Td>
{
policyProviders?.find((p) => p.type === permission.type)
?.name
}
</Td>
<Td>
<AssociatedPoliciesRenderer row={permission} />
</Td>
<Td>{permission.description}</Td>
<Td
actions={{
items: [
{
title: t("common:delete"),
onClick: async () => {
setSelectedPermission(permission);
toggleDeleteDialog();
},
},
],
}}
></Td>
</Tr>
<Tr
key={`child-${permission.id}`}
isExpanded={permission.isExpanded}
>
<Td />
<Td colSpan={5}>
<ExpandableRowContent>
{permission.isExpanded && (
<DescriptionList
isHorizontal
className="keycloak_resource_details"
>
<DetailDescription
name="associatedPolicy"
array={permission.associatedPolicies}
convert={(p) => p.name!}
/>
</DescriptionList>
)}
</ExpandableRowContent>
</Td>
</Tr>
</Tbody>
))}
</TableComposable>
)}
</PaginatingTableToolbar>
)}
{permissions.length === 0 && (
{noData && !searching && (
<EmptyPermissionsState
clientId={clientId}
isResourceEnabled={disabledCreate?.resources}
isScopeEnabled={disabledCreate?.scopes}
/>
)}
{noData && searching && (
<ListEmptyState
isSearchVariant
message={t("common:noSearchResults")}
instructions={t("common:noSearchResultsInstructions")}
/>
)}
</PageSection>
);
};

View file

@ -2,6 +2,8 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form";
import {
ActionGroup,
Button,
Dropdown,
DropdownToggle,
Form,
@ -17,17 +19,50 @@ import useToggle from "../../utils/useToggle";
import "./search-dropdown.css";
type SearchDropdownProps = {
types?: PolicyProviderRepresentation[];
export type SearchForm = {
name?: string;
resource?: string;
scope?: string;
type?: string;
};
export const SearchDropdown = ({ types }: SearchDropdownProps) => {
type SearchDropdownProps = {
types?: PolicyProviderRepresentation[];
onSearch: (form: SearchForm) => void;
};
export const SearchDropdown = ({ types, onSearch }: SearchDropdownProps) => {
const { t } = useTranslation("clients");
const { register, control } = useForm();
const {
register,
control,
formState: { isDirty },
handleSubmit,
} = useForm<SearchForm>({ mode: "onChange" });
const [open, toggle] = useToggle();
const [typeOpen, toggleType] = useToggle();
const submit = (form: SearchForm) => {
toggle();
onSearch(form);
};
const typeOptions = (value: string) => [
<SelectOption key="empty" value="">
{t("allTypes")}
</SelectOption>,
...(types || []).map((type) => (
<SelectOption
selected={type.type === value}
key={type.type}
value={type.type}
>
{type.name}
</SelectOption>
)),
];
return (
<Dropdown
data-testid="searchdropdown_dorpdown"
@ -45,6 +80,7 @@ export const SearchDropdown = ({ types }: SearchDropdownProps) => {
<Form
isHorizontal
className="keycloak__client_authentication__searchdropdown_form"
onSubmit={handleSubmit(submit)}
>
<FormGroup label={t("common:name")} fieldId="name">
<TextInput
@ -55,6 +91,24 @@ export const SearchDropdown = ({ types }: SearchDropdownProps) => {
data-testid="searchdropdown_name"
/>
</FormGroup>
<FormGroup label={t("resource")} fieldId="resource">
<TextInput
ref={register}
type="text"
id="resource"
name="resource"
data-testid="searchdropdown_resource"
/>
</FormGroup>
<FormGroup label={t("scope")} fieldId="scope">
<TextInput
ref={register}
type="text"
id="scope"
name="scope"
data-testid="searchdropdown_scope"
/>
</FormGroup>
<FormGroup label={t("common:type")} fieldId="type">
<Controller
name="type"
@ -69,24 +123,33 @@ export const SearchDropdown = ({ types }: SearchDropdownProps) => {
onChange(value);
toggleType();
}}
selections={value.name}
selections={value || t("allTypes")}
variant={SelectVariant.single}
aria-label={t("common:type")}
isOpen={typeOpen}
>
{types?.map((type) => (
<SelectOption
selected={type.type === value.type}
key={type.type}
value={type}
>
{type.name}
</SelectOption>
))}
{typeOptions(value)}
</Select>
)}
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
type="submit"
data-testid="search-btn"
isDisabled={!isDirty}
>
{t("common:search")}
</Button>
<Button
variant="link"
data-testid="revert-btn"
onClick={() => onSearch({})}
>
{t("common:clear")}
</Button>
</ActionGroup>
</Form>
</Dropdown>
);

View file

@ -126,6 +126,9 @@ export default {
associatedPermissions: "Associated permission",
allowRemoteResourceManagement: "Remote resource management",
resources: "Resources",
resource: "Resource",
allTypes: "All types",
scope: "Scope",
owner: "Owner",
uris: "URIs",
scopes: "Scopes",