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:
Erik Jan de Wit 2021-03-09 14:59:41 +01:00 committed by GitHub
parent 4ba5fcc723
commit 7bf85196e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 281 additions and 156 deletions

View file

@ -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,

View file

@ -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>
)} )}

View file

@ -8,129 +8,203 @@ 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}>
<FormGroup <>
hasNoPaddingTop {protocol === "openid-connect" && (
label={t("clientAuthentication")} <>
fieldId="kc-authentication" <FormGroup
> hasNoPaddingTop
<Controller label={t("clientAuthentication")}
name="publicClient" fieldId="kc-authentication"
defaultValue={false} >
control={control} <Controller
render={({ onChange, value }) => ( name="publicClient"
<Switch defaultValue={false}
id="kc-authentication" control={control}
name="publicClient" render={({ onChange, value }) => (
label={t("common:on")} <Switch
labelOff={t("common:off")} id="kc-authentication"
isChecked={value} name="publicClient"
onChange={onChange} label={t("common:on")}
/> labelOff={t("common:off")}
)} isChecked={value}
/> onChange={onChange}
</FormGroup> />
<FormGroup )}
hasNoPaddingTop />
label={t("clientAuthorization")} </FormGroup>
fieldId="kc-authorization" <FormGroup
> hasNoPaddingTop
<Controller label={t("clientAuthorization")}
name="authorizationServicesEnabled" fieldId="kc-authorization"
defaultValue={false} >
control={control} <Controller
render={({ onChange, value }) => ( name="authorizationServicesEnabled"
<Switch defaultValue={false}
id="kc-authorization" control={control}
name="authorizationServicesEnabled" render={({ onChange, value }) => (
label={t("common:on")} <Switch
labelOff={t("common:off")} id="kc-authorization"
isChecked={value} name="authorizationServicesEnabled"
onChange={onChange} label={t("common:on")}
/> labelOff={t("common:off")}
)} isChecked={value}
/> onChange={onChange}
</FormGroup> />
<FormGroup )}
hasNoPaddingTop />
label={t("authenticationFlow")} </FormGroup>
fieldId="kc-flow" <FormGroup
> hasNoPaddingTop
<Grid> label={t("authenticationFlow")}
<GridItem lg={4} sm={6}> fieldId="kc-flow"
<Controller >
name="standardFlowEnabled" <Grid>
defaultValue={false} <GridItem lg={4} sm={6}>
control={control} <Controller
render={({ onChange, value }) => ( name="standardFlowEnabled"
<Checkbox defaultValue={false}
label={t("standardFlow")} control={control}
id="kc-flow-standard" render={({ onChange, value }) => (
name="standardFlowEnabled" <Checkbox
isChecked={value} label={t("standardFlow")}
onChange={onChange} id="kc-flow-standard"
name="standardFlowEnabled"
isChecked={value}
onChange={onChange}
/>
)}
/>
</GridItem>
<GridItem lg={8} sm={6}>
<Controller
name="directAccessGrantsEnabled"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
<Checkbox
label={t("directAccess")}
id="kc-flow-direct"
name="directAccessGrantsEnabled"
isChecked={value}
onChange={onChange}
/>
)}
/>
</GridItem>
<GridItem lg={4} sm={6}>
<Controller
name="implicitFlowEnabled"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
<Checkbox
label={t("implicitFlow")}
id="kc-flow-implicit"
name="implicitFlowEnabled"
isChecked={value}
onChange={onChange}
/>
)}
/>
</GridItem>
<GridItem lg={8} sm={6}>
<Controller
name="serviceAccountsEnabled"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
<Checkbox
label={t("serviceAccount")}
id="kc-flow-service-account"
name="serviceAccountsEnabled"
isChecked={value}
onChange={onChange}
/>
)}
/>
</GridItem>
</Grid>
</FormGroup>
</>
)}
</>
<>
{protocol === "saml" && (
<>
<FormGroup
labelIcon={
<HelpItem
helpText="clients-help:encryptAssertions"
forLabel={t("encryptAssertions")}
forID="kc-encrypt"
/> />
)} }
/> label={t("encryptAssertions")}
</GridItem> fieldId="kc-encrypt"
<GridItem lg={8} sm={6}> >
<Controller <Controller
name="directAccessGrantsEnabled" name="attributes.saml_encrypt"
defaultValue={false} control={control}
control={control} defaultValue="false"
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Checkbox <Switch
label={t("directAccess")} id="kc-encrypt"
id="kc-flow-direct" label={t("common:on")}
name="directAccessGrantsEnabled" labelOff={t("common:off")}
isChecked={value} isChecked={value === "true"}
onChange={onChange} onChange={(value) => onChange("" + value)}
/>
)}
/>
</FormGroup>
<FormGroup
labelIcon={
<HelpItem
helpText="clients-help:clientSignature"
forLabel={t("clientSignature")}
forID="kc-client-signature"
/> />
)} }
/> label={t("clientSignature")}
</GridItem> fieldId="kc-client-signature"
<GridItem lg={4} sm={6}> >
<Controller <Controller
name="implicitFlowEnabled" name="attributes.saml_client_signature"
defaultValue={false} control={control}
control={control} defaultValue="false"
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Checkbox <Switch
label={t("implicitFlow")} id="kc-client-signature"
id="kc-flow-implicit" label={t("common:on")}
name="implicitFlowEnabled" labelOff={t("common:off")}
isChecked={value} isChecked={value === "true"}
onChange={onChange} onChange={(value) => onChange("" + value)}
/> />
)} )}
/> />
</GridItem> </FormGroup>
<GridItem lg={8} sm={6}> </>
<Controller )}
name="serviceAccountsEnabled" </>
defaultValue={false}
control={control}
render={({ onChange, value }) => (
<Checkbox
label={t("serviceAccount")}
id="kc-flow-service-account"
name="serviceAccountsEnabled"
isChecked={value}
onChange={onChange}
/>
)}
/>
</GridItem>
</Grid>
</FormGroup>
</FormAccess> </FormAccess>
); );
}; };

View file

@ -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>

View file

@ -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",

View file

@ -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) => {
setValue(k, obj[k]); if (k === "attributes") {
convertToFormValues(obj[k], "attributes", form.setValue);
} else {
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>

View file

@ -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",

View file

@ -22,23 +22,22 @@ 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]}
variantLabel="" variantLabel=""
title={message} title={message}
actionClose={ actionClose={
<AlertActionCloseButton <AlertActionCloseButton
title={message} title={message}
onClose={() => onCloseAlert(key)} onClose={() => onCloseAlert(key)}
/> />
} }
> >
{description && <p>{description}</p>} {description && <p>{description}</p>}
</Alert> </Alert>
</>
))} ))}
</AlertGroup> </AlertGroup>
); );

View file

@ -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[]>([]);

View file

@ -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 })
} }

View file

@ -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"