added save dialog

This commit is contained in:
Erik Jan de Wit 2021-03-04 09:20:37 +01:00
parent 3399b16c7e
commit 3fe5d8383a
8 changed files with 255 additions and 42 deletions

View file

@ -21,7 +21,7 @@ import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { formattedLinkTableCell } from "../components/external-link/FormattedLink"; import { formattedLinkTableCell } from "../components/external-link/FormattedLink";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { InitialAccessTokenList } from "./InitialAccessTokenList"; import { InitialAccessTokenList } from "./initial-access/InitialAccessTokenList";
export const ClientsSection = () => { export const ClientsSection = () => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");

View file

@ -1,41 +0,0 @@
import React from "react";
import moment from "moment";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAdminClient } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
export const InitialAccessTokenList = () => {
const adminClient = useAdminClient();
const { realm } = useRealm();
const loader = async () =>
await adminClient.realms.getClientsInitialAccess({ realm });
return (
<KeycloakDataTable
ariaLabelKey="clients:initialAccessToken"
searchPlaceholderKey="clients:searchInitialAccessToken"
loader={loader}
columns={[
{
name: "id",
},
{
name: "timestamp",
cellRenderer: (row) => moment(row.timestamp * 1000).fromNow(),
},
{
name: "expiration",
cellRenderer: (row) =>
moment(moment.now() - row.expiration * 1000).fromNow(),
},
{
name: "count",
},
{
name: "remainingCount",
},
]}
/>
);
};

View file

@ -4,6 +4,9 @@
"adminURL": "URL to the admin interface of the client. Set this if the client supports the adapter REST API. This REST API allows the auth server to push revocation policies and other administrative tasks. Usually this is set to the base URL of the client.", "adminURL": "URL to the admin interface of the client. Set this if the client supports the adapter REST API. This REST API allows the auth server to push revocation policies and other administrative tasks. Usually this is set to the base URL of the client.",
"downloadType": "this is information about the download type", "downloadType": "this is information about the download type",
"details": "this is information about the details", "details": "this is information about the details",
"createToken": "An initial access token can only be used to create clients",
"expiration": "Specifies how long the token should be valid",
"count": "Specifies how many clients can be created using the token",
"client-authenticator-type": "Client Authenticator used for authentication of this client against Keycloak server", "client-authenticator-type": "Client Authenticator used for authentication of this client against Keycloak server",
"registration-access-token": "The registration access token provides access for clients to the client registration service.", "registration-access-token": "The registration access token provides access for clients to the client registration service.",
"signature-algorithm": "JWA algorithm, which the client needs to use when signing a JWT for authentication. If left blank, the client is allowed to use any algorithm.", "signature-algorithm": "JWA algorithm, which the client needs to use when signing a JWT for authentication. If left blank, the client is allowed to use any algorithm.",

View file

@ -0,0 +1,44 @@
import React from "react";
import { useTranslation } from "react-i18next";
import {
Alert,
AlertVariant,
ClipboardCopy,
Form,
FormGroup,
Modal,
ModalVariant,
} from "@patternfly/react-core";
type AccessTokenDialogProps = {
token: string;
toggleDialog: () => void;
};
export const AccessTokenDialog = ({
token,
toggleDialog,
}: AccessTokenDialogProps) => {
const { t } = useTranslation("clients");
return (
<Modal
title={t("initialAccessTokenDetails")}
isOpen={true}
onClose={toggleDialog}
variant={ModalVariant.medium}
>
<Alert
title={t("copyInitialAccessToken")}
isInline
variant={AlertVariant.warning}
/>
<Form className="pf-u-mt-md">
<FormGroup label={t("initialAccessToken")} fieldId="initialAccessToken">
<ClipboardCopy id="initialAccessToken" isReadOnly>
{token}
</ClipboardCopy>
</FormGroup>
</Form>
</Modal>
);
};

View file

