Realm setting profiles (#1149)

* admin-events-list: added PF code editor

* realm-settings: added template for client policies tab

* realm-settings: fixed space between text and q icon

* admin-events-list: hardcoded representation dialog

* realm-settings: css improvement

* realm-settings: feedback applied

* realm-settings: feedback applied

* admin-events-list: fixed representation dialog

* admin-events-list: fixed representation dialog

* Update src/events/AdminEvents.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* admin-events-list: fixed representation dialog

* real-settings-profile: added subtabs

* realm-settings-list: added table displaying client profiles and delete dialog

* realm-settings-list: updated column name

* realm-settings-list: implemented radio button format change

* realm-settings-list: added buttons for JSON editor

* realm-settings-list: added isPaginated to profiles table

* realm-settings-list: impoved css for json editor

* realm-settings-list: fix for loader

* realm-settings-list: feedback fixes

* realm-settings-list: feedback fixes

* realm-settings-list: feedback fixes

* realm-settings-list: feedback fixes

* realm-settings-list: feedback fixes

* realm-settings-list: feedback fixes

* realm-settings-list: disbaled row when profile is global

* realm-settings-new-profile: added template for new profile - wip

* realm-settings-list: feedback fixes

* Add some @ts-ignore comments for the links

* Split routes into multiple files

* realm-settings: temporarily hardcoded path for cancelling new profile btn, bumped admin-client dep

* realm-settings: reverted back to Link

Co-authored-by: Agnieszka Gancarczyk <agancarc@redhat.com>
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
agagancarczyk 2021-09-20 13:42:07 +01:00 committed by GitHub
parent 9adcc7a52d
commit 2237ee4d65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 361 additions and 15 deletions

14
package-lock.json generated
View file

@ -9,7 +9,7 @@
"version": "0.0.1",
"license": "Apache",
"dependencies": {
"@keycloak/keycloak-admin-client": "^16.0.0-dev.17",
"@keycloak/keycloak-admin-client": "^16.0.0-dev.18",
"@patternfly/patternfly": "^4.135.2",
"@patternfly/react-code-editor": "^4.3.61",
"@patternfly/react-core": "4.157.3",
@ -2220,9 +2220,9 @@
}
},
"node_modules/@keycloak/keycloak-admin-client": {
"version": "16.0.0-dev.17",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.17.tgz",
"integrity": "sha512-3IQVbpzQguU96/ZCAFeE274pl4KvuSbjSDlm0Cget7ptnStc93hp12EcdMMhWbbtL+BqFVmBvIjSmch6f5Fatg==",
"version": "16.0.0-dev.18",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.18.tgz",
"integrity": "sha512-xGriGt20ZuSr4RNs0zS+EwGqfaL7ivKhHw1p3Y56sKtqmxu5+T8/KxxmAcQkqPu8qeUNL9e/8CqYwM4jqF95Jg==",
"dependencies": {
"axios": "^0.21.0",
"camelize": "^1.0.0",
@ -18428,9 +18428,9 @@
}
},
"@keycloak/keycloak-admin-client": {
"version": "16.0.0-dev.17",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.17.tgz",
"integrity": "sha512-3IQVbpzQguU96/ZCAFeE274pl4KvuSbjSDlm0Cget7ptnStc93hp12EcdMMhWbbtL+BqFVmBvIjSmch6f5Fatg==",
"version": "16.0.0-dev.18",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.18.tgz",
"integrity": "sha512-xGriGt20ZuSr4RNs0zS+EwGqfaL7ivKhHw1p3Y56sKtqmxu5+T8/KxxmAcQkqPu8qeUNL9e/8CqYwM4jqF95Jg==",
"requires": {
"axios": "^0.21.0",
"camelize": "^1.0.0",

View file

@ -24,7 +24,7 @@
"prepare": "husky install"
},
"dependencies": {
"@keycloak/keycloak-admin-client": "^16.0.0-dev.17",
"@keycloak/keycloak-admin-client": "^16.0.0-dev.18",
"@patternfly/patternfly": "^4.135.2",
"@patternfly/react-code-editor": "^4.3.61",
"@patternfly/react-core": "4.157.3",

View file

@ -586,7 +586,7 @@ export const AdminEvents = () => {
isReadOnly
code={code}
language={Language.json}
height="125px"
height="8rem"
/>
</DisplayDialog>
)}

View file

@ -0,0 +1,85 @@
import React from "react";
import {
ActionGroup,
Button,
FormGroup,
PageSection,
TextArea,
TextInput,
ValidatedOptions,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { FormAccess } from "../components/form-access/FormAccess";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { Link } from "react-router-dom";
import { useRealm } from "../context/realm-context/RealmContext";
export const NewClientProfileForm = () => {
const { t } = useTranslation("realm-settings");
const { register, errors } = useForm();
const { realm } = useRealm();
function save() {
//TODO
}
return (
<>
<ViewHeader titleKey={t("newClientProfile")} divider />
<PageSection variant="light">
<FormAccess isHorizontal role="view-realm" className="pf-u-mt-lg">
<FormGroup
label={t("newClientProfileName")}
fieldId="kc-name"
isRequired
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register({ required: true })}
type="text"
id="kc-client-profile-name"
name="name"
/>
</FormGroup>
<FormGroup
label={t("common:description")}
fieldId="kc-description"
validated={
errors.description
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={errors.description?.message}
>
<TextArea
name="description"
aria-label={t("description")}
ref={register()}
type="text"
id="kc-client-profile-description"
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
onClick={save}
data-testid="realm-settings-client-profile-save-button"
>
{t("common:save")}
</Button>
<Button
id="cancelCreateProfile"
component={Link}
// @ts-ignore
to={`/${realm}/realm-settings/clientPolicies`}
data-testid="cancelCreateProfile"
>
{t("common:cancel")}
</Button>
</ActionGroup>
</FormAccess>
</PageSection>
</>
);
};

View file

@ -1,12 +1,190 @@
import React from "react";
import { PageSection } from "@patternfly/react-core";
import React, { useMemo, useState } from "react";
import {
AlertVariant,
Button,
ButtonVariant,
Label,
PageSection,
ToolbarItem,
} from "@patternfly/react-core";
import { Divider, Flex, FlexItem, Radio, Title } from "@patternfly/react-core";
import { CodeEditor, Language } from "@patternfly/react-code-editor";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../context/auth/AdminClient";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useRealm } from "../context/realm-context/RealmContext";
import { useAlerts } from "../components/alert/Alerts";
import { Link } from "react-router-dom";
import "./RealmSettingsSection.css";
import type ClientPolicyExecutorRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyExecutorRepresentation";
import { toNewClientProfile } from "./routes/NewClientProfile";
type ClientProfile = {
description?: string;
executors?: ClientPolicyExecutorRepresentation[];
name?: string;
global: boolean;
};
export const ProfilesTab = () => {
const { t } = useTranslation("realm-settings");
const adminClient = useAdminClient();
const { realm } = useRealm();
const { addAlert, addError } = useAlerts();
const [profiles, setProfiles] = useState<ClientProfile[]>();
const [show, setShow] = useState(false);
const loader = async () => {
const allProfiles = await adminClient.clientPolicies.listProfiles({
realm,
includeGlobalProfiles: true,
});
const globalProfiles = allProfiles.globalProfiles?.map(
(globalProfiles) => ({
...globalProfiles,
global: true,
})
);
const profiles = allProfiles.profiles?.map((profiles) => ({
...profiles,
global: false,
}));
const allClientProfiles = globalProfiles?.concat(profiles ?? []);
setProfiles(allClientProfiles);
return allClientProfiles ?? [];
};
const code = useMemo(() => JSON.stringify(profiles, null, 2), [profiles]);
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteClientProfileConfirmTitle"),
messageKey: t("deleteClientProfileConfirm"),
continueButtonLabel: t("delete"),
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
// delete client profile here
addAlert(t("deleteClientSuccess"), AlertVariant.success);
} catch (error) {
addError(t("deleteClientError"), error);
}
},
});
const cellFormatter = (row: ClientProfile) => (
<Link to={""} key={row.name}>
{row.name} {""}
{row.global && <Label color="blue">{t("global")}</Label>}
</Link>
);
return (
<PageSection variant="light" padding={{ default: "noPadding" }}>
<h1>Profiles Tab</h1>
</PageSection>
<>
<DeleteConfirm />
<PageSection>
<Flex className="kc-profiles-config-section">
<FlexItem>
<Title headingLevel="h1" size="md">
{t("profilesConfigType")}
</Title>
</FlexItem>
<FlexItem>
<Radio
isChecked={!show}
name="formView"
onChange={() => setShow(false)}
label={t("profilesConfigTypes.formView")}
id="formView-radioBtn"
className="kc-form-radio-btn pf-u-mr-sm pf-u-ml-sm"
/>
</FlexItem>
<FlexItem>
<Radio
isChecked={show}
name="jsonEditor"
onChange={() => setShow(true)}
label={t("profilesConfigTypes.jsonEditor")}
id="jsonEditor-radioBtn"
className="kc-editor-radio-btn"
/>
</FlexItem>
</Flex>
</PageSection>
<Divider />
{!show ? (
<KeycloakDataTable
ariaLabelKey="userEventsRegistered"
searchPlaceholderKey="realm-settings:clientProfileSearch"
isPaginated
loader={loader}
toolbarItem={
<ToolbarItem>
<Button
id="createProfile"
component={Link}
// @ts-ignore
to={toNewClientProfile({ realm })}
data-testid="createProfile"
>
{t("createClientProfile")}
</Button>
</ToolbarItem>
}
isRowDisabled={(value) => value.global === true}
actions={[
{
title: t("common:delete"),
onRowClick: () => {
toggleDeleteDialog();
},
},
]}
columns={[
{
name: "name",
displayKey: t("clientProfileName"),
cellRenderer: cellFormatter,
},
{
name: "description",
displayKey: t("clientProfileDescription"),
},
]}
emptyState={
<ListEmptyState
message={t("emptyClientProfiles")}
instructions={t("emptyClientProfilesInstructions")}
/>
}
/>
) : (
<>
<div className="pf-u-mt-md pf-u-ml-lg">
<CodeEditor
isLineNumbersVisible
isLanguageLabelVisible
code={code}
language={Language.json}
height="30rem"
/>
</div>
<div className="pf-u-mt-md">
<Button
variant={ButtonVariant.primary}
className="pf-u-mr-md pf-u-ml-lg"
>
{t("save")}
</Button>
<Button variant={ButtonVariant.secondary}> {t("reload")}</Button>
</div>
</>
)}
</>
);
};

