keycloak-scim/src/clients/authorization/ResourceDetails.tsx

347 lines
11 KiB
TypeScript

import React, { useState } from "react";
import { Link, useHistory, useParams } from "react-router-dom";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
ActionGroup,
Alert,
AlertVariant,
Button,
ButtonVariant,
DropdownItem,
FormGroup,
PageSection,
Switch,
TextInput,
ValidatedOptions,
} from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
import type ResourceServerRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceServerRepresentation";
import { ResourceDetailsParams, toResourceDetails } from "../routes/Resource";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../../components/alert/Alerts";
import { FormAccess } from "../../components/form-access/FormAccess";
import type { KeyValueType } from "../../components/attribute-form/attribute-convert";
import { convertFormValuesToObject, convertToFormValues } from "../../util";
import { MultiLineInput } from "../../components/multi-line-input/MultiLineInput";
import { toAuthorizationTab } from "../routes/AuthenticationTab";
import { ScopePicker } from "./ScopePicker";
import { AttributeInput } from "../../components/attribute-input/AttributeInput";
import "./resource-details.css";
type SubmittedResource = Omit<ResourceRepresentation, "attributes"> & {
attributes: KeyValueType[];
};
export default function ResourceDetails() {
const { t } = useTranslation("clients");
const [client, setClient] = useState<ClientRepresentation>();
const [resource, setResource] = useState<ResourceRepresentation>();
const [permissions, setPermission] =
useState<ResourceServerRepresentation[]>();
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const form = useForm<SubmittedResource>({
shouldUnregister: false,
mode: "onChange",
});
const { register, errors, control, setValue, handleSubmit } = form;
const { id, resourceId, realm } = useParams<ResourceDetailsParams>();
const history = useHistory();
const setupForm = (resource: ResourceRepresentation = {}) => {
convertToFormValues(resource, setValue);
};
useFetch(
() =>
Promise.all([
adminClient.clients.findOne({ id }),
resourceId
? adminClient.clients.getResource({ id, resourceId })
: Promise.resolve(undefined),
resourceId
? adminClient.clients.listPermissionsByResource({ id, resourceId })
: Promise.resolve(undefined),
]),
([client, resource, permissions]) => {
if (!client) {
throw new Error(t("common:notFound"));
}
setClient(client);
setPermission(permissions);
setResource(resource);
setupForm(resource);
},
[]
);
const save = async (submitted: SubmittedResource) => {
const resource = convertFormValuesToObject<
SubmittedResource,
ResourceRepresentation
>(submitted);
try {
if (resourceId) {
await adminClient.clients.updateResource({ id, resourceId }, resource);
} else {
const result = await adminClient.clients.createResource(
{ id },
resource
);
history.push(toResourceDetails({ realm, id, resourceId: result._id! }));
}
addAlert(
t((resourceId ? "update" : "create") + "ResourceSuccess"),
AlertVariant.success
);
} catch (error) {
addError("clients:resourceSaveError", error);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "clients:deleteResource",
children: (
<>
{t("deleteResourceConfirm")}
{permissions?.length !== 0 && (
<Alert
variant="warning"
isInline
isPlain
title={t("deleteResourceWarning")}
className="pf-u-pt-lg"
>
<p className="pf-u-pt-xs">
{permissions?.map((permission) => (
<strong key={permission.id} className="pf-u-pr-md">
{permission.name}
</strong>
))}
</p>
</Alert>
)}
</>
),
continueButtonLabel: "clients:confirm",
onConfirm: async () => {
try {
await adminClient.clients.delResource({
id,
resourceId: resourceId!,
});
addAlert(t("resourceDeletedSuccess"), AlertVariant.success);
history.push(
toAuthorizationTab({ realm, clientId: id, tab: "resources" })
);
} catch (error) {
addError("clients:resourceDeletedError", error);
}
},
});
if (!client) {
return <KeycloakSpinner />;
}
return (
<>
<DeleteConfirm />
<ViewHeader
titleKey={resourceId ? resource?.name! : "clients:createResource"}
dropdownItems={
resourceId
? [
<DropdownItem
key="delete"
data-testid="delete-resource"
onClick={() => toggleDeleteDialog()}
>
{t("common:delete")}
</DropdownItem>,
]
: undefined
}
/>
<PageSection variant="light">
<FormProvider {...form}>
<FormAccess
isHorizontal
role="manage-clients"
className="keycloak__resource-details__form"
onSubmit={handleSubmit(save)}
>
<FormGroup
label={t("owner")}
fieldId="owner"
labelIcon={
<HelpItem
helpText="clients-help:owner"
fieldLabelId="clients:owner"
/>
}
>
<TextInput id="owner" value={client.clientId} isReadOnly />
</FormGroup>
<FormGroup
label={t("common:name")}
fieldId="name"
labelIcon={
<HelpItem
helpText="clients-help:resourceName"
fieldLabelId="name"
/>
}
helperTextInvalid={t("common:required")}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
>
<TextInput
id="name"
name="name"
ref={register({ required: true })}
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("displayName")}
fieldId="displayName"
labelIcon={
<HelpItem
helpText="clients-help:displayName"
fieldLabelId="name"
/>
}
>
<TextInput id="displayName" name="name" ref={register} />
</FormGroup>
<FormGroup
label={t("type")}
fieldId="type"
labelIcon={
<HelpItem helpText="clients-help:type" fieldLabelId="type" />
}
>
<TextInput id="type" name="type" ref={register} />
</FormGroup>
<FormGroup
label={t("uris")}
fieldId="uris"
labelIcon={
<HelpItem
helpText="clients-help:uris"
fieldLabelId="clients:uris"
/>
}
>
<MultiLineInput
name="uris"
aria-label={t("uri")}
addButtonLabel="clients:addUri"
/>
</FormGroup>
<ScopePicker clientId={id} />
<FormGroup
label={t("iconUri")}
fieldId="iconUri"
labelIcon={
<HelpItem
helpText="clients-help:iconUri"
fieldLabelId="clients:iconUri"
/>
}
>
<TextInput id="iconUri" name="icon_uri" ref={register} />
</FormGroup>
<FormGroup
hasNoPaddingTop
label={t("ownerManagedAccess")}
labelIcon={
<HelpItem
helpText="clients-help:ownerManagedAccess"
fieldLabelId="clients:ownerManagedAccess"
/>
}
fieldId="ownerManagedAccess"
>
<Controller
name="ownerManagedAccess"
control={control}
defaultValue={false}
render={({ onChange, value }) => (
<Switch
id="ownerManagedAccess"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
/>
)}
/>
</FormGroup>
<FormGroup
hasNoPaddingTop
label={t("resourceAttribute")}
labelIcon={
<HelpItem
helpText="clients-help:resourceAttribute"
fieldLabelId="clients:resourceAttribute"
/>
}
fieldId="resourceAttribute"
>
<AttributeInput name="attributes" />
</FormGroup>
<ActionGroup>
<div className="pf-u-mt-md">
<Button
variant={ButtonVariant.primary}
type="submit"
data-testid="save"
>
{t("common:save")}
</Button>
<Button
variant="link"
data-testid="cancel"
component={(props) => (
<Link
{...props}
to={toAuthorizationTab({
realm,
clientId: id,
tab: "resources",
})}
></Link>
)}
>
{t("common:cancel")}
</Button>
</div>
</ActionGroup>
</FormAccess>
</FormProvider>
</PageSection>
</>
);
}