@ -0,0 +1,129 @@
import React, { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form";
import {
ActionGroup,
AlertVariant,
Button,
FormGroup,
NumberInput,
PageSection,
} from "@patternfly/react-core";
import ClientInitialAccessPresentation from "keycloak-admin/lib/defs/clientInitialAccessPresentation";
import { FormAccess } from "../../components/form-access/FormAccess";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { TimeSelector } from "../../components/time-selector/TimeSelector";
import { useHistory } from "react-router-dom";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
import { AccessTokenDialog } from "./AccessTokenDialog";
export const CreateInitialAccessToken = () => {
const { t } = useTranslation("clients");
const { handleSubmit, control } = useForm();
const adminClient = useAdminClient();
const { realm } = useRealm();
const { addAlert } = useAlerts();
const history = useHistory();
const [token, setToken] = useState("");
const save = async (clientToken: ClientInitialAccessPresentation) => {
try {
const access = await adminClient.realms.createClientsInitialAccess(
{ realm },
clientToken
);
setToken(access.token);
} catch (error) {
addAlert(t("tokenSaveError", { error }), AlertVariant.danger);
}
};
return (
<>
{token && (
<AccessTokenDialog token={token} toggleDialog={() => setToken("")} />
)}
<ViewHeader
titleKey="clients:createToken"
subKey="clients-help:createToken"
/>
<PageSection variant="light">
<FormAccess
isHorizontal
role="create-client"
onSubmit={handleSubmit(save)}
>
<FormGroup
label={t("expiration")}
fieldId="expiration"
labelIcon={
<HelpItem
helpText="clients-help:expiration"
forLabel={t("expiration")}
forID="expiration"
/>
}
>
<Controller
name="expiration"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<TimeSelector value={value} onChange={onChange} />
)}
/>
</FormGroup>
<FormGroup
label={t("count")}
fieldId="count"
labelIcon={
<HelpItem
helpText="clients-help:count"
forLabel={t("count")}
forID="count"
/>
}
>
<Controller
name="count"
defaultValue={1}
control={control}
render={({ onChange, value }) => (
<NumberInput
inputName="count"
inputAriaLabel={t("count")}
min={1}
value={value}
onPlus={() => onChange(value + 1)}
onMinus={() => onChange(value - 1)}
onChange={(event) =>
onChange(Number((event.target as HTMLInputElement).value))
}
/>
)}
/>
</FormGroup>
<ActionGroup>
<Button variant="primary" type="submit">
{t("common:save")}
</Button>
<Button
variant="link"
onClick={() =>
history.push(`/${realm}/clients/initialAccessToken`)
}
>
{t("common:cancel")}
</Button>
</ActionGroup>
</FormAccess>
</PageSection>
</>
);
};

View file

@ -0,0 +1,61 @@
import React from "react";
import { useHistory, useRouteMatch } from "react-router-dom";
import moment from "moment";
import { useTranslation } from "react-i18next";
import { Button } from "@patternfly/react-core";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
export const InitialAccessTokenList = () => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const { realm } = useRealm();
const history = useHistory();
const { url } = useRouteMatch();
const loader = async () =>
await adminClient.realms.getClientsInitialAccess({ realm });
return (
<KeycloakDataTable
ariaLabelKey="clients:initialAccessToken"
searchPlaceholderKey="clients:searchInitialAccessToken"
loader={loader}
toolbarItem={
<>
<Button onClick={() => history.push(`${url}/create`)}>
{t("common:create")}
</Button>
</>
}
columns={[
{
name: "id",
displayKey: "clients:id",
},
{
name: "timestamp",
displayKey: "clients:timestamp",
cellRenderer: (row) => moment(row.timestamp * 1000).format("LLL"),
},
{
name: "expiration",
displayKey: "clients:expires",
cellRenderer: (row) =>
moment(row.timestamp * 1000 + row.expiration * 1000).fromNow(),
},
{
name: "count",
displayKey: "clients:count",
},
{
name: "remainingCount",
displayKey: "clients:remainingCount",
},
]}
/>
);
};

View file

@ -74,6 +74,16 @@
"disableConfirm": "If you disable this client, you cannot initiate a login or obtain access tokens.", "disableConfirm": "If you disable this client, you cannot initiate a login or obtain access tokens.",
"clientDeleteConfirm": "If you delete this client, all associated data will be removed.", "clientDeleteConfirm": "If you delete this client, all associated data will be removed.",
"searchInitialAccessToken": "Search token", "searchInitialAccessToken": "Search token",
"createToken": "Create initial access token",
"id": "ID",
"timestamp": "Created date",
"expirs": "Expires",
"count": "Count",
"remainingCount": "Remaining count",
"expiration": "Expiration",
"tokenSaveError": "Could not create initial access token {{error}}",
"initialAccessTokenDetails": "Initial access token details",
"copyInitialAccessToken": "Please copy and paste the initial access token before closing as it can not be retrieved later.",
"clientAuthentication": "Client authentication", "clientAuthentication": "Client authentication",
"authentication": "Authentication", "authentication": "Authentication",
"authenticationFlow": "Authentication flow", "authenticationFlow": "Authentication flow",

View file

@ -27,6 +27,7 @@ import { UserFederationLdapSettings } from "./user-federation/UserFederationLdap
import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm"; import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm";
import { RealmRoleTabs } from "./realm-roles/RealmRoleTabs"; import { RealmRoleTabs } from "./realm-roles/RealmRoleTabs";
import { SearchGroups } from "./groups/SearchGroups"; import { SearchGroups } from "./groups/SearchGroups";
import { CreateInitialAccessToken } from "./clients/initial-access/CreateInitialAccessToken";
export type RouteDef = BreadcrumbsRoute & { export type RouteDef = BreadcrumbsRoute & {
access: AccessType; access: AccessType;
@ -54,6 +55,12 @@ export const routes: RoutesFn = (t: TFunction) => [
breadcrumb: null, breadcrumb: null,
access: "query-clients", access: "query-clients",
}, },
{
path: "/:realm/clients/initialAccessToken/create",
component: CreateInitialAccessToken,
breadcrumb: t("clients:createToken"),
access: "manage-clients",
},
{ {
path: "/:realm/clients/add-client", path: "/:realm/clients/add-client",
component: NewClientForm, component: NewClientForm,