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:
parent
c656d6acee
commit
0f1d93d672
12 changed files with 1710 additions and 55 deletions
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
137
src/clients/ClientSettings.tsx
Normal file
137
src/clients/ClientSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
|
|
1509
src/clients/__tests__/__snapshots__/ClientList.test.tsx.snap
Normal file
1509
src/clients/__tests__/__snapshots__/ClientList.test.tsx.snap
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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}
|
|
@ -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;
|
|
@ -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 />}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
.sticky {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
top: 100px;
|
||||
}
|
Loading…
Reference in a new issue