Import client json file (#55)
* import form * added confirmation dialog * introduced page component for clients
This commit is contained in:
parent
a0b2b52b4f
commit
f1c9d2e49e
6 changed files with 286 additions and 86 deletions
64
src/App.tsx
64
src/App.tsx
|
@ -1,69 +1,16 @@
|
||||||
import React, { useContext, useState } from "react";
|
import React from "react";
|
||||||
import { ClientList } from "./clients/ClientList";
|
|
||||||
import { DataLoader } from "./components/data-loader/DataLoader";
|
|
||||||
import { HttpClientContext } from "./http-service/HttpClientContext";
|
|
||||||
import { ClientRepresentation } from "./model/client-model";
|
|
||||||
import { Page, PageSection, Button } from "@patternfly/react-core";
|
import { Page, PageSection, Button } from "@patternfly/react-core";
|
||||||
import { Header } from "./PageHeader";
|
import { Header } from "./PageHeader";
|
||||||
import { PageNav } from "./PageNav";
|
import { PageNav } from "./PageNav";
|
||||||
import { KeycloakContext } from "./auth/KeycloakContext";
|
|
||||||
import { TableToolbar } from "./components/table-toolbar/TableToolbar";
|
|
||||||
|
|
||||||
import { Help } from "./components/help-enabler/HelpHeader";
|
import { Help } from "./components/help-enabler/HelpHeader";
|
||||||
import {
|
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
|
||||||
BrowserRouter as Router,
|
|
||||||
Route,
|
|
||||||
Switch,
|
|
||||||
useHistory,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import { NewRealmForm } from "./forms/realm/NewRealmForm";
|
import { NewRealmForm } from "./forms/realm/NewRealmForm";
|
||||||
import { NewClientForm } from "./forms/client/NewClientForm";
|
import { NewClientForm } from "./forms/client/NewClientForm";
|
||||||
|
import { ImportForm } from "./forms/client/ImportForm";
|
||||||
|
import { ClientsPage } from "./page/ClientsPage";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const [max, setMax] = useState(10);
|
|
||||||
const [first, setFirst] = useState(0);
|
|
||||||
const httpClient = useContext(HttpClientContext);
|
|
||||||
const keycloak = useContext(KeycloakContext);
|
|
||||||
|
|
||||||
const loader = async () => {
|
|
||||||
return await httpClient
|
|
||||||
?.doGet("/admin/realms/master/clients", { params: { first, max } })
|
|
||||||
.then((r) => r.data as ClientRepresentation[]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Clients = () => {
|
|
||||||
const history = useHistory();
|
|
||||||
return (
|
|
||||||
<DataLoader loader={loader}>
|
|
||||||
{(clients) => (
|
|
||||||
<TableToolbar
|
|
||||||
count={clients!.length}
|
|
||||||
first={first}
|
|
||||||
max={max}
|
|
||||||
onNextClick={(f) => setFirst(f)}
|
|
||||||
onPreviousClick={(f) => setFirst(f)}
|
|
||||||
onPerPageSelect={(f, m) => {
|
|
||||||
setFirst(f);
|
|
||||||
setMax(m);
|
|
||||||
}}
|
|
||||||
toolbarItem={
|
|
||||||
<>
|
|
||||||
<Button onClick={() => history.push("/add-client")}>
|
|
||||||
Create client
|
|
||||||
</Button>
|
|
||||||
<Button variant="link">Import client</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ClientList
|
|
||||||
clients={clients}
|
|
||||||
baseUrl={keycloak!.authServerUrl()!}
|
|
||||||
/>
|
|
||||||
</TableToolbar>
|
|
||||||
)}
|
|
||||||
</DataLoader>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Help>
|
<Help>
|
||||||
|
@ -72,7 +19,8 @@ export const App = () => {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/add-realm" component={NewRealmForm}></Route>
|
<Route exact path="/add-realm" component={NewRealmForm}></Route>
|
||||||
<Route exact path="/add-client" component={NewClientForm}></Route>
|
<Route exact path="/add-client" component={NewClientForm}></Route>
|
||||||
<Route exact path="/" component={Clients}></Route>
|
<Route exact path="/import-client" component={ImportForm}></Route>
|
||||||
|
<Route exact path="/" component={ClientsPage}></Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
46
src/forms/client/ClientDescription.tsx
Normal file
46
src/forms/client/ClientDescription.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import React, { FormEvent } from "react";
|
||||||
|
import { FormGroup, TextInput } from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { ClientRepresentation } from "../../model/client-model";
|
||||||
|
|
||||||
|
type ClientDescriptionProps = {
|
||||||
|
onChange: (value: string, event: FormEvent<HTMLInputElement>) => void;
|
||||||
|
client: ClientRepresentation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClientDescription = ({
|
||||||
|
client,
|
||||||
|
onChange,
|
||||||
|
}: ClientDescriptionProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup label="Client ID" fieldId="kc-client-id">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
id="kc-client-id"
|
||||||
|
name="clientId"
|
||||||
|
value={client.clientId}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label="Name" fieldId="kc-name">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
id="kc-name"
|
||||||
|
name="name"
|
||||||
|
value={client.name}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label="Description" fieldId="kc-description">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
id="kc-description"
|
||||||
|
name="description"
|
||||||
|
value={client.description}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
171
src/forms/client/ImportForm.tsx
Normal file
171
src/forms/client/ImportForm.tsx
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import React, { useState, FormEvent, useContext } from "react";
|
||||||
|
import {
|
||||||
|
PageSection,
|
||||||
|
Text,
|
||||||
|
TextContent,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
FormGroup,
|
||||||
|
FileUpload,
|
||||||
|
TextInput,
|
||||||
|
ActionGroup,
|
||||||
|
Button,
|
||||||
|
AlertVariant,
|
||||||
|
Modal,
|
||||||
|
ModalVariant,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { ClientRepresentation } from "../../model/client-model";
|
||||||
|
import { ClientDescription } from "./ClientDescription";
|
||||||
|
import { HttpClientContext } from "../../http-service/HttpClientContext";
|
||||||
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
import { AlertPanel } from "../../components/alert/AlertPanel";
|
||||||
|
|
||||||
|
type FileUpload = {
|
||||||
|
value: string | File;
|
||||||
|
filename: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
modal: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImportForm = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const httpClient = useContext(HttpClientContext)!;
|
||||||
|
|
||||||
|
const [add, alerts, hide] = useAlerts();
|
||||||
|
const defaultUpload = {
|
||||||
|
value: "",
|
||||||
|
filename: "",
|
||||||
|
isLoading: false,
|
||||||
|
modal: false,
|
||||||
|
};
|
||||||
|
const [fileUpload, setFileUpload] = useState<FileUpload>(defaultUpload);
|
||||||
|
const defaultClient = {
|
||||||
|
protocol: "",
|
||||||
|
clientId: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
const [client, setClient] = useState<ClientRepresentation>(defaultClient);
|
||||||
|
|
||||||
|
const handleFileChange = (value: string | File, filename: string) => {
|
||||||
|
if (value === "" && client.protocol !== "") {
|
||||||
|
// clear clicked
|
||||||
|
setFileUpload({ ...fileUpload, modal: true });
|
||||||
|
} else {
|
||||||
|
setFileUpload({
|
||||||
|
...fileUpload,
|
||||||
|
value,
|
||||||
|
filename,
|
||||||
|
});
|
||||||
|
setClient({
|
||||||
|
...client,
|
||||||
|
...(value ? JSON.parse(value as string) : defaultClient),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleDescriptionChange = (
|
||||||
|
value: string,
|
||||||
|
event: FormEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const name = (event.target as HTMLInputElement).name;
|
||||||
|
setClient({ ...client, [name]: value });
|
||||||
|
};
|
||||||
|
const removeDialog = () => setFileUpload({ ...fileUpload, modal: false });
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
await httpClient.doPost("/admin/realms/master/clients", client);
|
||||||
|
add(t("Client imported"), AlertVariant.success);
|
||||||
|
} catch (error) {
|
||||||
|
add(`${t("Could not import client:")} '${error}'`, AlertVariant.danger);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AlertPanel alerts={alerts} onCloseAlert={hide} />
|
||||||
|
<PageSection variant="light">
|
||||||
|
<TextContent>
|
||||||
|
<Text component="h1">{t("Import client")}</Text>
|
||||||
|
{t(
|
||||||
|
"Clients are applications and services that can request authentication of a user"
|
||||||
|
)}
|
||||||
|
</TextContent>
|
||||||
|
</PageSection>
|
||||||
|
<Divider />
|
||||||
|
<PageSection variant="light">
|
||||||
|
{fileUpload.modal && (
|
||||||
|
<Modal
|
||||||
|
variant={ModalVariant.small}
|
||||||
|
title={t("Clear this file")}
|
||||||
|
isOpen
|
||||||
|
onClose={removeDialog}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="confirm"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setClient(defaultClient);
|
||||||
|
setFileUpload(defaultUpload);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("Clear")}
|
||||||
|
</Button>,
|
||||||
|
<Button key="cancel" variant="link" onClick={removeDialog}>
|
||||||
|
{t("Cancel")}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t("confirmImportClear")}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
<Form isHorizontal>
|
||||||
|
<FormGroup
|
||||||
|
label={t("Resource file")}
|
||||||
|
fieldId="realm-file"
|
||||||
|
helperText="Upload a JSON file"
|
||||||
|
>
|
||||||
|
<FileUpload
|
||||||
|
id="realm-file"
|
||||||
|
type="text"
|
||||||
|
value={fileUpload.value}
|
||||||
|
filename={fileUpload.filename}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
allowEditingUploadedText
|
||||||
|
onReadStarted={() =>
|
||||||
|
setFileUpload({ ...fileUpload, isLoading: true })
|
||||||
|
}
|
||||||
|
onReadFinished={() =>
|
||||||
|
setFileUpload({ ...fileUpload, isLoading: false })
|
||||||
|
}
|
||||||
|
isLoading={fileUpload.isLoading}
|
||||||
|
dropzoneProps={{
|
||||||
|
accept: ".json",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<ClientDescription
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
client={client}
|
||||||
|
/>
|
||||||
|
<FormGroup label={t("Type")} fieldId="kc-type">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
id="kc-type"
|
||||||
|
name="protocol"
|
||||||
|
value={client.protocol}
|
||||||
|
isReadOnly
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<ActionGroup>
|
||||||
|
<Button variant="primary" onClick={save}>
|
||||||
|
{t("Save")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="link">{t("Cancel")}</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</Form>
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,16 +1,17 @@
|
||||||
import React, { useState, FormEvent, useEffect, useContext } from "react";
|
import React, { useState, FormEvent, useEffect, useContext } from "react";
|
||||||
import {
|
import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
TextInput,
|
|
||||||
Form,
|
Form,
|
||||||
Select,
|
Select,
|
||||||
SelectVariant,
|
SelectVariant,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
import { HttpClientContext } from "../../http-service/HttpClientContext";
|
import { HttpClientContext } from "../../http-service/HttpClientContext";
|
||||||
import { sortProvider } from "../../util";
|
import { sortProvider } from "../../util";
|
||||||
import { ServerInfoRepresentation } from "../../model/server-info";
|
import { ServerInfoRepresentation } from "../../model/server-info";
|
||||||
import { ClientRepresentation } from "../../model/client-model";
|
import { ClientRepresentation } from "../../model/client-model";
|
||||||
|
import { ClientDescription } from "./ClientDescription";
|
||||||
|
|
||||||
type Step1Props = {
|
type Step1Props = {
|
||||||
onChange: (value: string, event: FormEvent<HTMLInputElement>) => void;
|
onChange: (value: string, event: FormEvent<HTMLInputElement>) => void;
|
||||||
|
@ -67,33 +68,7 @@ export const Step1 = ({ client, onChange }: Step1Props) => {
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup label="Client ID" fieldId="kc-client-id">
|
<ClientDescription onChange={onChange} client={client} />
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
id="kc-client-id"
|
|
||||||
name="clientId"
|
|
||||||
value={client.clientId}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup label="Name" fieldId="kc-name">
|
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
id="kc-name"
|
|
||||||
name="name"
|
|
||||||
value={client.name}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup label="Description" fieldId="kc-description">
|
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
id="kc-description"
|
|
||||||
name="description"
|
|
||||||
value={client.description}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"fullName": "{{givenName}} {{familyName}}",
|
"fullName": "{{givenName}} {{familyName}}",
|
||||||
"unknownUser": "Anonymous",
|
"unknownUser": "Anonymous",
|
||||||
"Keycloak Administration Console": "RH-SSO Administration Console",
|
"Keycloak Administration Console": "RH-SSO Administration Console",
|
||||||
|
"confirmImportClear": "If you clear this file, you need to click Browse button to re-import a valid file",
|
||||||
|
|
||||||
"********* MASTHEAD *********": "",
|
"********* MASTHEAD *********": "",
|
||||||
"Sign out": "Sign out",
|
"Sign out": "Sign out",
|
||||||
|
|
59
src/page/ClientsPage.tsx
Normal file
59
src/page/ClientsPage.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import React, { useState, useContext } from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button } from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { DataLoader } from "../components/data-loader/DataLoader";
|
||||||
|
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
|
||||||
|
import { ClientList } from "../clients/ClientList";
|
||||||
|
import { HttpClientContext } from "../http-service/HttpClientContext";
|
||||||
|
import { KeycloakContext } from "../auth/KeycloakContext";
|
||||||
|
import { ClientRepresentation } from "../model/client-model";
|
||||||
|
|
||||||
|
export const ClientsPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const history = useHistory();
|
||||||
|
const [max, setMax] = useState(10);
|
||||||
|
const [first, setFirst] = useState(0);
|
||||||
|
const httpClient = useContext(HttpClientContext)!;
|
||||||
|
const keycloak = useContext(KeycloakContext);
|
||||||
|
|
||||||
|
const loader = async () => {
|
||||||
|
return await httpClient
|
||||||
|
.doGet("/admin/realms/master/clients", { params: { first, max } })
|
||||||
|
.then((r) => r.data as ClientRepresentation[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataLoader loader={loader}>
|
||||||
|
{(clients) => (
|
||||||
|
<TableToolbar
|
||||||
|
count={clients!.length}
|
||||||
|
first={first}
|
||||||
|
max={max}
|
||||||
|
onNextClick={setFirst}
|
||||||
|
onPreviousClick={setFirst}
|
||||||
|
onPerPageSelect={(f, m) => {
|
||||||
|
setFirst(f);
|
||||||
|
setMax(m);
|
||||||
|
}}
|
||||||
|
toolbarItem={
|
||||||
|
<>
|
||||||
|
<Button onClick={() => history.push("/add-client")}>
|
||||||
|
{t("Create client")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => history.push("/import-client")}
|
||||||
|
variant="link"
|
||||||
|
>
|
||||||
|
{t("Import client")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ClientList clients={clients} baseUrl={keycloak!.authServerUrl()!} />
|
||||||
|
</TableToolbar>
|
||||||
|
)}
|
||||||
|
</DataLoader>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in a new issue