initial version of the client settings page (#89)

* initial version of the client settings page

* fix test

* fixed spelling

* merge errors

* fix merge error

* renamed Step1 and Step2
This commit is contained in:
Erik Jan de Wit 2020-09-22 14:43:51 +02:00 committed by GitHub
parent c656d6acee
commit 0f1d93d672
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1710 additions and 55 deletions

View file

@ -22,6 +22,7 @@ import { UserFederationSection } from "./user-federation/UserFederationSection";
import { PageNotFoundSection } from "./PageNotFoundSection";
import { RealmContextProvider } from "./components/realm-context/RealmContext";
import { ClientSettings } from "./clients/ClientSettings";
export const App = () => {
return (
@ -33,6 +34,11 @@ export const App = () => {
<Route exact path="/add-realm" component={NewRealmForm}></Route>
<Route exact path="/clients" component={ClientsSection}></Route>
<Route
exact
path="/client-settings"
component={ClientSettings}
></Route>
<Route exact path="/add-client" component={NewClientForm}></Route>
<Route exact path="/import-client" component={ImportForm}></Route>

View file

@ -1,5 +1,6 @@
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import {
Table,
TableBody,
@ -41,11 +42,11 @@ export const ClientList = ({ baseUrl, clients }: ClientListProps) => {
const field = data!.toString();
const value = convertClientId(field);
return field.indexOf("true") !== -1 ? (
<>{value}</>
<Link to="client-settings">{value}</Link>
) : (
<>
<Link to="client-settings">
{value} <Badge isRead>Disabled</Badge>
</>
</Link>
);
};
@ -60,12 +61,18 @@ export const ClientList = ({ baseUrl, clients }: ClientListProps) => {
};
/* eslint-disable no-template-curly-in-string */
const replaceBaseUrl = (r: ClientRepresentation) =>
r.rootUrl &&
r.rootUrl
.replace("${authBaseUrl}", baseUrl)
.replace("${authAdminUrl}", baseUrl) +
(r.baseUrl ? r.baseUrl.substr(1) : "");
const replaceBaseUrl = (r: ClientRepresentation) => {
if (r.rootUrl) {
if (!r.rootUrl.startsWith("http") || r.rootUrl.indexOf("$") !== -1) {
r.rootUrl =
r.rootUrl
.replace("${authBaseUrl}", baseUrl)
.replace("${authAdminUrl}", baseUrl) +
(r.baseUrl ? r.baseUrl.substr(1) : "");
}
}
return r.rootUrl;
};
const data = clients!
.map((r) => {
@ -120,7 +127,7 @@ export const ClientList = ({ baseUrl, clients }: ClientListProps) => {
httpClient.doDelete(
`/admin/realms/${realm}/clients/${data[rowId].client.id}`
);
add(t("clientDeletedSucess"), AlertVariant.success);
add(t("clientDeletedSuccess"), AlertVariant.success);
} catch (error) {
add(`${t("clientDeleteError")} ${error}`, AlertVariant.danger);
}

View file

@ -0,0 +1,137 @@
import React, { useState, FormEvent } from "react";
import { useTranslation } from "react-i18next";
import {
FormGroup,
TextInput,
Form,
Dropdown,
DropdownToggle,
DropdownItem,
Switch,
TextArea,
PageSection,
} from "@patternfly/react-core";
import { useForm } from "react-hook-form";
import { ScrollForm } from "../components/scroll-form/ScrollForm";
import { ClientDescription } from "./ClientDescription";
import { ClientRepresentation } from "./models/client-model";
import { CapabilityConfig } from "./add/CapabilityConfig";
type ClientSettingsProps = {
client: ClientRepresentation;
};
export const ClientSettings = ({ client: clientInit }: ClientSettingsProps) => {
const { t } = useTranslation("clients");
const [client, setClient] = useState({ ...clientInit });
const form = useForm();
const onChange = (
value: string | boolean,
event: FormEvent<HTMLInputElement>
) => {
const target = event.target;
const name = (target as HTMLInputElement).name;
setClient({
...client,
[name]: value,
});
};
return (
<PageSection>
<ScrollForm
sections={[
t("capabilityConfig"),
t("generalSettings"),
t("accessSettings"),
t("loginSettings"),
]}
>
<Form isHorizontal>
<CapabilityConfig form={form} />
</Form>
<Form isHorizontal>
<ClientDescription register={form.register} />
</Form>
<Form isHorizontal>
<FormGroup label={t("rootUrl")} fieldId="kc-root-url">
<TextInput
type="text"
id="kc-root-url"
name="rootUrl"
value={client.rootUrl}
onChange={onChange}
/>
</FormGroup>
<FormGroup label={t("validRedirectUri")} fieldId="kc-redirect">
<TextInput
type="text"
id="kc-redirect"
name="redirectUris"
onChange={onChange}
/>
</FormGroup>
<FormGroup label={t("homeURL")} fieldId="kc-home-url">
<TextInput
type="text"
id="kc-home-url"
name="baseUrl"
value={client.baseUrl}
onChange={onChange}
/>
</FormGroup>
</Form>
<Form isHorizontal>
<FormGroup label={t("loginTheme")} fieldId="kc-login-theme">
<Dropdown
id="kc-login-theme"
toggle={
<DropdownToggle id="toggle-id" onToggle={() => {}}>
{t("loginTheme")}
</DropdownToggle>
}
dropdownItems={[
<DropdownItem key="link">Link</DropdownItem>,
<DropdownItem key="action" component="button" />,
]}
/>
</FormGroup>
<FormGroup label={t("consentRequired")} fieldId="kc-consent">
<Switch
id="kc-consent"
name="consentRequired"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={client.consentRequired}
onChange={onChange}
/>
</FormGroup>
<FormGroup
label={t("displayOnClient")}
fieldId="kc-display-on-client"
>
<Switch
id="kc-display-on-client"
name="alwaysDisplayInConsole"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={client.alwaysDisplayInConsole}
onChange={onChange}
/>
</FormGroup>
<FormGroup
label={t("consentScreenText")}
fieldId="kc-consent-screen-text"
>
<TextArea
id="kc-consent-screen-text"
name="consentText"
//value={client.protocolMappers![0].consentText}
/>
</FormGroup>
</Form>
</ScrollForm>
</PageSection>
);
};

View file

@ -1,17 +1,15 @@
import React from "react";
import { MemoryRouter } from "react-router-dom";
import { render } from "@testing-library/react";
import clientMock from "./mock-clients.json";
import { I18nextProvider } from "react-i18next";
import i18n from "../../i18n";
import { ClientList } from "../ClientList";
test("renders ClientList", () => {
const { getByText } = render(
<I18nextProvider i18n={i18n}>
const container = render(
<MemoryRouter>
<ClientList clients={clientMock} baseUrl="http://blog.nerdin.ch" />
</I18nextProvider>
</MemoryRouter>
);
const headerElement = getByText(/Client ID/i);
expect(headerElement).toBeInTheDocument();
expect(container).toMatchSnapshot();
});

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import {
FormGroup,
Switch,
@ -8,13 +9,12 @@ import {
Form,
} from "@patternfly/react-core";
import { UseFormMethods, Controller } from "react-hook-form";
import { useTranslation } from "react-i18next";
type Step2Props = {
type CapabilityConfigProps = {
form: UseFormMethods;
};
export const Step2 = ({ form }: Step2Props) => {
export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
const { t } = useTranslation("clients");
return (
<Form isHorizontal>
@ -34,13 +34,13 @@ export const Step2 = ({ form }: Step2Props) => {
)}
/>
</FormGroup>
<FormGroup label={t("authentication")} fieldId="kc-authorisation">
<FormGroup label={t("clientAuthorization")} fieldId="kc-authorization">
<Controller
name="authorizationServicesEnabled"
control={form.control}
render={({ onChange, value }) => (
<Switch
id="kc-authorisation"
id="kc-authorization"
name="authorizationServicesEnabled"
label={t("common:on")}
labelOff={t("common:off")}
@ -59,7 +59,6 @@ export const Step2 = ({ form }: Step2Props) => {
render={({ onChange, value }) => (
<Checkbox
label={t("standardFlow")}
aria-label={t("enableStandardFlow")}
id="kc-flow-standard"
name="standardFlowEnabled"
isChecked={value}
@ -75,7 +74,6 @@ export const Step2 = ({ form }: Step2Props) => {
render={({ onChange, value }) => (
<Checkbox
label={t("directAccess")}
aria-label={t("enableDirectAccess")}
id="kc-flow-direct"
name="directAccessGrantsEnabled"
isChecked={value}
@ -90,9 +88,8 @@ export const Step2 = ({ form }: Step2Props) => {
control={form.control}
render={({ onChange, value }) => (
<Checkbox
label={t("implicidFlow")}
aria-label={t("enableImplicidFlow")}
id="kc-flow-implicid"
label={t("implicitFlow")}
id="kc-flow-implicit"
name="implicitFlowEnabled"
isChecked={value}
onChange={onChange}
@ -107,7 +104,6 @@ export const Step2 = ({ form }: Step2Props) => {
render={({ onChange, value }) => (
<Checkbox
label={t("serviceAccount")}
aria-label={t("enableServiceAccount")}
id="kc-flow-service-account"
name="serviceAccountsEnabled"
isChecked={value}

View file

@ -14,11 +14,11 @@ import { sortProvider } from "../../util";
import { ServerInfoRepresentation } from "../models/server-info";
import { ClientDescription } from "../ClientDescription";
type Step1Props = {
type GeneralSettingsProps = {
form: UseFormMethods;
};
export const Step1 = ({ form }: Step1Props) => {
export const GeneralSettings = ({ form }: GeneralSettingsProps) => {
const httpClient = useContext(HttpClientContext)!;
const { t } = useTranslation();
const { errors, control, register } = form;

View file

@ -15,8 +15,8 @@ import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { HttpClientContext } from "../../http-service/HttpClientContext";
import { Step1 } from "./Step1";
import { Step2 } from "./Step2";
import { GeneralSettings } from "./GeneralSettings";
import { CapabilityConfig } from "./CapabilityConfig";
import { ClientRepresentation } from "../models/client-model";
import { useAlerts } from "../../components/alert/Alerts";
import { RealmContext } from "../../components/realm-context/RealmContext";
@ -110,11 +110,11 @@ export const NewClientForm = () => {
steps={[
{
name: t("generalSettings"),
component: <Step1 form={methods} />,
component: <GeneralSettings form={methods} />,
},
{
name: t("capabilityConfig"),
component: <Step2 form={methods} />,
component: <CapabilityConfig form={methods} />,
},
]}
footer={<Footer />}

View file

@ -1,5 +1,7 @@
{
"clients": {
"clientAuthorization": "Client authorization",
"implicitFlow": "Implicit flow",
"createClient": "Create client",
"importClient": "Import client",
"clientID": "Client ID",
@ -12,19 +14,23 @@
"capabilityConfig": "Capability config",
"clientsExplain": "Clients are applications and services that can request authentication of a user",
"clientImportError": "Could not import client",
"clientImportSuccess": "Client imported succeful",
"clientDeletedSucess": "The client has been deleted",
"clientImportSuccess": "Client imported successful",
"clientDeletedSuccess": "The client has been deleted",
"clientDeleteError": "Could not delete client:",
"clientAuthentication": "Client authentication",
"authentication": "Authentication",
"authenticationFlow": "Authentication flow",
"standardFlow": "Standard flow",
"enableStandardFlow": "Enable standard flow",
"directAccess": "Direct access",
"enableDirectAccess": "Enable direct access",
"implicidFlow": "Implicid flow",
"enableImplicidFlow": "Enable implicid flow",
"serviceAccount": "Service account",
"enableServiceAccount": "Enable service account"
"enableServiceAccount": "Enable service account",
"displayOnClient": "Display client on screen",
"consentScreenText": "Client consent screen text",
"loginSettings": "Login settings",
"accessSettings": "Access settings",
"rootUrl": "Root URL",
"validRedirectUri": "Valid redirect URIs",
"loginTheme": "Login theme",
"consentRequired": "Consent required"
}
}

View file

@ -1,5 +1,5 @@
import React, { Children, useEffect, useState } from "react";
import { Form, Grid, GridItem, Title } from "@patternfly/react-core";
import { Grid, GridItem, Title } from "@patternfly/react-core";
import { FormPanel } from "./FormPanel";
import style from "./scroll-form.module.css";
@ -70,13 +70,11 @@ export const ScrollForm = ({ sections, children }: ScrollFormProps) => {
<>
<Grid hasGutter>
<GridItem span={8}>
<Form>
{sections.map((cat, index) => (
<FormPanel id={cat} key={cat} title={cat}>
{nodes[index]}
</FormPanel>
))}
</Form>
{sections.map((cat, index) => (
<FormPanel id={cat} key={cat} title={cat}>
{nodes[index]}
</FormPanel>
))}
</GridItem>
<GridItem span={4}>
<Nav />

View file

@ -1,11 +1,9 @@
.panel {
padding: 24px;
border-style: solid;
border-color: var(--pf-global--BorderColor--100);
border-width: var(--pf-global--BorderWidth--sm);
padding-top: 24px;
padding-bottom: 48px;
}
.title {
padding-bottom: 8px;
padding-bottom: 24px;
}

View file

@ -1,5 +1,5 @@
.sticky {
position: fixed;
top: 50px;
top: 100px;
}