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,6 +243,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{!noData && (
|
||||||
<TableComposable aria-label={t("resources")} variant="compact">
|
<TableComposable aria-label={t("resources")} variant="compact">
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
|
@ -327,15 +333,23 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||||
</Tbody>
|
</Tbody>
|
||||||
))}
|
))}
|
||||||
</TableComposable>
|
</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