diff --git a/js/apps/admin-ui/package.json b/js/apps/admin-ui/package.json index 70dd3ed24e..79488c6a95 100644 --- a/js/apps/admin-ui/package.json +++ b/js/apps/admin-ui/package.json @@ -78,6 +78,7 @@ "keycloak-js": "workspace:*", "lodash-es": "^4.17.21", "monaco-editor": "^0.47.0", + "p-debounce": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", diff --git a/js/apps/admin-ui/src/identity-providers/add/AddOpenIdConnect.tsx b/js/apps/admin-ui/src/identity-providers/add/AddOpenIdConnect.tsx index a651bbfd99..d56e15884a 100644 --- a/js/apps/admin-ui/src/identity-providers/add/AddOpenIdConnect.tsx +++ b/js/apps/admin-ui/src/identity-providers/add/AddOpenIdConnect.tsx @@ -33,6 +33,7 @@ export default function AddOpenIdConnect() { const form = useForm({ defaultValues: { alias: id }, + mode: "onChange", }); const { handleSubmit, diff --git a/js/apps/admin-ui/src/identity-providers/add/AddSamlConnect.tsx b/js/apps/admin-ui/src/identity-providers/add/AddSamlConnect.tsx index 5135445fb9..9bfd4ea044 100644 --- a/js/apps/admin-ui/src/identity-providers/add/AddSamlConnect.tsx +++ b/js/apps/admin-ui/src/identity-providers/add/AddSamlConnect.tsx @@ -30,6 +30,7 @@ export default function AddSamlConnect() { const form = useForm({ defaultValues: { alias: id, config: { allowCreate: "true" } }, + mode: "onChange", }); const { handleSubmit, diff --git a/js/apps/admin-ui/src/identity-providers/component/DiscoveryEndpointField.tsx b/js/apps/admin-ui/src/identity-providers/component/DiscoveryEndpointField.tsx index 291f294e48..8474e42b7e 100644 --- a/js/apps/admin-ui/src/identity-providers/component/DiscoveryEndpointField.tsx +++ b/js/apps/admin-ui/src/identity-providers/component/DiscoveryEndpointField.tsx @@ -1,11 +1,11 @@ import { FormGroup, Switch } from "@patternfly/react-core"; -import { ReactNode, useEffect, useState } from "react"; +import debouncePromise from "p-debounce"; +import { ReactNode, useMemo, useState } from "react"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { HelpItem } from "ui-shared"; +import { HelpItem, TextControl } from "ui-shared"; import { adminClient } from "../../admin-client"; -import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput"; import environment from "../../environment"; type DiscoveryEndpointFieldProps = { @@ -22,14 +22,9 @@ export const DiscoveryEndpointField = ({ const { t } = useTranslation(); const { setValue, - register, - setError, - watch, clearErrors, formState: { errors }, } = useFormContext(); - const discoveryUrl = watch("discoveryEndpoint"); - const [discovery, setDiscovery] = useState(true); const [discovering, setDiscovering] = useState(false); const [discoveryResult, setDiscoveryResult] = @@ -39,31 +34,23 @@ export const DiscoveryEndpointField = ({ Object.keys(result).map((k) => setValue(`config.${k}`, result[k])); }; - useEffect(() => { - if (!discoveryUrl) { + const discover = async (fromUrl: string) => { + setDiscovering(true); + try { + const result = await adminClient.identityProviders.importFromUrl({ + providerId: id, + fromUrl, + }); + setupForm(result); + setDiscoveryResult(result); + } catch (error) { + return (error as Error).message; + } finally { setDiscovering(false); - return; } + }; - (async () => { - clearErrors("discoveryError"); - try { - const result = await adminClient.identityProviders.importFromUrl({ - providerId: id, - fromUrl: discoveryUrl, - }); - setupForm(result); - setDiscoveryResult(result); - } catch (error) { - setError("discoveryError", { - type: "manual", - message: (error as Error).message, - }); - } - - setDiscovering(false); - })(); - }, [discovering]); + const discoverDebounced = useMemo(() => debouncePromise(discover, 1000), []); return ( <> @@ -98,20 +85,21 @@ export const DiscoveryEndpointField = ({ /> {discovery && ( - + labelIcon={t( + id === "oidc" + ? "discoveryEndpointHelp" + : "samlEntityDescriptorHelp", + )} + type="url" + placeholder={ + id === "oidc" + ? "https://hostname/auth/realms/master/.well-known/openid-configuration" + : "" } validated={ errors.discoveryError || errors.discoveryEndpoint @@ -120,42 +108,16 @@ export const DiscoveryEndpointField = ({ ? "default" : "success" } - helperTextInvalid={ - errors.discoveryEndpoint - ? t("required") - : t("noValidMetaDataFound", { - error: errors.discoveryError?.message, - }) + customIconUrl={ + discovering + ? environment.resourceUrl + "/discovery-load-indicator.svg" + : "" } - isRequired - > - setDiscovering(true), - })} - /> - + rules={{ + required: t("required"), + validate: (value: string) => discoverDebounced(value), + }} + /> )} {!discovery && fileUpload} {discovery && !errors.discoveryError && children(true)} diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index bb08a580be..2ae984fbdd 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -198,6 +198,9 @@ importers: monaco-editor: specifier: ^0.47.0 version: 0.47.0 + p-debounce: + specifier: ^4.0.0 + version: 4.0.0 react: specifier: ^18.2.0 version: 18.2.0 @@ -2183,6 +2186,10 @@ packages: resolution: {integrity: sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==} dev: true + /@types/debounce-promise@3.1.9: + resolution: {integrity: sha512-awNxydYSU+E2vL7EiOAMtiSOfL5gUM5X4YSE2A92qpxDJQ/rXz6oMPYBFDcDywlUmvIDI6zsqgq17cGm5CITQw==} + dev: false + /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true @@ -2954,6 +2961,28 @@ packages: possible-typed-array-names: 1.0.0 dev: true + /awesome-debounce-promise@2.1.0: + resolution: {integrity: sha512-0Dv4j2wKk5BrNZh4jgV2HUdznaeVgEK/WTvcHhZWUElhmQ1RR+iURRoLEwICFyR0S/5VtxfcvY6gR+qSe95jNg==} + engines: {node: '>=8', npm: '>=5'} + dependencies: + '@types/debounce-promise': 3.1.9 + awesome-imperative-promise: 1.0.1 + awesome-only-resolves-last-promise: 1.0.3 + debounce-promise: 3.1.2 + dev: false + + /awesome-imperative-promise@1.0.1: + resolution: {integrity: sha512-EmPr3FqbQGqlNh+WxMNcF9pO9uDQJnOC4/3rLBQNH9m4E9qI+8lbfHCmHpVAsmGqPJPKhCjJLHUQzQW/RBHRdQ==} + engines: {node: '>=8', npm: '>=5'} + dev: false + + /awesome-only-resolves-last-promise@1.0.3: + resolution: {integrity: sha512-7q4WPsYiD8Omvi/yHL314DkvsD/lM//Z2/KcU1vWk0xJotiV0GMJTgHTpWl3n90HJqpXKg7qX+VVNs5YbQyPRQ==} + engines: {node: '>=8', npm: '>=5'} + dependencies: + awesome-imperative-promise: 1.0.1 + dev: false + /aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} dev: true @@ -3633,6 +3662,10 @@ packages: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} dev: true + /debounce-promise@3.1.2: + resolution: {integrity: sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==} + dev: false + /debug@3.2.7(supports-color@8.1.1): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -6463,6 +6496,11 @@ packages: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} dev: true + /p-debounce@4.0.0: + resolution: {integrity: sha512-4Ispi9I9qYGO4lueiLDhe4q4iK5ERK8reLsuzH6BPaXn53EGaua8H66PXIFGrW897hwjXp+pVLrm/DLxN0RF0A==} + engines: {node: '>=12'} + dev: false + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -8709,6 +8747,7 @@ packages: '@patternfly/react-icons': 4.93.7(react-dom@18.2.0)(react@18.2.0) '@patternfly/react-styles': 4.92.8 '@patternfly/react-table': 4.113.6(react-dom@18.2.0)(react@18.2.0) + awesome-debounce-promise: 2.1.0 dagre: 0.8.5 file-saver: 2.0.5 file-selector: 0.6.0