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 { ClientList } from "./clients/ClientList";
|
||||
import { DataLoader } from "./components/data-loader/DataLoader";
|
||||
import { HttpClientContext } from "./http-service/HttpClientContext";
|
||||
import { ClientRepresentation } from "./model/client-model";
|
||||
import React from "react";
|
||||
import { Page, PageSection, Button } from "@patternfly/react-core";
|
||||
import { Header } from "./PageHeader";
|
||||
import { PageNav } from "./PageNav";
|
||||
import { KeycloakContext } from "./auth/KeycloakContext";
|
||||
import { TableToolbar } from "./components/table-toolbar/TableToolbar";
|
||||
|
||||
import { Help } from "./components/help-enabler/HelpHeader";
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Route,
|
||||
Switch,
|
||||
useHistory,
|
||||
} from "react-router-dom";
|
||||
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
|
||||
import { NewRealmForm } from "./forms/realm/NewRealmForm";
|
||||
import { NewClientForm } from "./forms/client/NewClientForm";
|
||||
import { ImportForm } from "./forms/client/ImportForm";
|
||||
import { ClientsPage } from "./page/ClientsPage";
|
||||
|
||||
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 (
|
||||
<Router>
|
||||
<Help>
|
||||
|
@ -72,7 +19,8 @@ export const App = () => {
|
|||
<Switch>
|
||||
<Route exact path="/add-realm" component={NewRealmForm}></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>
|
||||
</PageSection>
|
||||
</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 {
|
||||
FormGroup,
|
||||
TextInput,
|
||||
Form,
|
||||
Select,
|
||||
SelectVariant,
|
||||
SelectOption,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { HttpClientContext } from "../../http-service/HttpClientContext";
|
||||
import { sortProvider } from "../../util";
|
||||
import { ServerInfoRepresentation } from "../../model/server-info";
|
||||
import { ClientRepresentation } from "../../model/client-model";
|
||||
import { ClientDescription } from "./ClientDescription";
|
||||
|
||||
type Step1Props = {
|
||||
onChange: (value: string, event: FormEvent<HTMLInputElement>) => void;
|
||||
|
@ -67,33 +68,7 @@ export const Step1 = ({ client, onChange }: Step1Props) => {
|
|||
))}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
<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>
|
||||
<ClientDescription onChange={onChange} client={client} />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"fullName": "{{givenName}} {{familyName}}",
|
||||
"unknownUser": "Anonymous",
|
||||
"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 *********": "",
|
||||
"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