added permission search (#1890)
This commit is contained in:
parent
92c73a7fcc
commit
c4724b21c8
3 changed files with 186 additions and 106 deletions
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue