keycloak-scim/js/apps/admin-ui/src/clients/authorization/ScopeSelect.tsx

123 lines
3.5 KiB
TypeScript
Raw Normal View History

import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
import { Select, SelectOption, SelectVariant } from "@patternfly/react-core";
import { useRef, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { adminClient } from "../../admin-client";
import { useFetch } from "../../utils/useFetch";
type ScopeSelectProps = {
clientId: string;
resourceId?: string;
preSelected?: string;
};
export const ScopeSelect = ({
clientId,
resourceId,
preSelected,
}: ScopeSelectProps) => {
const { t } = useTranslation();
const {
control,
getValues,
setValue,
formState: { errors },
} = useFormContext();
const [scopes, setScopes] = useState<ScopeRepresentation[]>([]);
const [selectedScopes, setSelectedScopes] = useState<ScopeRepresentation[]>(
[],
);
const [search, setSearch] = useState("");
const [open, setOpen] = useState(false);
const firstUpdate = useRef(true);
const values: string[] | undefined = getValues("scopes");
const toSelectOptions = (scopes: ScopeRepresentation[]) =>
scopes.map((scope) => (
<SelectOption key={scope.id} value={scope}>
{scope.name}
</SelectOption>
));
useFetch(
async (): Promise<ScopeRepresentation[]> => {
if (!resourceId) {
return adminClient.clients.listAllScopes(
Object.assign(
{ id: clientId, deep: false },
search === "" ? null : { name: search },
),
);
}
if (resourceId && !firstUpdate.current) {
setValue("scopes", []);
}
firstUpdate.current = false;
return adminClient.clients.listScopesByResource({
id: clientId,
resourceName: resourceId,
});
},
(scopes) => {
setScopes(scopes);
if (!search)
setSelectedScopes(
scopes.filter((s: ScopeRepresentation) => values?.includes(s.id!)),
);
},
[resourceId, search],
);
return (
<Controller
name="scopes"
defaultValue={preSelected ? [preSelected] : []}
control={control}
rules={{ validate: (value) => value.length > 0 }}
render={({ field }) => (
<Select
toggleId="scopes"
variant={SelectVariant.typeaheadMulti}
onToggle={setOpen}
onFilter={(_, filter) => {
setSearch(filter);
return toSelectOptions(scopes);
}}
onClear={() => {
field.onChange([]);
setSearch("");
}}
selections={selectedScopes.map((s) => s.name)}
onSelect={(_, selectedValue) => {
const option =
typeof selectedValue === "string"
? selectedScopes.find((s) => s.name === selectedValue)!
: (selectedValue as ScopeRepresentation);
const changedValue = selectedScopes.find((p) => p.id === option.id)
? selectedScopes.filter((p) => p.id !== option.id)
: [...selectedScopes, option];
field.onChange(changedValue.map((s) => s.id));
setSelectedScopes(changedValue);
setSearch("");
}}
isOpen={open}
aria-labelledby={t("scopes")}
validated={errors.scopes ? "error" : "default"}
isDisabled={!!preSelected}
typeAheadAriaLabel={t("scopes")}
>
{toSelectOptions(scopes)}
</Select>
)}
/>
);
};