put shared logic into common component + fixes (#1415)
* put shared logic into common component + fixes fixes: #1398 * removed conflicted name * code review * pr review comments * update admin-client for better retrun types import * use image file instead of encoded data:uri * fix package lock * is typechecked to be string
This commit is contained in:
parent
79f8452ab5
commit
48a006e1fa
11 changed files with 411 additions and 435 deletions
|
@ -28,8 +28,8 @@ describe("Identity provider test", () => {
|
|||
|
||||
const keycloakServer = Cypress.env("KEYCLOAK_SERVER");
|
||||
const discoveryUrl = `${keycloakServer}/auth/realms/master/.well-known/openid-configuration`;
|
||||
const samlDiscoveryUrl = `${keycloakServer}/auth/realms/master/protocol/saml/descriptor`;
|
||||
const authorizationUrl = `${keycloakServer}/auth/realms/master/protocol/openid-connect/auth`;
|
||||
const ssoServiceUrl = `${keycloakServer}/auth/realms/sso`;
|
||||
|
||||
describe("Identity provider creation", () => {
|
||||
const identityProviderName = "github";
|
||||
|
@ -112,8 +112,8 @@ describe("Identity provider test", () => {
|
|||
createProviderPage
|
||||
.clickCreateDropdown()
|
||||
.clickItem(samlProviderName)
|
||||
.toggleEntityDescriptor()
|
||||
.fillSsoServiceUrl(ssoServiceUrl)
|
||||
.fillDiscoveryUrl(samlDiscoveryUrl)
|
||||
.shouldBeSuccessful()
|
||||
.clickAdd();
|
||||
masthead.checkNotificationMessage(createSuccessMsg);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@ export default class CreateProviderPage {
|
|||
private clientSecretField = "clientSecret";
|
||||
private discoveryEndpoint = "discoveryEndpoint";
|
||||
private authorizationUrl = "authorizationUrl";
|
||||
private useEntityDescriptorSwitch = "useEntityDescriptor";
|
||||
private addButton = "createProvider";
|
||||
private ssoServiceUrl = "sso-service-url";
|
||||
|
||||
|
@ -96,9 +95,4 @@ export default class CreateProviderPage {
|
|||
cy.findByTestId(this.authorizationUrl).should("have.value", value);
|
||||
return this;
|
||||
}
|
||||
|
||||
toggleEntityDescriptor() {
|
||||
cy.findByTestId(this.useEntityDescriptorSwitch).click({ force: true });
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -7,7 +7,7 @@
|
|||
"name": "keycloak-admin-ui",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@keycloak/keycloak-admin-client": "^16.0.0-dev.34",
|
||||
"@keycloak/keycloak-admin-client": "^16.0.0-dev.37",
|
||||
"@patternfly/patternfly": "^4.144.5",
|
||||
"@patternfly/react-code-editor": "^4.3.85",
|
||||
"@patternfly/react-core": "^4.162.3",
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@keycloak/keycloak-admin-client": "^16.0.0-dev.34",
|
||||
"@keycloak/keycloak-admin-client": "^16.0.0-dev.37",
|
||||
"@patternfly/patternfly": "^4.144.5",
|
||||
"@patternfly/react-code-editor": "^4.3.85",
|
||||
"@patternfly/react-core": "^4.162.3",
|
||||
|
|
1
public/discovery-load-indicator.svg
Normal file
1
public/discovery-load-indicator.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><circle cx="50" cy="50" fill="none" stroke="#0066cc" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform></circle></svg>
|
After Width: | Height: | Size: 438 B |
159
src/components/json-file-upload/FileUploadForm.tsx
Normal file
159
src/components/json-file-upload/FileUploadForm.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
FormGroup,
|
||||
FileUpload,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Button,
|
||||
FileUploadProps,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CodeEditor, Language } from "@patternfly/react-code-editor";
|
||||
|
||||
type FileUploadType = {
|
||||
value: string;
|
||||
filename: string;
|
||||
isLoading: boolean;
|
||||
modal: boolean;
|
||||
};
|
||||
|
||||
export type FileUploadEvent =
|
||||
| React.DragEvent<HTMLElement> // User dragged/dropped a file
|
||||
| React.ChangeEvent<HTMLTextAreaElement> // User typed in the TextArea
|
||||
| React.MouseEvent<HTMLButtonElement, MouseEvent>; // User clicked Clear button
|
||||
|
||||
export type FileUploadFormProps = Omit<FileUploadProps, "onChange"> & {
|
||||
id: string;
|
||||
extension: string;
|
||||
onChange: (value: string) => void;
|
||||
helpText?: string;
|
||||
unWrap?: boolean;
|
||||
language?: Language;
|
||||
};
|
||||
|
||||
export const FileUploadForm = ({
|
||||
id,
|
||||
onChange,
|
||||
helpText = "common-help:helpFileUpload",
|
||||
unWrap = false,
|
||||
language,
|
||||
extension,
|
||||
...rest
|
||||
}: FileUploadFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultUpload: FileUploadType = {
|
||||
value: "",
|
||||
filename: "",
|
||||
isLoading: false,
|
||||
modal: false,
|
||||
};
|
||||
const [fileUpload, setFileUpload] = useState<FileUploadType>(defaultUpload);
|
||||
const removeDialog = () => setFileUpload({ ...fileUpload, modal: false });
|
||||
const handleChange = (
|
||||
value: string | File,
|
||||
filename: string,
|
||||
event:
|
||||
| React.DragEvent<HTMLElement>
|
||||
| React.ChangeEvent<HTMLTextAreaElement>
|
||||
| React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
): void => {
|
||||
if (
|
||||
event.nativeEvent instanceof MouseEvent &&
|
||||
!(event.nativeEvent instanceof DragEvent)
|
||||
) {
|
||||
setFileUpload({ ...fileUpload, modal: true });
|
||||
} else {
|
||||
setFileUpload({
|
||||
...fileUpload,
|
||||
value: value.toString(),
|
||||
filename,
|
||||
});
|
||||
|
||||
onChange(value.toString());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileUpload.modal && (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("clearFile")}
|
||||
isOpen
|
||||
onClose={removeDialog}
|
||||
actions={[
|
||||
<Button
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setFileUpload(defaultUpload);
|
||||
onChange("");
|
||||
}}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>,
|
||||
<Button key="cancel" variant="link" onClick={removeDialog}>
|
||||
{t("cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{t("clearFileExplain")}
|
||||
</Modal>
|
||||
)}
|
||||
{unWrap && (
|
||||
<FileUpload
|
||||
id={id}
|
||||
{...rest}
|
||||
type="text"
|
||||
value={fileUpload.value}
|
||||
filename={fileUpload.filename}
|
||||
onChange={handleChange}
|
||||
onReadStarted={() =>
|
||||
setFileUpload({ ...fileUpload, isLoading: true })
|
||||
}
|
||||
onReadFinished={() =>
|
||||
setFileUpload({ ...fileUpload, isLoading: false })
|
||||
}
|
||||
isLoading={fileUpload.isLoading}
|
||||
dropzoneProps={{
|
||||
accept: extension,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!unWrap && (
|
||||
<FormGroup
|
||||
label={t("resourceFile")}
|
||||
fieldId={id}
|
||||
helperText={t(helpText)}
|
||||
>
|
||||
<FileUpload
|
||||
id={id}
|
||||
{...rest}
|
||||
type="text"
|
||||
value={fileUpload.value}
|
||||
filename={fileUpload.filename}
|
||||
onChange={handleChange}
|
||||
onReadStarted={() =>
|
||||
setFileUpload({ ...fileUpload, isLoading: true })
|
||||
}
|
||||
onReadFinished={() =>
|
||||
setFileUpload({ ...fileUpload, isLoading: false })
|
||||
}
|
||||
isLoading={fileUpload.isLoading}
|
||||
hideDefaultPreview
|
||||
>
|
||||
<CodeEditor
|
||||
isLineNumbersVisible
|
||||
code={fileUpload.value}
|
||||
language={language}
|
||||
height="128px"
|
||||
onChange={(value, event) =>
|
||||
handleChange(value ?? "", fileUpload.filename, event)
|
||||
}
|
||||
/>
|
||||
</FileUpload>
|
||||
</FormGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,167 +1,31 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
FormGroup,
|
||||
FileUpload,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Button,
|
||||
FileUploadProps,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CodeEditor, Language } from "@patternfly/react-code-editor";
|
||||
import React from "react";
|
||||
import { Language } from "@patternfly/react-code-editor";
|
||||
|
||||
type FileUpload = {
|
||||
value: string;
|
||||
filename: string;
|
||||
isLoading: boolean;
|
||||
modal: boolean;
|
||||
};
|
||||
import { FileUploadForm, FileUploadFormProps } from "./FileUploadForm";
|
||||
|
||||
export type JsonFileUploadEvent =
|
||||
| React.DragEvent<HTMLElement> // User dragged/dropped a file
|
||||
| React.ChangeEvent<HTMLTextAreaElement> // User typed in the TextArea
|
||||
| React.MouseEvent<HTMLButtonElement, MouseEvent>; // User clicked Clear button
|
||||
|
||||
export type JsonFileUploadProps = Omit<FileUploadProps, "onChange"> & {
|
||||
id: string;
|
||||
export type JsonFileUploadProps = Omit<
|
||||
FileUploadFormProps,
|
||||
"onChange" | "language" | "extension"
|
||||
> & {
|
||||
onChange: (obj: object) => void;
|
||||
helpText?: string;
|
||||
unWrap?: boolean;
|
||||
};
|
||||
|
||||
export const JsonFileUpload = ({
|
||||
id,
|
||||
onChange,
|
||||
helpText = "common-help:helpFileUpload",
|
||||
unWrap = false,
|
||||
...rest
|
||||
}: JsonFileUploadProps) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultUpload = {
|
||||
value: "",
|
||||
filename: "",
|
||||
isLoading: false,
|
||||
modal: false,
|
||||
};
|
||||
const [fileUpload, setFileUpload] = useState<FileUpload>(defaultUpload);
|
||||
const removeDialog = () => setFileUpload({ ...fileUpload, modal: false });
|
||||
const handleChange = (
|
||||
value: string | File,
|
||||
filename: string,
|
||||
event:
|
||||
| React.DragEvent<HTMLElement>
|
||||
| React.ChangeEvent<HTMLTextAreaElement>
|
||||
| React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
): void => {
|
||||
if (
|
||||
event.nativeEvent instanceof MouseEvent &&
|
||||
!(event.nativeEvent instanceof DragEvent)
|
||||
) {
|
||||
setFileUpload({ ...fileUpload, modal: true });
|
||||
} else {
|
||||
setFileUpload({
|
||||
...fileUpload,
|
||||
value: value as string,
|
||||
filename,
|
||||
});
|
||||
|
||||
if (value) {
|
||||
let obj = {};
|
||||
export const JsonFileUpload = ({ onChange, ...props }: JsonFileUploadProps) => {
|
||||
const handleChange = (value: string) => {
|
||||
try {
|
||||
obj = JSON.parse(value as string);
|
||||
onChange(JSON.parse(value));
|
||||
} catch (error) {
|
||||
onChange({});
|
||||
console.warn("Invalid json, ignoring value using {}");
|
||||
}
|
||||
|
||||
onChange(obj);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileUpload.modal && (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("clearFile")}
|
||||
isOpen
|
||||
onClose={removeDialog}
|
||||
actions={[
|
||||
<Button
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setFileUpload(defaultUpload);
|
||||
onChange({});
|
||||
}}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>,
|
||||
<Button key="cancel" variant="link" onClick={removeDialog}>
|
||||
{t("cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{t("clearFileExplain")}
|
||||
</Modal>
|
||||
)}
|
||||
{unWrap && (
|
||||
<FileUpload
|
||||
id={id}
|
||||
{...rest}
|
||||
type="text"
|
||||
value={fileUpload.value}
|
||||
filename={fileUpload.filename}
|
||||
onChange={handleChange}
|
||||
onReadStarted={() =>
|
||||
setFileUpload({ ...fileUpload, isLoading: true })
|
||||
}
|
||||
onReadFinished={() =>
|
||||
setFileUpload({ ...fileUpload, isLoading: false })
|
||||
}
|
||||
isLoading={fileUpload.isLoading}
|
||||
dropzoneProps={{
|
||||
accept: ".json",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!unWrap && (
|
||||
<FormGroup
|
||||
label={t("resourceFile")}
|
||||
fieldId={id}
|
||||
helperText={t(helpText)}
|
||||
>
|
||||
<FileUpload
|
||||
id={id}
|
||||
{...rest}
|
||||
type="text"
|
||||
value={fileUpload.value}
|
||||
filename={fileUpload.filename}
|
||||
onChange={handleChange}
|
||||
onReadStarted={() =>
|
||||
setFileUpload({ ...fileUpload, isLoading: true })
|
||||
}
|
||||
onReadFinished={() =>
|
||||
setFileUpload({ ...fileUpload, isLoading: false })
|
||||
}
|
||||
isLoading={fileUpload.isLoading}
|
||||
dropzoneProps={{
|
||||
accept: ".json",
|
||||
}}
|
||||
hideDefaultPreview
|
||||
>
|
||||
<CodeEditor
|
||||
isLineNumbersVisible
|
||||
code={fileUpload.value}
|
||||
<FileUploadForm
|
||||
{...props}
|
||||
language={Language.json}
|
||||
height="128px"
|
||||
onChange={(value, event) =>
|
||||
handleChange(value ?? "", fileUpload.filename, event)
|
||||
}
|
||||
extension=".json"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FileUpload>
|
||||
</FormGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,12 +18,16 @@ import { SamlConnectSettings } from "./SamlConnectSettings";
|
|||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
|
||||
type DiscoveryIdentityProvider = IdentityProviderRepresentation & {
|
||||
discoveryEndpoint?: string;
|
||||
};
|
||||
|
||||
export default function AddSamlConnect() {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
const history = useHistory();
|
||||
const id = "saml";
|
||||
|
||||
const form = useForm<IdentityProviderRepresentation>({
|
||||
const form = useForm<DiscoveryIdentityProvider>({
|
||||
defaultValues: { alias: id },
|
||||
});
|
||||
const {
|
||||
|
@ -35,7 +39,8 @@ export default function AddSamlConnect() {
|
|||
const { addAlert } = useAlerts();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const save = async (provider: IdentityProviderRepresentation) => {
|
||||
const save = async (provider: DiscoveryIdentityProvider) => {
|
||||
delete provider.discoveryEndpoint;
|
||||
try {
|
||||
await adminClient.identityProviders.create({
|
||||
...provider,
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { FormGroup, Switch, TextInput, Title } from "@patternfly/react-core";
|
||||
import { FormGroup, Title } from "@patternfly/react-core";
|
||||
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
import type { OIDCConfigurationRepresentation } from "../OIDCConfigurationRepresentation";
|
||||
import { JsonFileUpload } from "../../components/json-file-upload/JsonFileUpload";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { DiscoverySettings } from "./DiscoverySettings";
|
||||
import { getBaseUrl } from "../../util";
|
||||
|
||||
type Result = OIDCConfigurationRepresentation & {
|
||||
error: string;
|
||||
};
|
||||
import { DiscoveryEndpointField } from "../component/DiscoveryEndpointField";
|
||||
|
||||
export const OpenIdConnectSettings = () => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
|
@ -21,40 +17,13 @@ export const OpenIdConnectSettings = () => {
|
|||
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
const { setValue, register, errors } = useFormContext();
|
||||
|
||||
const [discovery, setDiscovery] = useState(true);
|
||||
const [discoveryUrl, setDiscoveryUrl] = useState("");
|
||||
const [discovering, setDiscovering] = useState(false);
|
||||
const [discoveryResult, setDiscoveryResult] = useState<Result>();
|
||||
const { setValue, errors, setError } = useFormContext();
|
||||
|
||||
const setupForm = (result: any) => {
|
||||
Object.keys(result).map((k) => setValue(`config.${k}`, result[k]));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (discovering) {
|
||||
setDiscovering(!!discoveryUrl);
|
||||
if (discoveryUrl)
|
||||
(async () => {
|
||||
let result;
|
||||
try {
|
||||
result = await adminClient.identityProviders.importFromUrl({
|
||||
providerId: id,
|
||||
fromUrl: discoveryUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
result = { error };
|
||||
}
|
||||
|
||||
setDiscoveryResult(result as Result);
|
||||
setupForm(result);
|
||||
setDiscovering(false);
|
||||
})();
|
||||
}
|
||||
}, [discovering]);
|
||||
|
||||
const fileUpload = async (obj: object) => {
|
||||
const fileUpload = async (obj?: object) => {
|
||||
if (obj) {
|
||||
const formData = new FormData();
|
||||
formData.append("providerId", id);
|
||||
|
@ -75,8 +44,11 @@ export const OpenIdConnectSettings = () => {
|
|||
);
|
||||
const result = await response.json();
|
||||
setupForm(result);
|
||||
} catch (error: any) {
|
||||
setDiscoveryResult({ error });
|
||||
} catch (error) {
|
||||
setError("discoveryError", {
|
||||
type: "manual",
|
||||
message: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -86,76 +58,10 @@ export const OpenIdConnectSettings = () => {
|
|||
<Title headingLevel="h4" size="xl" className="kc-form-panel__title">
|
||||
{t("oidcSettings")}
|
||||
</Title>
|
||||
<FormGroup
|
||||
label={t("useDiscoveryEndpoint")}
|
||||
fieldId="kc-discovery-endpoint-switch"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:useDiscoveryEndpoint"
|
||||
forLabel={t("useDiscoveryEndpoint")}
|
||||
forID="kc-discovery-endpoint-switch"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
id="kc-discovery-endpoint-switch"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={discovery}
|
||||
onChange={setDiscovery}
|
||||
/>
|
||||
</FormGroup>
|
||||
{discovery && (
|
||||
<FormGroup
|
||||
label={t("discoveryEndpoint")}
|
||||
fieldId="kc-discovery-endpoint"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:discoveryEndpoint"
|
||||
forLabel={t("discoveryEndpoint")}
|
||||
forID="kc-discovery-endpoint"
|
||||
/>
|
||||
}
|
||||
validated={
|
||||
discoveryResult?.error || errors.discoveryEndpoint
|
||||
? "error"
|
||||
: !discoveryResult
|
||||
? "default"
|
||||
: "success"
|
||||
}
|
||||
helperTextInvalid={
|
||||
errors.discoveryEndpoint
|
||||
? t("common:required")
|
||||
: t("noValidMetaDataFound")
|
||||
}
|
||||
isRequired
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
name="discoveryEndpoint"
|
||||
data-testid="discoveryEndpoint"
|
||||
id="kc-discovery-endpoint"
|
||||
placeholder="https://hostname/auth/realms/master/.well-known/openid-configuration"
|
||||
value={discoveryUrl}
|
||||
onChange={setDiscoveryUrl}
|
||||
onBlur={() => setDiscovering(!discovering)}
|
||||
validated={
|
||||
discoveryResult?.error || errors.discoveryEndpoint
|
||||
? "error"
|
||||
: !discoveryResult
|
||||
? "default"
|
||||
: "success"
|
||||
}
|
||||
customIconUrl={
|
||||
discovering
|
||||
? 'data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"%3E%3Ccircle cx="50" cy="50" fill="none" stroke="%230066cc" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138"%3E%3CanimateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"%3E%3C/animateTransform%3E%3C/circle%3E%3C/svg%3E'
|
||||
: ""
|
||||
}
|
||||
ref={register({ required: true })}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{!discovery && (
|
||||
|
||||
<DiscoveryEndpointField
|
||||
id="oidc"
|
||||
fileUpload={
|
||||
<FormGroup
|
||||
label={t("importConfig")}
|
||||
fieldId="kc-import-config"
|
||||
|
@ -166,23 +72,22 @@ export const OpenIdConnectSettings = () => {
|
|||
forID="kc-import-config"
|
||||
/>
|
||||
}
|
||||
validated={discoveryResult?.error ? "error" : "default"}
|
||||
helperTextInvalid={discoveryResult?.error?.toString()}
|
||||
validated={errors.discoveryError ? "error" : "default"}
|
||||
helperTextInvalid={errors.discoveryError}
|
||||
>
|
||||
<JsonFileUpload
|
||||
id="kc-import-config"
|
||||
helpText="identity=providers-help:jsonFileUpload"
|
||||
hideDefaultPreview
|
||||
unWrap
|
||||
validated={discoveryResult?.error ? "error" : "default"}
|
||||
validated={errors.discoveryError ? "error" : "default"}
|
||||
onChange={(value) => fileUpload(value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{discovery && discoveryResult && !discoveryResult.error && (
|
||||
<DiscoverySettings readOnly={true} />
|
||||
)}
|
||||
{!discovery && <DiscoverySettings readOnly={false} />}
|
||||
}
|
||||
>
|
||||
{(readonly) => <DiscoverySettings readOnly={readonly} />}
|
||||
</DiscoveryEndpointField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,26 +1,17 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import {
|
||||
FormGroup,
|
||||
Switch,
|
||||
TextInput,
|
||||
Title,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { FormGroup, TextInput, Title } from "@patternfly/react-core";
|
||||
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
|
||||
|
||||
import { JsonFileUpload } from "../../components/json-file-upload/JsonFileUpload";
|
||||
import { FileUploadForm } from "../../components/json-file-upload/FileUploadForm";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { DescriptorSettings } from "./DescriptorSettings";
|
||||
import { getBaseUrl } from "../../util";
|
||||
|
||||
type Result = IdentityProviderRepresentation & {
|
||||
error: string;
|
||||
};
|
||||
import { DiscoveryEndpointField } from "../component/DiscoveryEndpointField";
|
||||
|
||||
export const SamlConnectSettings = () => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
|
@ -28,16 +19,7 @@ export const SamlConnectSettings = () => {
|
|||
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
const { setValue, register, errors } = useFormContext();
|
||||
|
||||
const [descriptor, setDescriptor] = useState(true);
|
||||
|
||||
const [entityUrl, setEntityUrl] = useState("");
|
||||
const [descriptorUrl, setDescriptorUrl] = useState("");
|
||||
const [discovering, setDiscovering] = useState(false);
|
||||
const [discoveryResult, setDiscoveryResult] = useState<Result>();
|
||||
|
||||
const defaultEntityUrl = `${getBaseUrl(adminClient)}realms/${realm}`;
|
||||
const { setValue, register, errors, setError } = useFormContext();
|
||||
|
||||
const setupForm = (result: IdentityProviderRepresentation) => {
|
||||
Object.entries(result).map(([key, value]) =>
|
||||
|
@ -45,38 +27,10 @@ export const SamlConnectSettings = () => {
|
|||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!discovering) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDiscovering(!!entityUrl);
|
||||
|
||||
if (!entityUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
let result;
|
||||
try {
|
||||
result = await adminClient.identityProviders.importFromUrl({
|
||||
providerId: id,
|
||||
fromUrl: entityUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
result = { error };
|
||||
}
|
||||
|
||||
setDiscoveryResult(result as Result);
|
||||
setupForm(result);
|
||||
setDiscovering(false);
|
||||
})();
|
||||
}, [discovering]);
|
||||
|
||||
const fileUpload = async (obj: object) => {
|
||||
const fileUpload = async (xml: string) => {
|
||||
const formData = new FormData();
|
||||
formData.append("providerId", id);
|
||||
formData.append("file", new Blob([JSON.stringify(obj)]));
|
||||
formData.append("file", new Blob([xml]));
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
|
@ -93,8 +47,11 @@ export const SamlConnectSettings = () => {
|
|||
);
|
||||
const result = await response.json();
|
||||
setupForm(result);
|
||||
} catch (error: any) {
|
||||
setDiscoveryResult({ error });
|
||||
} catch (error) {
|
||||
setError("discoveryError", {
|
||||
type: "manual",
|
||||
message: (error as Error).message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -120,62 +77,13 @@ export const SamlConnectSettings = () => {
|
|||
name="config.entityId"
|
||||
data-testid="serviceProviderEntityId"
|
||||
id="kc-service-provider-entity-id"
|
||||
value={entityUrl || defaultEntityUrl}
|
||||
onChange={setEntityUrl}
|
||||
ref={register()}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={t("useEntityDescriptor")}
|
||||
fieldId="kc-use-entity-descriptor"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:useEntityDescriptor"
|
||||
forLabel={t("useEntityDescriptor")}
|
||||
forID="kc-use-entity-descriptor-switch"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
id="kc-use-entity-descriptor-switch"
|
||||
label={t("common:on")}
|
||||
data-testid="useEntityDescriptor"
|
||||
labelOff={t("common:off")}
|
||||
isChecked={descriptor}
|
||||
onChange={setDescriptor}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{descriptor && (
|
||||
<FormGroup
|
||||
label={t("samlEntityDescriptor")}
|
||||
fieldId="kc-saml-entity-descriptor"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:samlEntityDescriptor"
|
||||
forLabel={t("samlEntityDescriptor")}
|
||||
forID="kc-saml-entity-descriptor"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
name="samlEntityDescriptor"
|
||||
data-testid="samlEntityDescriptor"
|
||||
id="kc-saml-entity-descriptor"
|
||||
value={descriptorUrl}
|
||||
onChange={setDescriptorUrl}
|
||||
ref={register()}
|
||||
validated={
|
||||
errors.samlEntityDescriptor
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{!descriptor && (
|
||||
<DiscoveryEndpointField
|
||||
id="saml"
|
||||
fileUpload={
|
||||
<FormGroup
|
||||
label={t("importConfig")}
|
||||
fieldId="kc-import-config"
|
||||
|
@ -186,24 +94,22 @@ export const SamlConnectSettings = () => {
|
|||
forID="kc-import-config"
|
||||
/>
|
||||
}
|
||||
validated={discoveryResult?.error ? "error" : "default"}
|
||||
helperTextInvalid={discoveryResult?.error.toString()}
|
||||
validated={errors.discoveryError ? "error" : "default"}
|
||||
helperTextInvalid={errors.discoveryError}
|
||||
>
|
||||
<JsonFileUpload
|
||||
<FileUploadForm
|
||||
id="kc-import-config"
|
||||
helpText="identity-providers-help:jsonFileUpload"
|
||||
extension=".xml"
|
||||
hideDefaultPreview
|
||||
unWrap
|
||||
validated={discoveryResult?.error ? "error" : "default"}
|
||||
validated={errors.discoveryError ? "error" : "default"}
|
||||
onChange={(value) => fileUpload(value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
{descriptor && discoveryResult && !discoveryResult.error && (
|
||||
<DescriptorSettings readOnly={true} />
|
||||
)}
|
||||
{!descriptor && <DescriptorSettings readOnly={false} />}
|
||||
}
|
||||
>
|
||||
{(readonly) => <DescriptorSettings readOnly={readonly} />}
|
||||
</DiscoveryEndpointField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
142
src/identity-providers/component/DiscoveryEndpointField.tsx
Normal file
142
src/identity-providers/component/DiscoveryEndpointField.tsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { FormGroup, TextInput, Switch } from "@patternfly/react-core";
|
||||
|
||||
import environment from "../../environment";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
|
||||
type DiscoveryEndpointFieldProps = {
|
||||
id: string;
|
||||
fileUpload: ReactNode;
|
||||
children: (readOnly: boolean) => ReactNode;
|
||||
};
|
||||
|
||||
export const DiscoveryEndpointField = ({
|
||||
id,
|
||||
fileUpload,
|
||||
children,
|
||||
}: DiscoveryEndpointFieldProps) => {
|
||||
const { t } = useTranslation("identity-providers");
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
|
||||
const { setValue, register, errors, setError, watch, clearErrors } =
|
||||
useFormContext();
|
||||
const discoveryUrl = watch("discoveryEndpoint");
|
||||
|
||||
const [discovery, setDiscovery] = useState(true);
|
||||
const [discovering, setDiscovering] = useState(false);
|
||||
const [discoveryResult, setDiscoveryResult] =
|
||||
useState<Record<string, string>>();
|
||||
|
||||
const setupForm = (result: Record<string, string>) => {
|
||||
Object.keys(result).map((k) => setValue(`config.${k}`, result[k]));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!discoveryUrl) {
|
||||
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]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup
|
||||
label={t("useDiscoveryEndpoint")}
|
||||
fieldId="kc-discovery-endpoint-switch"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:useDiscoveryEndpoint"
|
||||
forLabel={t("useDiscoveryEndpoint")}
|
||||
forID="kc-discovery-endpoint-switch"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
id="kc-discovery-endpoint-switch"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={discovery}
|
||||
onChange={setDiscovery}
|
||||
/>
|
||||
</FormGroup>
|
||||
{discovery && (
|
||||
<FormGroup
|
||||
label={t("discoveryEndpoint")}
|
||||
fieldId="kc-discovery-endpoint"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="identity-providers-help:discoveryEndpoint"
|
||||
forLabel={t("discoveryEndpoint")}
|
||||
forID="kc-discovery-endpoint"
|
||||
/>
|
||||
}
|
||||
validated={
|
||||
errors.discoveryError || errors.discoveryEndpoint
|
||||
? "error"
|
||||
: !discoveryResult
|
||||
? "default"
|
||||
: "success"
|
||||
}
|
||||
helperTextInvalid={
|
||||
errors.discoveryEndpoint
|
||||
? t("common:required")
|
||||
: t("noValidMetaDataFound")
|
||||
}
|
||||
isRequired
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
name="discoveryEndpoint"
|
||||
data-testid="discoveryEndpoint"
|
||||
id="kc-discovery-endpoint"
|
||||
placeholder={
|
||||
id === "oidc"
|
||||
? "https://hostname/auth/realms/master/.well-known/openid-configuration"
|
||||
: "https://hostname/context/saml/discovery"
|
||||
}
|
||||
onBlur={() => setDiscovering(true)}
|
||||
validated={
|
||||
errors.discoveryError || errors.discoveryEndpoint
|
||||
? "error"
|
||||
: !discoveryResult
|
||||
? "default"
|
||||
: "success"
|
||||
}
|
||||
customIconUrl={
|
||||
discovering
|
||||
? environment.resourceUrl + "./discovery-load-indicator.svg"
|
||||
: ""
|
||||
}
|
||||
ref={register({ required: true })}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{!discovery && fileUpload}
|
||||
{discovery && !errors.discoveryError && children(true)}
|
||||
{!discovery && children(false)}
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Reference in a new issue