* fixed visual and logical errors described in #423 fixing: #423 * changed reload to reset * format
This commit is contained in:
parent
398ca19ec1
commit
b56788d942
10 changed files with 268 additions and 108 deletions
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue