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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -1,5 +1,7 @@
{ {
"clients": { "clients": {
"clientAuthorization": "Client authorization",
"implicitFlow": "Implicit flow",
"createClient": "Create client", "createClient": "Create client",
"importClient": "Import client", "importClient": "Import client",
"clientID": "Client ID", "clientID": "Client ID",
@ -12,19 +14,23 @@
"capabilityConfig": "Capability config", "capabilityConfig": "Capability config",
"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",
"clientImportError": "Could not import client", "clientImportError": "Could not import client",
"clientImportSuccess": "Client imported succeful", "clientImportSuccess": "Client imported successful",
"clientDeletedSucess": "The client has been deleted", "clientDeletedSuccess": "The client has been deleted",
"clientDeleteError": "Could not delete client:", "clientDeleteError": "Could not delete client:",
"clientAuthentication": "Client authentication", "clientAuthentication": "Client authentication",
"authentication": "Authentication", "authentication": "Authentication",
"authenticationFlow": "Authentication flow", "authenticationFlow": "Authentication flow",
"standardFlow": "Standard flow", "standardFlow": "Standard flow",
"enableStandardFlow": "Enable standard flow",
"directAccess": "Direct access", "directAccess": "Direct access",
"enableDirectAccess": "Enable direct access",
"implicidFlow": "Implicid flow",
"enableImplicidFlow": "Enable implicid flow",
"serviceAccount": "Service account", "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 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 { FormPanel } from "./FormPanel";
import style from "./scroll-form.module.css"; import style from "./scroll-form.module.css";
@ -70,13 +70,11 @@ export const ScrollForm = ({ sections, children }: ScrollFormProps) => {
<> <>
<Grid hasGutter> <Grid hasGutter>
<GridItem span={8}> <GridItem span={8}>
<Form>
{sections.map((cat, index) => ( {sections.map((cat, index) => (
<FormPanel id={cat} key={cat} title={cat}> <FormPanel id={cat} key={cat} title={cat}>
{nodes[index]} {nodes[index]}
</FormPanel> </FormPanel>
))} ))}
</Form>
</GridItem> </GridItem>
<GridItem span={4}> <GridItem span={4}>
<Nav /> <Nav />

View file

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

View file

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