View file

@ -180,4 +180,16 @@ article.pf-c-card.pf-m-flat.kc-login-settings-template
.kc-help-text {
margin: var(--pf-global--spacer--sm);
font-size: var(--pf-global--FontSize--sm);
}
.kc-profiles-config-section {
align-items: center;
}
.kc-form-radio-btn > input {
transform: scale(1.6);
}
.kc-editor-radio-btn > input {
transform: scale(1.6);
}

View file

@ -202,6 +202,31 @@ export default {
clientPoliciesTab: "Client policies tab",
clientProfilesSubTab: "Client profiles subtab",
clientPoliciesSubTab: "Client policies subtab",
profilesConfigType: "Configure via:",
profilesConfigTypes: {
formView: "Form view",
jsonEditor: "JSON editor",
},
clientProfileSearch: "Search",
clientProfileName: "Name",
clientProfileDescription: "Description",
emptyClientProfiles: "No profiles",
emptyClientProfilesInstructions:
"There are no profiles, select 'Create client profile' to create a new client profile",
deleteClientProfileConfirmTitle: "Delete profile?",
deleteClientProfileConfirm:
"This action will permanently delete the profile custom-profile. This cannot be undone.",
deleteClientSuccess: "Client profile deleted",
deleteClientError: "Could not delete profile: {{error}}",
createClientProfile: "Create client profile",
allClientPolicies: "Client policies",
newClientProfile: "Create client profile",
newClientProfileName: "Client profile name",
delete: "delete",
save: "Save",
reload: "Reload",
global: "Global",
description: "description",
tokens: "Tokens",
key: "Key",
value: "Value",

View file

@ -6,6 +6,8 @@ import { JavaKeystoreSettingsRoute } from "./routes/JavaKeystoreSettings";
import { RealmSettingsRoute } from "./routes/RealmSettings";
import { RsaGeneratedSettingsRoute } from "./routes/RsaGeneratedSettings";
import { RsaSettingsRoute } from "./routes/RsaSettings";
import { ClientPoliciesRoute } from "./routes/ClientPolicies";
import { NewClientProfileRoute } from "./routes/NewClientProfile";
const routes: RouteDef[] = [
RealmSettingsRoute,
@ -15,6 +17,8 @@ const routes: RouteDef[] = [
JavaKeystoreSettingsRoute,
RsaGeneratedSettingsRoute,
RsaSettingsRoute,
ClientPoliciesRoute,
NewClientProfileRoute,
];
export default routes;

View file

@ -0,0 +1,21 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import { ProfilesTab } from "../ProfilesTab";
export type ClientPoliciesParams = {
realm: string;
};
export const ClientPoliciesRoute: RouteDef = {
path: "/:realm/realm-settings/clientPolicies",
component: ProfilesTab,
breadcrumb: (t) => t("realm-settings:allClientPolicies"),
access: "view-realm",
};
export const toClientPolicies = (
params: ClientPoliciesParams
): LocationDescriptorObject => ({
pathname: generatePath(ClientPoliciesRoute.path, params),
});

View file

@ -0,0 +1,21 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import { NewClientProfileForm } from "../NewClientProfileForm";
export type NewClientProfileParams = {
realm: string;
};
export const NewClientProfileRoute: RouteDef = {
path: "/:realm/realm-settings/clientPolicies/new-client-profile",
component: NewClientProfileForm,
breadcrumb: (t) => t("realm-settings:newClientProfile"),
access: "view-realm",
};
export const toNewClientProfile = (
params: NewClientProfileParams
): LocationDescriptorObject => ({
pathname: generatePath(NewClientProfileRoute.path, params),
});