Fixed issue using import (#393)
* add icons change description to use TextArea * add missing saml config * switch based on protocol * add capabilty config to import * fixed key element warning * fixed merge error * don't clear on file drag event * don't allow editing of json * prettier * fix tests * missing timeout
This commit is contained in:
parent
4ba5fcc723
commit
7bf85196e3
11 changed files with 281 additions and 156 deletions
|
@ -1,7 +1,14 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormGroup, TextInput, ValidatedOptions } from "@patternfly/react-core";
|
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
TextArea,
|
||||||
|
TextInput,
|
||||||
|
ValidatedOptions,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
import { FormAccess } from "../components/form-access/FormAccess";
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
import { ClientForm } from "./ClientDetails";
|
import { ClientForm } from "./ClientDetails";
|
||||||
|
@ -12,6 +19,13 @@ export const ClientDescription = () => {
|
||||||
return (
|
return (
|
||||||
<FormAccess role="manage-clients" unWrap>
|
<FormAccess role="manage-clients" unWrap>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:clientID"
|
||||||
|
forLabel={t("clientID")}
|
||||||
|
forID="kc-client-id"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={t("clientID")}
|
label={t("clientID")}
|
||||||
fieldId="kc-client-id"
|
fieldId="kc-client-id"
|
||||||
helperTextInvalid={t("common:required")}
|
helperTextInvalid={t("common:required")}
|
||||||
|
@ -30,10 +44,27 @@ export const ClientDescription = () => {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup label={t("common:name")} fieldId="kc-name">
|
<FormGroup
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:clientName"
|
||||||
|
forLabel={t("common:name")}
|
||||||
|
forID="kc-name"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("common:name")}
|
||||||
|
fieldId="kc-name"
|
||||||
|
>
|
||||||
<TextInput ref={register()} type="text" id="kc-name" name="name" />
|
<TextInput ref={register()} type="text" id="kc-name" name="name" />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:description"
|
||||||
|
forLabel={t("common:description")}
|
||||||
|
forID="kc-description"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={t("common:description")}
|
label={t("common:description")}
|
||||||
fieldId="kc-description"
|
fieldId="kc-description"
|
||||||
validated={
|
validated={
|
||||||
|
@ -41,7 +72,7 @@ export const ClientDescription = () => {
|
||||||
}
|
}
|
||||||
helperTextInvalid={errors.description?.message}
|
helperTextInvalid={errors.description?.message}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextArea
|
||||||
ref={register({
|
ref={register({
|
||||||
maxLength: {
|
maxLength: {
|
||||||
value: 255,
|
value: 255,
|
||||||
|
|
|
@ -74,7 +74,7 @@ export const ClientsSection = () => {
|
||||||
<Link key={client.id} to={`/${realm}/clients/${client.id}/settings`}>
|
<Link key={client.id} to={`/${realm}/clients/${client.id}/settings`}>
|
||||||
{client.clientId}
|
{client.clientId}
|
||||||
{!client.enabled && (
|
{!client.enabled && (
|
||||||
<Badge isRead className="pf-u-ml-sm">
|
<Badge key={`${client.id}-disabled`} isRead className="pf-u-ml-sm">
|
||||||
Disabled
|
Disabled
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -8,15 +8,28 @@ import {
|
||||||
GridItem,
|
GridItem,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||||
import { ClientForm } from "../ClientDetails";
|
import { ClientForm } from "../ClientDetails";
|
||||||
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
export const CapabilityConfig = () => {
|
type CapabilityConfigProps = {
|
||||||
|
unWrap?: boolean;
|
||||||
|
protocol?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CapabilityConfig = ({
|
||||||
|
unWrap,
|
||||||
|
protocol: type,
|
||||||
|
}: CapabilityConfigProps) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const { control } = useFormContext<ClientForm>();
|
const { control, watch } = useFormContext<ClientForm>();
|
||||||
|
const protocol = type || watch("protocol");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormAccess isHorizontal role="manage-clients">
|
<FormAccess isHorizontal role="manage-clients" unWrap={unWrap}>
|
||||||
|
<>
|
||||||
|
{protocol === "openid-connect" && (
|
||||||
|
<>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
hasNoPaddingTop
|
hasNoPaddingTop
|
||||||
label={t("clientAuthentication")}
|
label={t("clientAuthentication")}
|
||||||
|
@ -131,6 +144,67 @@ export const CapabilityConfig = () => {
|
||||||
</GridItem>
|
</GridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
<>
|
||||||
|
{protocol === "saml" && (
|
||||||
|
<>
|
||||||
|
<FormGroup
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:encryptAssertions"
|
||||||
|
forLabel={t("encryptAssertions")}
|
||||||
|
forID="kc-encrypt"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("encryptAssertions")}
|
||||||
|
fieldId="kc-encrypt"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="attributes.saml_encrypt"
|
||||||
|
control={control}
|
||||||
|
defaultValue="false"
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Switch
|
||||||
|
id="kc-encrypt"
|
||||||
|
label={t("common:on")}
|
||||||
|
labelOff={t("common:off")}
|
||||||
|
isChecked={value === "true"}
|
||||||
|
onChange={(value) => onChange("" + value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:clientSignature"
|
||||||
|
forLabel={t("clientSignature")}
|
||||||
|
forID="kc-client-signature"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("clientSignature")}
|
||||||
|
fieldId="kc-client-signature"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="attributes.saml_client_signature"
|
||||||
|
control={control}
|
||||||
|
defaultValue="false"
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Switch
|
||||||
|
id="kc-client-signature"
|
||||||
|
label={t("common:on")}
|
||||||
|
labelOff={t("common:off")}
|
||||||
|
isChecked={value === "true"}
|
||||||
|
onChange={(value) => onChange("" + value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</FormAccess>
|
</FormAccess>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -111,11 +111,11 @@ export const NewClientForm = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("capabilityConfig"),
|
name: t("capabilityConfig"),
|
||||||
component: <CapabilityConfig />,
|
component: <CapabilityConfig protocol={client.protocol} />,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
footer={<Footer />}
|
footer={<Footer />}
|
||||||
onSave={() => save()}
|
onSave={save}
|
||||||
/>
|
/>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
"clients-help": {
|
"clients-help": {
|
||||||
"webOrigins": "Allowed CORS origins. To permit all origins of Valid Redirect URIs, add '+'. This does not include the '*' wildcard though. To permit all origins, explicitly add '*'.",
|
"webOrigins": "Allowed CORS origins. To permit all origins of Valid Redirect URIs, add '+'. This does not include the '*' wildcard though. To permit all origins, explicitly add '*'.",
|
||||||
"adminURL": "URL to the admin interface of the client. Set this if the client supports the adapter REST API. This REST API allows the auth server to push revocation policies and other administrative tasks. Usually this is set to the base URL of the client.",
|
"adminURL": "URL to the admin interface of the client. Set this if the client supports the adapter REST API. This REST API allows the auth server to push revocation policies and other administrative tasks. Usually this is set to the base URL of the client.",
|
||||||
|
"clientID": "Specifies ID referenced in URI and tokens. For example 'my-client'. For SAML this is also the expected issuer value from authn requests",
|
||||||
|
"clientName": "Specifies display name of the client. For example 'My Client'. Supports keys for localized values as well. For example: ${my_client}",
|
||||||
|
"description": "Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example: ${my_client_description}",
|
||||||
|
"encryptAssertions": "Should SAML assertions be encrypted with client's public key using AES?",
|
||||||
|
"clientSignature": "Will the client sign their saml requests and responses? And should they be validated?",
|
||||||
"downloadType": "this is information about the download type",
|
"downloadType": "this is information about the download type",
|
||||||
"details": "this is information about the details",
|
"details": "this is information about the details",
|
||||||
"createToken": "An initial access token can only be used to create clients",
|
"createToken": "An initial access token can only be used to create clients",
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
PageSection,
|
PageSection,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
|
@ -8,7 +10,6 @@ import {
|
||||||
AlertVariant,
|
AlertVariant,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { ClientDescription } from "../ClientDescription";
|
import { ClientDescription } from "../ClientDescription";
|
||||||
import { JsonFileUpload } from "../../components/json-file-upload/JsonFileUpload";
|
import { JsonFileUpload } from "../../components/json-file-upload/JsonFileUpload";
|
||||||
|
@ -17,10 +18,15 @@ import { ViewHeader } from "../../components/view-header/ViewHeader";
|
||||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||||
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
|
import { convertFormValuesToObject, convertToFormValues } from "../../util";
|
||||||
|
import { CapabilityConfig } from "../add/CapabilityConfig";
|
||||||
|
|
||||||
export const ImportForm = () => {
|
export const ImportForm = () => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
|
const history = useHistory();
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
|
const { realm } = useRealm();
|
||||||
const form = useForm<ClientRepresentation>();
|
const form = useForm<ClientRepresentation>();
|
||||||
const { register, handleSubmit, setValue } = form;
|
const { register, handleSubmit, setValue } = form;
|
||||||
|
|
||||||
|
@ -36,18 +42,27 @@ export const ImportForm = () => {
|
||||||
|
|
||||||
const obj = value ? JSON.parse(value as string) : defaultClient;
|
const obj = value ? JSON.parse(value as string) : defaultClient;
|
||||||
Object.keys(obj).forEach((k) => {
|
Object.keys(obj).forEach((k) => {
|
||||||
|
if (k === "attributes") {
|
||||||
|
convertToFormValues(obj[k], "attributes", form.setValue);
|
||||||
|
} else {
|
||||||
setValue(k, obj[k]);
|
setValue(k, obj[k]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = async (client: ClientRepresentation) => {
|
const save = async (client: ClientRepresentation) => {
|
||||||
try {
|
try {
|
||||||
await adminClient.clients.create({ ...client });
|
const newClient = await adminClient.clients.create({
|
||||||
|
...client,
|
||||||
|
attributes: convertFormValuesToObject(client.attributes || {}),
|
||||||
|
});
|
||||||
addAlert(t("clientImportSuccess"), AlertVariant.success);
|
addAlert(t("clientImportSuccess"), AlertVariant.success);
|
||||||
|
history.push(`/${realm}/clients/${newClient.id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addAlert(`${t("clientImportError")} '${error}'`, AlertVariant.danger);
|
addAlert(t("clientImportError", { error }), AlertVariant.danger);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
|
@ -72,11 +87,17 @@ export const ImportForm = () => {
|
||||||
ref={register()}
|
ref={register()}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<CapabilityConfig unWrap={true} />
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">
|
||||||
{t("common:save")}
|
{t("common:save")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="link">{t("common:cancel")}</Button>
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => history.push(`/${realm}/clients`)}
|
||||||
|
>
|
||||||
|
{t("common:cancel")}
|
||||||
|
</Button>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</FormAccess>
|
</FormAccess>
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
"webOrigins": "Web origins",
|
"webOrigins": "Web origins",
|
||||||
"adminURL": "Admin URL",
|
"adminURL": "Admin URL",
|
||||||
"formatOption": "Format option",
|
"formatOption": "Format option",
|
||||||
|
"encryptAssertions": "Encrypt assertions",
|
||||||
|
"clientSignature": "Client signature required",
|
||||||
"downloadAdaptorTitle": "Download adaptor configs",
|
"downloadAdaptorTitle": "Download adaptor configs",
|
||||||
"credentials": "Credentials",
|
"credentials": "Credentials",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
|
@ -61,7 +63,7 @@
|
||||||
"clientsExplain": "Clients are applications and services that can request authentication of a user",
|
"clientsExplain": "Clients are applications and services that can request authentication of a user",
|
||||||
"createSuccess": "Client created successfully",
|
"createSuccess": "Client created successfully",
|
||||||
"createError": "Could not create client: '{{error}}'",
|
"createError": "Could not create client: '{{error}}'",
|
||||||
"clientImportError": "Could not import client",
|
"clientImportError": "Could not import client: {{error}}",
|
||||||
"clientSaveSuccess": "Client successfully updated",
|
"clientSaveSuccess": "Client successfully updated",
|
||||||
"clientSaveError": "Client could not be updated:",
|
"clientSaveError": "Client could not be updated:",
|
||||||
"clientImportSuccess": "Client imported successfully",
|
"clientImportSuccess": "Client imported successfully",
|
||||||
|
|
|
@ -22,8 +22,8 @@ export function AlertPanel({ alerts, onCloseAlert }: AlertPanelProps) {
|
||||||
return (
|
return (
|
||||||
<AlertGroup isToast>
|
<AlertGroup isToast>
|
||||||
{alerts.map(({ key, variant, message, description }) => (
|
{alerts.map(({ key, variant, message, description }) => (
|
||||||
<>
|
|
||||||
<Alert
|
<Alert
|
||||||
|
timeout={true}
|
||||||
key={key}
|
key={key}
|
||||||
isLiveRegion
|
isLiveRegion
|
||||||
variant={AlertVariant[variant]}
|
variant={AlertVariant[variant]}
|
||||||
|
@ -38,7 +38,6 @@ export function AlertPanel({ alerts, onCloseAlert }: AlertPanelProps) {
|
||||||
>
|
>
|
||||||
{description && <p>{description}</p>}
|
{description && <p>{description}</p>}
|
||||||
</Alert>
|
</Alert>
|
||||||
</>
|
|
||||||
))}
|
))}
|
||||||
</AlertGroup>
|
</AlertGroup>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,11 +16,6 @@ export const AlertContext = createContext<AlertProps>({
|
||||||
|
|
||||||
export const useAlerts = () => useContext(AlertContext);
|
export const useAlerts = () => useContext(AlertContext);
|
||||||
|
|
||||||
type TimeOut = {
|
|
||||||
key: number;
|
|
||||||
timeOut: NodeJS.Timeout;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AlertProvider = ({ children }: { children: ReactNode }) => {
|
export const AlertProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [alerts, setAlerts] = useState<AlertType[]>([]);
|
const [alerts, setAlerts] = useState<AlertType[]>([]);
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,10 @@ export const JsonFileUpload = ({
|
||||||
| React.ChangeEvent<HTMLTextAreaElement>
|
| React.ChangeEvent<HTMLTextAreaElement>
|
||||||
| React.MouseEvent<HTMLButtonElement, MouseEvent>
|
| React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
): void => {
|
): void => {
|
||||||
if (event.nativeEvent instanceof MouseEvent) {
|
if (
|
||||||
|
event.nativeEvent instanceof MouseEvent &&
|
||||||
|
!(event.nativeEvent instanceof DragEvent)
|
||||||
|
) {
|
||||||
setFileUpload({ ...fileUpload, modal: true });
|
setFileUpload({ ...fileUpload, modal: true });
|
||||||
} else {
|
} else {
|
||||||
setFileUpload({
|
setFileUpload({
|
||||||
|
@ -100,7 +103,6 @@ export const JsonFileUpload = ({
|
||||||
value={fileUpload.value}
|
value={fileUpload.value}
|
||||||
filename={fileUpload.filename}
|
filename={fileUpload.filename}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
allowEditingUploadedText
|
|
||||||
onReadStarted={() =>
|
onReadStarted={() =>
|
||||||
setFileUpload({ ...fileUpload, isLoading: true })
|
setFileUpload({ ...fileUpload, isLoading: true })
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,6 @@ exports[`<JsonFileUpload /> render 1`] = `
|
||||||
className="pf-c-form__group-control"
|
className="pf-c-form__group-control"
|
||||||
>
|
>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
allowEditingUploadedText={true}
|
|
||||||
dropzoneProps={
|
dropzoneProps={
|
||||||
Object {
|
Object {
|
||||||
"accept": ".json",
|
"accept": ".json",
|
||||||
|
@ -59,7 +58,6 @@ exports[`<JsonFileUpload /> render 1`] = `
|
||||||
preventDropOnDocument={true}
|
preventDropOnDocument={true}
|
||||||
>
|
>
|
||||||
<FileUploadField
|
<FileUploadField
|
||||||
allowEditingUploadedText={true}
|
|
||||||
containerRef={[Function]}
|
containerRef={[Function]}
|
||||||
filename=""
|
filename=""
|
||||||
id="test"
|
id="test"
|
||||||
|
@ -303,7 +301,6 @@ exports[`<JsonFileUpload /> upload file 1`] = `
|
||||||
className="pf-c-form__group-control"
|
className="pf-c-form__group-control"
|
||||||
>
|
>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
allowEditingUploadedText={true}
|
|
||||||
dropzoneProps={
|
dropzoneProps={
|
||||||
Object {
|
Object {
|
||||||
"accept": ".json",
|
"accept": ".json",
|
||||||
|
@ -330,7 +327,6 @@ exports[`<JsonFileUpload /> upload file 1`] = `
|
||||||
preventDropOnDocument={true}
|
preventDropOnDocument={true}
|
||||||
>
|
>
|
||||||
<FileUploadField
|
<FileUploadField
|
||||||
allowEditingUploadedText={true}
|
|
||||||
containerRef={[Function]}
|
containerRef={[Function]}
|
||||||
filename=""
|
filename=""
|
||||||
id="upload"
|
id="upload"
|
||||||
|
|
Loading…
Reference in a new issue