diff --git a/src/clients/ClientDetails.tsx b/src/clients/ClientDetails.tsx index 5e18abdd78..a9952d2298 100644 --- a/src/clients/ClientDetails.tsx +++ b/src/clients/ClientDetails.tsx @@ -3,6 +3,7 @@ import { Alert, AlertVariant, ButtonVariant, + Divider, DropdownItem, PageSection, Spinner, @@ -13,7 +14,7 @@ import { import { useParams } from "react-router-dom"; import { useErrorHandler } from "react-error-boundary"; import { useTranslation } from "react-i18next"; -import { Controller, FormProvider, useForm, useWatch } from "react-hook-form"; +import { Controller, FormProvider, useForm } from "react-hook-form"; import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; import _ from "lodash"; @@ -77,6 +78,8 @@ const ClientDetailHeader = ({ toggleDownloadDialog()}> {t("downloadAdapterConfig")} @@ -84,6 +87,7 @@ const ClientDetailHeader = ({ exportClient(client)}> {t("common:export")} , + , toggleDeleteDialog()}> {t("common:delete")} , @@ -129,12 +133,6 @@ export const ClientDetails = () => { const [activeTab2, setActiveTab2] = useState(30); const form = useForm(); - const publicClient = useWatch({ - control: form.control, - name: "publicClient", - defaultValue: false, - }); - const { clientId } = useParams<{ clientId: string }>(); const [client, setClient] = useState(); @@ -274,17 +272,28 @@ export const ClientDetails = () => { /> )} /> - + - + {t("common:settings")}} > - save()} /> + save()} + reset={() => setupForm(client)} + /> - {publicClient && ( + {client.publicClient && ( void; + reset: () => void; }; -export const ClientSettings = ({ save }: ClientSettingsProps) => { - const { register, control } = useFormContext(); +export const ClientSettings = ({ save, reset }: ClientSettingsProps) => { + const { register, control, watch } = useFormContext(); const { t } = useTranslation("clients"); + const [loginThemeOpen, setLoginThemeOpen] = useState(false); + const loginThemes = useServerInfo().themes!["login"]; + const consentRequired: boolean = watch("consentRequired"); + const displayOnConsentScreen: string = watch( + "attributes.display-on-consent-screen" + ); + return ( <> { - + + } + > { ref={register} /> - - + + } + > + - + + } + > { /> } > - + { + + } + fieldId="loginTheme" + > + ( + + )} + /> + { labelOff={t("common:off")} isChecked={value === "true"} onChange={(value) => onChange("" + value)} + isDisabled={!consentRequired} /> )} /> @@ -141,14 +235,12 @@ export const ClientSettings = ({ save }: ClientSettingsProps) => { id="kc-consent-screen-text" name="attributes.consent-screen-text" ref={register} + isDisabled={ + !(consentRequired && displayOnConsentScreen === "true") + } /> - - - - + diff --git a/src/clients/add/CapabilityConfig.tsx b/src/clients/add/CapabilityConfig.tsx index b8101ea729..fcb8f57857 100644 --- a/src/clients/add/CapabilityConfig.tsx +++ b/src/clients/add/CapabilityConfig.tsx @@ -6,6 +6,7 @@ import { Checkbox, Grid, GridItem, + InputGroup, } from "@patternfly/react-core"; import { Controller, useFormContext } from "react-hook-form"; import { FormAccess } from "../../components/form-access/FormAccess"; @@ -22,8 +23,10 @@ export const CapabilityConfig = ({ protocol: type, }: CapabilityConfigProps) => { const { t } = useTranslation("clients"); - const { control, watch } = useFormContext(); + const { control, watch, setValue } = useFormContext(); const protocol = type || watch("protocol"); + const clientAuthentication = watch("publicClient"); + const clientAuthorization = watch("authorizationServicesEnabled"); return ( @@ -67,7 +70,13 @@ export const CapabilityConfig = ({ label={t("common:on")} labelOff={t("common:off")} isChecked={value} - onChange={onChange} + onChange={(value) => { + onChange(value); + if (value) { + setValue("serviceAccountsEnabled", true); + } + }} + isDisabled={!clientAuthentication} /> )} /> @@ -78,67 +87,98 @@ export const CapabilityConfig = ({ fieldId="kc-flow" > - + ( - + + + + )} /> - + ( - + + + + )} /> - + ( - + + + + )} /> - + ( - + + + + )} /> diff --git a/src/clients/add/NewClientForm.tsx b/src/clients/add/NewClientForm.tsx index 55364e7f04..e6b7ef4434 100644 --- a/src/clients/add/NewClientForm.tsx +++ b/src/clients/add/NewClientForm.tsx @@ -34,8 +34,8 @@ export const NewClientForm = () => { authorizationServicesEnabled: false, serviceAccountsEnabled: false, implicitFlowEnabled: false, - directAccessGrantsEnabled: false, - standardFlowEnabled: false, + directAccessGrantsEnabled: true, + standardFlowEnabled: true, }); const { addAlert } = useAlerts(); const methods = useForm({ defaultValues: client }); diff --git a/src/clients/help.json b/src/clients/help.json index bfc67b9026..d0ed756f2d 100644 --- a/src/clients/help.json +++ b/src/clients/help.json @@ -1,10 +1,18 @@ { "clients-help": { + "serviceAccount": "Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. In terms of OAuth2 specification, this enables support of 'Client Credentials Grant' for this client.", + "directAccess": "This enables support for Direct Access Grants, which means that client has access to username/password of user and exchange it directly with Keycloak server for access token. In terms of OAuth2 specification, this enables support of 'Resource Owner Password Credentials Grant' for this client.", + "standardFlow": "This enables standard OpenID Connect redirect based authentication with authorization code. In terms of OpenID Connect or OAuth2 specifications, this enables support of 'Authorization Code Flow' for this client.", + "implicitFlow": "This enables support for OpenID Connect redirect based authentication without authorization code. In terms of OpenID Connect or OAuth2 specifications, this enables support of 'Implicit Flow' for this client.", + "rootURL": "Root URL appended to relative URLs", + "validRedirectURIs": "Valid URI pattern a browser can redirect to after a successful login or logout. Simple wildcards are allowed such as 'http://example.com/*'. Relative path can be specified too such as /my/relative/path/*. Relative paths are relative to the client root URL, or if none is specified the auth server root URL is used. For SAML, you must set valid URI patterns if you are relying on the consumer service URL embedded with the login request.", "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 '*'.", + "homeURL": "Default URL to use when the auth server needs to redirect or link back to 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}", + "loginTheme": "Select theme for login, OTP, grant, registration, and forgot password pages.", "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", diff --git a/src/clients/messages.json b/src/clients/messages.json index 450b994833..7216957d20 100644 --- a/src/clients/messages.json +++ b/src/clients/messages.json @@ -7,6 +7,7 @@ "clientID": "Client ID", "homeURL": "Home URL", "webOrigins": "Web origins", + "addWebOrigins": "Add web origins", "adminURL": "Admin URL", "formatOption": "Format option", "encryptAssertions": "Encrypt assertions", @@ -109,6 +110,7 @@ "accessSettings": "Access settings", "rootUrl": "Root URL", "validRedirectUri": "Valid redirect URIs", + "addRedirectUri": "Add valid redirect URIs", "loginTheme": "Login theme", "consentRequired": "Consent required", "clientAuthenticator": "Client Authenticator", diff --git a/src/components/multi-line-input/MultiLineInput.tsx b/src/components/multi-line-input/MultiLineInput.tsx index 8d32987baa..1a0fa12db1 100644 --- a/src/components/multi-line-input/MultiLineInput.tsx +++ b/src/components/multi-line-input/MultiLineInput.tsx @@ -1,14 +1,14 @@ -import React, { useEffect } from "react"; -import { useFieldArray, useFormContext } from "react-hook-form"; +import React, { Fragment, useEffect } from "react"; +import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { TextInput, - Split, - SplitItem, Button, ButtonVariant, TextInputProps, + InputGroup, } from "@patternfly/react-core"; -import { MinusIcon, PlusIcon } from "@patternfly/react-icons"; +import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons"; +import { useTranslation } from "react-i18next"; export type MultiLine = { value: string; @@ -26,14 +26,24 @@ export function toValue(formValue: MultiLine[]): string[] { export type MultiLineInputProps = Omit & { name: string; + addButtonLabel?: string; }; -export const MultiLineInput = ({ name, ...rest }: MultiLineInputProps) => { +export const MultiLineInput = ({ + name, + addButtonLabel, + ...rest +}: MultiLineInputProps) => { + const { t } = useTranslation(); const { register, control, reset } = useFormContext(); const { fields, append, remove } = useFieldArray({ name, control, }); + const currentValues: + | { [name: string]: { value: string } } + | undefined = useWatch({ control, name }); + useEffect(() => { reset({ [name]: [{ value: "" }], @@ -42,8 +52,8 @@ export const MultiLineInput = ({ name, ...rest }: MultiLineInputProps) => { return ( <> {fields.map(({ id, value }, index) => ( - - + + { defaultValue={value} {...rest} /> - - - {index === fields.length - 1 && ( - - )} - {index !== fields.length - 1 && ( - - )} - - + + + {index === fields.length - 1 && ( + + )} + ))} ); diff --git a/src/components/scroll-form/ScrollForm.tsx b/src/components/scroll-form/ScrollForm.tsx index 6b080c2002..067827580f 100644 --- a/src/components/scroll-form/ScrollForm.tsx +++ b/src/components/scroll-form/ScrollForm.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { Grid, GridItem, + GridProps, JumpLinks, JumpLinksItem, PageSection, @@ -12,7 +13,7 @@ import { mainPageContentId } from "../../App"; import { FormPanel } from "./FormPanel"; import "./scroll-form.css"; -type ScrollFormProps = { +type ScrollFormProps = GridProps & { sections: string[]; children: React.ReactNode; }; @@ -21,12 +22,16 @@ const spacesToHyphens = (string: string): string => { return string.replace(/\s+/g, "-"); }; -export const ScrollForm = ({ sections, children }: ScrollFormProps) => { +export const ScrollForm = ({ + sections, + children, + ...rest +}: ScrollFormProps) => { const { t } = useTranslation("common"); const nodes = Children.toArray(children); return ( - + {sections.map((cat, index) => ( diff --git a/src/components/view-header/ViewHeader.tsx b/src/components/view-header/ViewHeader.tsx index a8fcddec33..6167a86ece 100644 --- a/src/components/view-header/ViewHeader.tsx +++ b/src/components/view-header/ViewHeader.tsx @@ -27,7 +27,6 @@ export type ViewHeaderProps = { badge?: string; badgeId?: string; badgeIsRead?: boolean; - dividerComponent?: "div" | "hr" | "li" | undefined; subKey: string; actionsDropdownId?: string; subKeyLinkProps?: FormattedLinkProps; @@ -44,7 +43,6 @@ export const ViewHeader = ({ titleKey, badge, badgeIsRead, - dividerComponent, subKey, subKeyLinkProps, dropdownItems, @@ -92,7 +90,7 @@ export const ViewHeader = ({ - + {onToggle && ( @@ -163,7 +161,7 @@ export const ViewHeader = ({ /> )} - {divider && } + {divider && } ); }; diff --git a/src/user/UsersTabs.tsx b/src/user/UsersTabs.tsx index e77915f9bd..e6e72d0d31 100644 --- a/src/user/UsersTabs.tsx +++ b/src/user/UsersTabs.tsx @@ -48,11 +48,7 @@ export const UsersTabs = () => { return ( <> - + {id && (