fixed visual and logical errors described in #423 (#429)

* fixed visual and logical errors described in #423

fixing: #423

* changed reload to reset

* format
This commit is contained in:
Erik Jan de Wit 2021-03-19 08:49:33 +01:00 committed by GitHub
parent 398ca19ec1
commit b56788d942
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 268 additions and 108 deletions

View file

@ -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 = ({
<ViewHeader
titleKey={client ? client.clientId! : ""}
subKey="clients:clientsExplain"
badge={client.protocol}
divider={false}
dropdownItems={[
<DropdownItem key="download" onClick={() => toggleDownloadDialog()}>
{t("downloadAdapterConfig")}
@ -84,6 +87,7 @@ const ClientDetailHeader = ({
<DropdownItem key="export" onClick={() => exportClient(client)}>
{t("common:export")}
</DropdownItem>,
<Divider key="divider" />,
<DropdownItem key="delete" onClick={() => toggleDeleteDialog()}>
{t("common:delete")}
</DropdownItem>,
@ -129,12 +133,6 @@ export const ClientDetails = () => {
const [activeTab2, setActiveTab2] = useState(30);
const form = useForm<ClientForm>();
const publicClient = useWatch({
control: form.control,
name: "publicClient",
defaultValue: false,
});
const { clientId } = useParams<{ clientId: string }>();
const [client, setClient] = useState<ClientRepresentation>();
@ -274,17 +272,28 @@ export const ClientDetails = () => {
/>
)}
/>
<PageSection variant="light">
<PageSection variant="light" className="pf-u-p-0">
<FormProvider {...form}>
<KeycloakTabs isBox>
<KeycloakTabs
isBox
inset={{
default: "insetNone",
md: "insetSm",
xl: "inset2xl",
"2xl": "insetLg",
}}
>
<Tab
id="settings"
eventKey="settings"
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
>
<ClientSettings save={() => save()} />
<ClientSettings
save={() => save()}
reset={() => setupForm(client)}
/>
</Tab>
{publicClient && (
{client.publicClient && (
<Tab
id="credentials"
eventKey="credentials"

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
FormGroup,
@ -6,8 +6,9 @@ import {
Form,
Switch,
TextArea,
ActionGroup,
Button,
Select,
SelectVariant,
SelectOption,
} from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form";
@ -17,18 +18,29 @@ import { CapabilityConfig } from "./add/CapabilityConfig";
import { MultiLineInput } from "../components/multi-line-input/MultiLineInput";
import { FormAccess } from "../components/form-access/FormAccess";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { SaveReset } from "./advanced/SaveReset";
type ClientSettingsProps = {
save: () => 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 (
<>
<ScrollForm
className="pf-u-p-lg"
sections={[
t("capabilityConfig"),
t("generalSettings"),
@ -41,7 +53,17 @@ export const ClientSettings = ({ save }: ClientSettingsProps) => {
<ClientDescription />
</Form>
<FormAccess isHorizontal role="manage-clients">
<FormGroup label={t("rootUrl")} fieldId="kc-root-url">
<FormGroup
label={t("rootUrl")}
fieldId="kc-root-url"
labelIcon={
<HelpItem
helpText="clients-help:rootUrl"
forLabel={t("rootUrl")}
forID="kc-root-url"
/>
}
>
<TextInput
type="text"
id="kc-root-url"
@ -49,10 +71,33 @@ export const ClientSettings = ({ save }: ClientSettingsProps) => {
ref={register}
/>
</FormGroup>
<FormGroup label={t("validRedirectUri")} fieldId="kc-redirect">
<MultiLineInput name="redirectUris" />
<FormGroup
label={t("validRedirectUri")}
fieldId="kc-redirect"
labelIcon={
<HelpItem
helpText="clients-help:validRedirectURIs"
forLabel={t("validRedirectUri")}
forID="kc-redirect"
/>
}
>
<MultiLineInput
name="redirectUris"
addButtonLabel="clients:addRedirectUri"
/>
</FormGroup>
<FormGroup label={t("homeURL")} fieldId="kc-home-url">
<FormGroup
label={t("homeURL")}
fieldId="kc-home-url"
labelIcon={
<HelpItem
helpText="clients-help:homeURL"
forLabel={t("homeURL")}
forID="kc-home-url"
/>
}
>
<TextInput
type="text"
id="kc-home-url"
@ -71,7 +116,10 @@ export const ClientSettings = ({ save }: ClientSettingsProps) => {
/>
}
>
<MultiLineInput name="webOrigins" />
<MultiLineInput
name="webOrigins"
addButtonLabel="clients:addWebOrigins"
/>
</FormGroup>
<FormGroup
label={t("adminURL")}
@ -93,6 +141,51 @@ export const ClientSettings = ({ save }: ClientSettingsProps) => {
</FormGroup>
</FormAccess>
<FormAccess isHorizontal role="manage-clients">
<FormGroup
label={t("loginTheme")}
labelIcon={
<HelpItem
helpText="clients-help:loginTheme"
forLabel={t("loginTheme")}
forID="loginTheme"
/>
}
fieldId="loginTheme"
>
<Controller
name="attributes.login_theme"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<Select
toggleId="loginTheme"
onToggle={() => setLoginThemeOpen(!loginThemeOpen)}
onSelect={(_, value) => {
onChange(value as string);
setLoginThemeOpen(false);
}}
selections={value || t("common:choose")}
variant={SelectVariant.single}
aria-label={t("loginTheme")}
isOpen={loginThemeOpen}
>
<SelectOption key="empty" value="">
{t("common:choose")}
</SelectOption>
<>
{loginThemes &&
loginThemes.map((theme) => (
<SelectOption
selected={theme.name === value}
key={theme.name}
value={theme.name}
/>
))}
</>
</Select>
)}
/>
</FormGroup>
<FormGroup
label={t("consentRequired")}
fieldId="kc-consent"
@ -129,6 +222,7 @@ export const ClientSettings = ({ save }: ClientSettingsProps) => {
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")
}
/>
</FormGroup>
<ActionGroup className="keycloak__form_actions">
<Button variant="primary" onClick={save}>
{t("common:save")}
</Button>
<Button variant="link">{t("common:cancel")}</Button>
</ActionGroup>
<SaveReset name="settings" save={save} reset={reset} />
</FormAccess>
</ScrollForm>
</>

View file

@ -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<ClientForm>();
const { control, watch, setValue } = useFormContext<ClientForm>();
const protocol = type || watch("protocol");
const clientAuthentication = watch("publicClient");
const clientAuthorization = watch("authorizationServicesEnabled");
return (
<FormAccess isHorizontal role="manage-clients" unWrap={unWrap}>
@ -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"
>
<Grid>
<GridItem lg={4} sm={6}>
<GridItem lg={3} sm={6}>
<Controller
name="standardFlowEnabled"
defaultValue={false}
defaultValue={true}
control={control}
render={({ onChange, value }) => (
<Checkbox
label={t("standardFlow")}
id="kc-flow-standard"
name="standardFlowEnabled"
isChecked={value}
onChange={onChange}
/>
<InputGroup>
<Checkbox
label={t("standardFlow")}
id="kc-flow-standard"
name="standardFlowEnabled"
isChecked={value}
onChange={onChange}
/>
<HelpItem
helpText="clients-help:standardFlow"
forLabel={t("standardFlow")}
forID="kc-flow-standard"
/>
</InputGroup>
)}
/>
</GridItem>
<GridItem lg={8} sm={6}>
<GridItem lg={9} sm={6}>
<Controller
name="directAccessGrantsEnabled"
defaultValue={false}
defaultValue={true}
control={control}
render={({ onChange, value }) => (
<Checkbox
label={t("directAccess")}
id="kc-flow-direct"
name="directAccessGrantsEnabled"
isChecked={value}
onChange={onChange}
/>
<InputGroup>
<Checkbox
label={t("directAccess")}
id="kc-flow-direct"
name="directAccessGrantsEnabled"
isChecked={value}
onChange={onChange}
/>
<HelpItem
helpText="clients-help:directAccess"
forLabel={t("directAccess")}
forID="kc-flow-direct"
/>
</InputGroup>
)}
/>
</GridItem>
<GridItem lg={4} sm={6}>
<GridItem lg={3} 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}
/>
<InputGroup>
<Checkbox
label={t("implicitFlow")}
id="kc-flow-implicit"
name="implicitFlowEnabled"
isChecked={value}
onChange={onChange}
/>
<HelpItem
helpText="clients-help:implicitFlow"
forLabel={t("implicitFlow")}
forID="kc-flow-implicit"
/>
</InputGroup>
)}
/>
</GridItem>
<GridItem lg={8} sm={6}>
<GridItem lg={9} 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}
/>
<InputGroup>
<Checkbox
label={t("serviceAccount")}
id="kc-flow-service-account"
name="serviceAccountsEnabled"
isChecked={value}
onChange={onChange}
isDisabled={
!clientAuthentication || clientAuthorization
}
/>
<HelpItem
helpText="clients-help:serviceAccount"
forLabel={t("serviceAccount")}
forID="kc-flow-service-account"
/>
</InputGroup>
)}
/>
</GridItem>

View file

@ -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<ClientRepresentation>({ defaultValues: client });

View file

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

View file

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

View file

@ -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<TextInputProps, "form"> & {
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) => (
<Split key={id}>
<SplitItem>
<Fragment key={id}>
<InputGroup>
<TextInput
id={id}
ref={register()}
@ -51,29 +61,29 @@ export const MultiLineInput = ({ name, ...rest }: MultiLineInputProps) => {
defaultValue={value}
{...rest}
/>
</SplitItem>
<SplitItem>
{index === fields.length - 1 && (
<Button
variant={ButtonVariant.link}
onClick={() => append({})}
tabIndex={-1}
isDisabled={rest.isDisabled}
>
<PlusIcon />
</Button>
)}
{index !== fields.length - 1 && (
<Button
variant={ButtonVariant.link}
onClick={() => remove(index)}
tabIndex={-1}
>
<MinusIcon />
</Button>
)}
</SplitItem>
</Split>
<Button
variant={ButtonVariant.link}
onClick={() => remove(index)}
tabIndex={-1}
isDisabled={index === fields.length - 1}
>
<MinusCircleIcon />
</Button>
</InputGroup>
{index === fields.length - 1 && (
<Button
variant={ButtonVariant.link}
onClick={() => append({})}
tabIndex={-1}
isDisabled={
rest.isDisabled ||
!(currentValues && currentValues[index]?.value)
}
>
<PlusCircleIcon /> {t(addButtonLabel || "common:add")}
</Button>
)}
</Fragment>
))}
</>
);

View file

@ -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 (
<Grid hasGutter>
<Grid hasGutter {...rest}>
<GridItem span={8}>
{sections.map((cat, index) => (
<FormPanel scrollId={spacesToHyphens(cat)} key={cat} title={cat}>

View file

@ -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 = ({
</LevelItem>
<LevelItem></LevelItem>
<LevelItem>
<Toolbar>
<Toolbar className="pf-u-p-0">
<ToolbarContent>
{onToggle && (
<ToolbarItem>
@ -163,7 +161,7 @@ export const ViewHeader = ({
/>
)}
</PageSection>
{divider && <Divider component={dividerComponent} />}
{divider && <Divider component="div" />}
</>
);
};

View file

@ -48,11 +48,7 @@ export const UsersTabs = () => {
return (
<>
<ViewHeader
titleKey={id! || t("users:createUser")}
subKey=""
dividerComponent="div"
/>
<ViewHeader titleKey={id! || t("users:createUser")} subKey="" />
<PageSection variant="light">
{id && (
<KeycloakTabs isBox>