changed to use ui-shared (#27933)
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
parent
5a99c558dc
commit
53d52ecf15
6 changed files with 114 additions and 289 deletions
|
@ -29,6 +29,7 @@ export default class PoliciesTab extends CommonPage {
|
|||
inputClient(clientName: string) {
|
||||
cy.get("#clients").click();
|
||||
cy.get("ul li").contains(clientName).click();
|
||||
cy.get("#clients").click();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,16 +5,11 @@ import {
|
|||
Dropdown,
|
||||
DropdownToggle,
|
||||
Form,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
|
||||
import { SelectControl, TextControl } from "ui-shared";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
|
||||
import "./search-dropdown.css";
|
||||
|
@ -42,16 +37,14 @@ export const SearchDropdown = ({
|
|||
type,
|
||||
}: SearchDropdownProps) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm<SearchForm>({ mode: "onChange" });
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
handleSubmit,
|
||||
} = useForm<SearchForm>({ mode: "onChange" });
|
||||
} = form;
|
||||
|
||||
const [open, toggle] = useToggle();
|
||||
const [typeOpen, toggleType] = useToggle();
|
||||
|
||||
const submit = (form: SearchForm) => {
|
||||
toggle();
|
||||
|
@ -60,21 +53,6 @@ export const SearchDropdown = ({
|
|||
|
||||
useEffect(() => reset(search), [search]);
|
||||
|
||||
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"
|
||||
|
@ -91,105 +69,59 @@ export const SearchDropdown = ({
|
|||
}
|
||||
isOpen={open}
|
||||
>
|
||||
<Form
|
||||
isHorizontal
|
||||
className="keycloak__client_authentication__searchdropdown_form"
|
||||
onSubmit={handleSubmit(submit)}
|
||||
>
|
||||
<FormGroup label={t("name")} fieldId="name">
|
||||
<KeycloakTextInput
|
||||
id="name"
|
||||
data-testid="searchdropdown_name"
|
||||
{...register("name")}
|
||||
/>
|
||||
</FormGroup>
|
||||
{type === "resource" && (
|
||||
<>
|
||||
<FormGroup label={t("type")} fieldId="type">
|
||||
<KeycloakTextInput
|
||||
id="type"
|
||||
data-testid="searchdropdown_type"
|
||||
{...register("type")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={t("uris")} fieldId="uri">
|
||||
<KeycloakTextInput
|
||||
id="uri"
|
||||
data-testid="searchdropdown_uri"
|
||||
{...register("uri")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={t("owner")} fieldId="owner">
|
||||
<KeycloakTextInput
|
||||
id="owner"
|
||||
data-testid="searchdropdown_owner"
|
||||
{...register("owner")}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
{type !== "resource" && type !== "policy" && (
|
||||
<FormGroup label={t("resource")} fieldId="resource">
|
||||
<KeycloakTextInput
|
||||
id="resource"
|
||||
data-testid="searchdropdown_resource"
|
||||
{...register("resource")}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{type !== "policy" && (
|
||||
<FormGroup label={t("scope")} fieldId="scope">
|
||||
<KeycloakTextInput
|
||||
id="scope"
|
||||
data-testid="searchdropdown_scope"
|
||||
{...register("scope")}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{type !== "resource" && (
|
||||
<FormGroup label={t("type")} fieldId="type">
|
||||
<Controller
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
isHorizontal
|
||||
className="keycloak__client_authentication__searchdropdown_form"
|
||||
onSubmit={handleSubmit(submit)}
|
||||
>
|
||||
<TextControl name="name" label={t("name")} />
|
||||
{type === "resource" && (
|
||||
<>
|
||||
<TextControl name="type" label={t("type")} />
|
||||
<TextControl name="uris" label={t("uris")} />
|
||||
<TextControl name="owner" label={t("owner")} />
|
||||
</>
|
||||
)}
|
||||
{type !== "resource" && type !== "policy" && (
|
||||
<TextControl name="resource" label={t("resource")} />
|
||||
)}
|
||||
{type !== "policy" && <TextControl name="scope" label={t("scope")} />}
|
||||
{type !== "resource" && (
|
||||
<SelectControl
|
||||
name="type"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
toggleId="type"
|
||||
onToggle={toggleType}
|
||||
onSelect={(event, value) => {
|
||||
event.stopPropagation();
|
||||
field.onChange(value);
|
||||
toggleType();
|
||||
}}
|
||||
selections={field.value || t("allTypes")}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("type")}
|
||||
isOpen={typeOpen}
|
||||
>
|
||||
{typeOptions(field.value)}
|
||||
</Select>
|
||||
)}
|
||||
label={t("type")}
|
||||
controller={{
|
||||
defaultValue: "",
|
||||
}}
|
||||
options={[
|
||||
{ key: "", value: t("allTypes") },
|
||||
...(types || []).map(({ type, name }) => ({
|
||||
key: type!,
|
||||
value: name!,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="search-btn"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("search")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="revert-btn"
|
||||
onClick={() => onSearch({})}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
)}
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="search-btn"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("search")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="revert-btn"
|
||||
onClick={() => onSearch({})}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type ResourceServerRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceServerRepresentation";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
|
@ -5,24 +6,22 @@ import {
|
|||
FormGroup,
|
||||
PageSection,
|
||||
Radio,
|
||||
Switch,
|
||||
} from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HelpItem } from "ui-shared";
|
||||
|
||||
import { adminClient } from "../../admin-client";
|
||||
import type ResourceServerRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceServerRepresentation";
|
||||
import { DefaultSwitchControl } from "../../components/SwitchControl";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { FixedButtonsGroup } from "../../components/form/FixedButtonGroup";
|
||||
import { FormAccess } from "../../components/form/FormAccess";
|
||||
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
import { useFetch } from "../../utils/useFetch";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
import { DecisionStrategySelect } from "./DecisionStrategySelect";
|
||||
import { ImportDialog } from "./ImportDialog";
|
||||
import { useFetch } from "../../utils/useFetch";
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
|
||||
const POLICY_ENFORCEMENT_MODES = [
|
||||
"ENFORCING",
|
||||
|
@ -145,35 +144,12 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
|||
</FormGroup>
|
||||
<FormProvider {...form}>
|
||||
<DecisionStrategySelect isLimited />
|
||||
</FormProvider>
|
||||
<FormGroup
|
||||
hasNoPaddingTop
|
||||
label={t("allowRemoteResourceManagement")}
|
||||
fieldId="allowRemoteResourceManagement"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("allowRemoteResourceManagementHelp")}
|
||||
fieldLabelId="allowRemoteResourceManagement"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
<DefaultSwitchControl
|
||||
name="allowRemoteResourceManagement"
|
||||
data-testid="allowRemoteResourceManagement"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
id="allowRemoteResourceManagement"
|
||||
label={t("on")}
|
||||
labelOff={t("off")}
|
||||
isChecked={field.value}
|
||||
onChange={field.onChange}
|
||||
aria-label={t("allowRemoteResourceManagement")}
|
||||
/>
|
||||
)}
|
||||
label={t("allowRemoteResourceManagement")}
|
||||
labelIcon={t("allowRemoteResourceManagementHelp")}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormProvider>
|
||||
<FixedButtonsGroup
|
||||
name="authenticationSettings"
|
||||
reset={() => reset(resource)}
|
||||
|
|
|
@ -1,114 +1,18 @@
|
|||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
||||
import {
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { SelectVariant } from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HelpItem } from "ui-shared";
|
||||
|
||||
import { adminClient } from "../../../admin-client";
|
||||
import { useFetch } from "../../../utils/useFetch";
|
||||
import { ClientSelect } from "../../../components/client/ClientSelect";
|
||||
|
||||
export const Client = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
control,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
const values: string[] | undefined = getValues("clients");
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [clients, setClients] = useState<ClientRepresentation[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
const params: ClientQuery = {
|
||||
max: 20,
|
||||
};
|
||||
if (search) {
|
||||
params.clientId = search;
|
||||
params.search = true;
|
||||
}
|
||||
|
||||
if (values?.length && !search) {
|
||||
return await Promise.all(
|
||||
values.map(
|
||||
(id: string) =>
|
||||
adminClient.clients.findOne({ id }) as ClientRepresentation,
|
||||
),
|
||||
);
|
||||
}
|
||||
return await adminClient.clients.find(params);
|
||||
},
|
||||
setClients,
|
||||
[search],
|
||||
);
|
||||
|
||||
const convert = (clients: ClientRepresentation[]) =>
|
||||
clients.map((option) => (
|
||||
<SelectOption
|
||||
key={option.id!}
|
||||
value={option.id}
|
||||
selected={values?.includes(option.id!)}
|
||||
>
|
||||
{option.clientId}
|
||||
</SelectOption>
|
||||
));
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
<ClientSelect
|
||||
name="clients"
|
||||
label={t("clients")}
|
||||
labelIcon={
|
||||
<HelpItem helpText={t("policyClientHelp")} fieldLabelId="client" />
|
||||
}
|
||||
fieldId="clients"
|
||||
helperTextInvalid={t("requiredClient")}
|
||||
validated={errors.clients ? "error" : "default"}
|
||||
isRequired
|
||||
>
|
||||
<Controller
|
||||
name="clients"
|
||||
defaultValue={[]}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => value.length > 0,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
toggleId="clients"
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
typeAheadAriaLabel={t("clients")}
|
||||
onToggle={(open) => setOpen(open)}
|
||||
isOpen={open}
|
||||
selections={field.value}
|
||||
aria-label={t("selectClients")}
|
||||
onFilter={(_, value) => {
|
||||
setSearch(value);
|
||||
return convert(clients);
|
||||
}}
|
||||
onSelect={(_, v) => {
|
||||
const option = v.toString();
|
||||
if (field.value.includes(option)) {
|
||||
field.onChange(
|
||||
field.value.filter((item: string) => item !== option),
|
||||
);
|
||||
} else {
|
||||
field.onChange([...field.value, option]);
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{convert(clients)}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
helpText={t("policyClientHelp")}
|
||||
required
|
||||
defaultValue={[]}
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
||||
import { SelectVariant } from "@patternfly/react-core";
|
||||
import { SelectProps, SelectVariant } from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SelectControl } from "ui-shared";
|
||||
|
@ -9,7 +9,7 @@ import { adminClient } from "../../admin-client";
|
|||
import { useFetch } from "../../utils/useFetch";
|
||||
import type { ComponentProps } from "../dynamic/components";
|
||||
|
||||
type ClientSelectProps = ComponentProps & {};
|
||||
type ClientSelectProps = ComponentProps & Pick<SelectProps, "variant">;
|
||||
|
||||
export const ClientSelect = ({
|
||||
name,
|
||||
|
@ -18,6 +18,7 @@ export const ClientSelect = ({
|
|||
defaultValue,
|
||||
isDisabled = false,
|
||||
required = false,
|
||||
variant = SelectVariant.typeahead,
|
||||
}: ClientSelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
@ -54,12 +55,12 @@ export const ClientSelect = ({
|
|||
},
|
||||
}}
|
||||
onFilter={(value) => setSearch(value)}
|
||||
variant={SelectVariant.typeahead}
|
||||
variant={variant}
|
||||
isDisabled={isDisabled}
|
||||
options={[
|
||||
{ key: "", value: t("none") },
|
||||
...clients.map(({ id, clientId }) => ({ key: id!, value: clientId! })),
|
||||
]}
|
||||
options={clients.map(({ id, clientId }) => ({
|
||||
key: id!,
|
||||
value: clientId!,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,12 +1,3 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldValues,
|
||||
FieldPath,
|
||||
useFormContext,
|
||||
UseControllerProps,
|
||||
} from "react-hook-form";
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
|
@ -14,6 +5,15 @@ import {
|
|||
SelectVariant,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
UseControllerProps,
|
||||
useFormContext,
|
||||
} from "react-hook-form";
|
||||
import { FormLabel } from "./FormLabel";
|
||||
|
||||
export type SelectControlOption = {
|
||||
|
@ -21,6 +21,8 @@ export type SelectControlOption = {
|
|||
value: string;
|
||||
};
|
||||
|
||||
type OptionType = string[] | SelectControlOption[];
|
||||
|
||||
export type SelectControlProps<
|
||||
T extends FieldValues,
|
||||
P extends FieldPath<T> = FieldPath<T>,
|
||||
|
@ -37,12 +39,17 @@ export type SelectControlProps<
|
|||
UseControllerProps<T, P> & {
|
||||
name: string;
|
||||
label?: string;
|
||||
options: string[] | SelectControlOption[];
|
||||
options: OptionType;
|
||||
labelIcon?: string;
|
||||
controller: Omit<ControllerProps, "name" | "render">;
|
||||
onFilter?: (value: string) => void;
|
||||
};
|
||||
|
||||
const isString = (option: SelectControlOption | string): option is string =>
|
||||
typeof option === "string";
|
||||
const key = (option: SelectControlOption | string) =>
|
||||
isString(option) ? option : option.key;
|
||||
|
||||
export const SelectControl = <
|
||||
T extends FieldValues,
|
||||
P extends FieldPath<T> = FieldPath<T>,
|
||||
|
@ -68,19 +75,19 @@ export const SelectControl = <
|
|||
option.toString().toLowerCase().startsWith(lowercasePrefix),
|
||||
)
|
||||
.map((option) => (
|
||||
<SelectOption
|
||||
key={typeof option === "string" ? option : option.key}
|
||||
value={typeof option === "string" ? option : option.key}
|
||||
>
|
||||
{typeof option === "string" ? option : option.value}
|
||||
<SelectOption key={key(option)} value={key(option)}>
|
||||
{isString(option) ? option : option.value}
|
||||
</SelectOption>
|
||||
));
|
||||
};
|
||||
const isSelectBasedOptions = (
|
||||
options: OptionType,
|
||||
): options is SelectControlOption[] => typeof options[0] !== "string";
|
||||
return (
|
||||
<FormLabel
|
||||
name={name}
|
||||
label={label}
|
||||
isRequired={controller.rules?.required === true}
|
||||
isRequired={!!controller.rules?.required}
|
||||
error={errors[name]}
|
||||
labelIcon={labelIcon}
|
||||
>
|
||||
|
@ -94,8 +101,8 @@ export const SelectControl = <
|
|||
toggleId={name.slice(name.lastIndexOf(".") + 1)}
|
||||
onToggle={(isOpen) => setOpen(isOpen)}
|
||||
selections={
|
||||
typeof options[0] !== "string"
|
||||
? (options as SelectControlOption[])
|
||||
isSelectBasedOptions(options)
|
||||
? options
|
||||
.filter((o) =>
|
||||
Array.isArray(value)
|
||||
? value.includes(o.key)
|
||||
|
@ -104,11 +111,15 @@ export const SelectControl = <
|
|||
.map((o) => o.value)
|
||||
: value
|
||||
}
|
||||
onSelect={(_, v) => {
|
||||
if (variant === "typeaheadmulti") {
|
||||
onSelect={(event, v) => {
|
||||
event.stopPropagation();
|
||||
if (Array.isArray(value)) {
|
||||
const option = v.toString();
|
||||
if (value.includes(option)) {
|
||||
onChange(value.filter((item: string) => item !== option));
|
||||
const key = isSelectBasedOptions(options)
|
||||
? options.find((o) => o.value === option)?.key
|
||||
: option;
|
||||
if (value.includes(key)) {
|
||||
onChange(value.filter((item: string) => item !== key));
|
||||
} else {
|
||||
onChange([...value, option]);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue