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 { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
import useToggle from "../../utils/useToggle";
|
import useToggle from "../../utils/useToggle";
|
||||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
import { SearchDropdown } from "./SearchDropdown";
|
import { SearchDropdown, SearchForm } from "./SearchDropdown";
|
||||||
import { MoreLabel } from "./MoreLabel";
|
import { MoreLabel } from "./MoreLabel";
|
||||||
import { DetailDescription } from "./DetailDescription";
|
import { DetailDescription } from "./DetailDescription";
|
||||||
import { EmptyPermissionsState } from "./EmptyPermissionsState";
|
import { EmptyPermissionsState } from "./EmptyPermissionsState";
|
||||||
import { toNewPermission } from "../routes/NewPermission";
|
import { toNewPermission } from "../routes/NewPermission";
|
||||||
import { toPermissionDetails } from "../routes/PermissionDetails";
|
import { toPermissionDetails } from "../routes/PermissionDetails";
|
||||||
|
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||||
|
|
||||||
import "./permissions.css";
|
import "./permissions.css";
|
||||||
|
|
||||||
|
@ -64,6 +65,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||||
const [disabledCreate, setDisabledCreate] =
|
const [disabledCreate, setDisabledCreate] =
|
||||||
useState<{ resources: boolean; scopes: boolean }>();
|
useState<{ resources: boolean; scopes: boolean }>();
|
||||||
const [createOpen, toggleCreate] = useToggle();
|
const [createOpen, toggleCreate] = useToggle();
|
||||||
|
const [search, setSearch] = useState<SearchForm>({});
|
||||||
|
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const refresh = () => setKey(key + 1);
|
const refresh = () => setKey(key + 1);
|
||||||
|
@ -90,6 +92,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||||
first,
|
first,
|
||||||
max,
|
max,
|
||||||
id: clientId,
|
id: clientId,
|
||||||
|
...search,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
|
@ -109,7 +112,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
setPermissions,
|
setPermissions,
|
||||||
[key]
|
[key, search]
|
||||||
);
|
);
|
||||||
|
|
||||||
useFetch(
|
useFetch(
|
||||||
|
@ -166,10 +169,12 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||||
return <KeycloakSpinner />;
|
return <KeycloakSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const noData = permissions.length === 0;
|
||||||
|
const searching = Object.keys(search).length !== 0;
|
||||||
return (
|
return (
|
||||||
<PageSection variant="light" className="pf-u-p-0">
|
<PageSection variant="light" className="pf-u-p-0">
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
{permissions.length > 0 && (
|
{(!noData || searching) && (
|
||||||
<PaginatingTableToolbar
|
<PaginatingTableToolbar
|
||||||
count={permissions.length}
|
count={permissions.length}
|
||||||
first={first}
|
first={first}
|
||||||
|
@ -183,7 +188,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||||
toolbarItem={
|
toolbarItem={
|
||||||
<>
|
<>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<SearchDropdown types={policyProviders} />
|
<SearchDropdown types={policyProviders} onSearch={setSearch} />
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
@ -238,104 +243,113 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableComposable aria-label={t("resources")} variant="compact">
|
{!noData && (
|
||||||
<Thead>
|
<TableComposable aria-label={t("resources")} variant="compact">
|
||||||
<Tr>
|
<Thead>
|
||||||
<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>
|
<Tr>
|
||||||
<Td
|
<Th />
|
||||||
expand={{
|
<Th>{t("common:name")}</Th>
|
||||||
rowIndex,
|
<Th>{t("common:type")}</Th>
|
||||||
isExpanded: permission.isExpanded,
|
<Th>{t("associatedPolicy")}</Th>
|
||||||
onToggle: (_, rowIndex) => {
|
<Th>{t("common:description")}</Th>
|
||||||
const rows = permissions.map((p, index) =>
|
<Th />
|
||||||
index === rowIndex
|
</Tr>
|
||||||
? { ...p, isExpanded: !p.isExpanded }
|
</Thead>
|
||||||
: p
|
{permissions.map((permission, rowIndex) => (
|
||||||
);
|
<Tbody key={permission.id} isExpanded={permission.isExpanded}>
|
||||||
setPermissions(rows);
|
<Tr>
|
||||||
},
|
<Td
|
||||||
}}
|
expand={{
|
||||||
/>
|
rowIndex,
|
||||||
<Td data-testid={`name-column-${permission.name}`}>
|
isExpanded: permission.isExpanded,
|
||||||
<Link
|
onToggle: (_, rowIndex) => {
|
||||||
to={toPermissionDetails({
|
const rows = permissions.map((p, index) =>
|
||||||
realm,
|
index === rowIndex
|
||||||
id: clientId,
|
? { ...p, isExpanded: !p.isExpanded }
|
||||||
permissionType: permission.type!,
|
: p
|
||||||
permissionId: permission.id!,
|
);
|
||||||
})}
|
setPermissions(rows);
|
||||||
>
|
|
||||||
{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>
|
<Td data-testid={`name-column-${permission.name}`}>
|
||||||
</Tr>
|
<Link
|
||||||
<Tr
|
to={toPermissionDetails({
|
||||||
key={`child-${permission.id}`}
|
realm,
|
||||||
isExpanded={permission.isExpanded}
|
id: clientId,
|
||||||
>
|
permissionType: permission.type!,
|
||||||
<Td />
|
permissionId: permission.id!,
|
||||||
<Td colSpan={5}>
|
})}
|
||||||
<ExpandableRowContent>
|
>
|
||||||
{permission.isExpanded && (
|
{permission.name}
|
||||||
<DescriptionList
|
</Link>
|
||||||
isHorizontal
|
</Td>
|
||||||
className="keycloak_resource_details"
|
<Td>
|
||||||
>
|
{
|
||||||
<DetailDescription
|
policyProviders?.find((p) => p.type === permission.type)
|
||||||
name="associatedPolicy"
|
?.name
|
||||||
array={permission.associatedPolicies}
|
}
|
||||||
convert={(p) => p.name!}
|
</Td>
|
||||||
/>
|
<Td>
|
||||||
</DescriptionList>
|
<AssociatedPoliciesRenderer row={permission} />
|
||||||
)}
|
</Td>
|
||||||
</ExpandableRowContent>
|
<Td>{permission.description}</Td>
|
||||||
</Td>
|
<Td
|
||||||
</Tr>
|
actions={{
|
||||||
</Tbody>
|
items: [
|
||||||
))}
|
{
|
||||||
</TableComposable>
|
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>
|
</PaginatingTableToolbar>
|
||||||
)}
|
)}
|
||||||
{permissions.length === 0 && (
|
{noData && !searching && (
|
||||||
<EmptyPermissionsState
|
<EmptyPermissionsState
|
||||||
clientId={clientId}
|
clientId={clientId}
|
||||||
isResourceEnabled={disabledCreate?.resources}
|
isResourceEnabled={disabledCreate?.resources}
|
||||||
isScopeEnabled={disabledCreate?.scopes}
|
isScopeEnabled={disabledCreate?.scopes}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{noData && searching && (
|
||||||
|
<ListEmptyState
|
||||||
|
isSearchVariant
|
||||||
|
message={t("common:noSearchResults")}
|
||||||
|
instructions={t("common:noSearchResultsInstructions")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,8 @@ import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
|
ActionGroup,
|
||||||
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
Form,
|
Form,
|
||||||
|
@ -17,17 +19,50 @@ import useToggle from "../../utils/useToggle";
|
||||||
|
|
||||||
import "./search-dropdown.css";
|
import "./search-dropdown.css";
|
||||||
|
|
||||||
type SearchDropdownProps = {
|
export type SearchForm = {
|
||||||
types?: PolicyProviderRepresentation[];
|
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 { t } = useTranslation("clients");
|
||||||
const { register, control } = useForm();
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
formState: { isDirty },
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm<SearchForm>({ mode: "onChange" });
|
||||||
|
|
||||||
const [open, toggle] = useToggle();
|
const [open, toggle] = useToggle();
|
||||||
const [typeOpen, toggleType] = 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 (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
data-testid="searchdropdown_dorpdown"
|
data-testid="searchdropdown_dorpdown"
|
||||||
|
@ -45,6 +80,7 @@ export const SearchDropdown = ({ types }: SearchDropdownProps) => {
|
||||||
<Form
|
<Form
|
||||||
isHorizontal
|
isHorizontal
|
||||||
className="keycloak__client_authentication__searchdropdown_form"
|
className="keycloak__client_authentication__searchdropdown_form"
|
||||||
|
onSubmit={handleSubmit(submit)}
|
||||||
>
|
>
|
||||||
<FormGroup label={t("common:name")} fieldId="name">
|
<FormGroup label={t("common:name")} fieldId="name">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -55,6 +91,24 @@ export const SearchDropdown = ({ types }: SearchDropdownProps) => {
|
||||||
data-testid="searchdropdown_name"
|
data-testid="searchdropdown_name"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</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">
|
<FormGroup label={t("common:type")} fieldId="type">
|
||||||
<Controller
|
<Controller
|
||||||
name="type"
|
name="type"
|
||||||
|
@ -69,24 +123,33 @@ export const SearchDropdown = ({ types }: SearchDropdownProps) => {
|
||||||
onChange(value);
|
onChange(value);
|
||||||
toggleType();
|
toggleType();
|
||||||
}}
|
}}
|
||||||
selections={value.name}
|
selections={value || t("allTypes")}
|
||||||
variant={SelectVariant.single}
|
variant={SelectVariant.single}
|
||||||
aria-label={t("common:type")}
|
aria-label={t("common:type")}
|
||||||
isOpen={typeOpen}
|
isOpen={typeOpen}
|
||||||
>
|
>
|
||||||
{types?.map((type) => (
|
{typeOptions(value)}
|
||||||
<SelectOption
|
|
||||||
selected={type.type === value.type}
|
|
||||||
key={type.type}
|
|
||||||
value={type}
|
|
||||||
>
|
|
||||||
{type.name}
|
|
||||||
</SelectOption>
|
|
||||||
))}
|
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</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>
|
</Form>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|
|
@ -126,6 +126,9 @@ export default {
|
||||||
associatedPermissions: "Associated permission",
|
associatedPermissions: "Associated permission",
|
||||||
allowRemoteResourceManagement: "Remote resource management",
|
allowRemoteResourceManagement: "Remote resource management",
|
||||||
resources: "Resources",
|
resources: "Resources",
|
||||||
|
resource: "Resource",
|
||||||
|
allTypes: "All types",
|
||||||
|
scope: "Scope",
|
||||||
owner: "Owner",
|
owner: "Owner",
|
||||||
uris: "URIs",
|
uris: "URIs",
|
||||||
scopes: "Scopes",
|
scopes: "Scopes",
|
||||||
|
|
Loading…
Reference in a new issue