Import client json file (#55)

* import form

* added confirmation dialog

* introduced page component for clients
This commit is contained in:
Erik Jan de Wit 2020-09-03 21:25:05 +02:00 committed by GitHub
parent a0b2b52b4f
commit f1c9d2e49e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 286 additions and 86 deletions

View file

@ -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>

View 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>
</>
);
};

View 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>
</>
);
};

View file

@ -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>
); );
}; };

View file

@ -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
View 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>
);
};