Add overview for attributes groups to realm settings (#1822)

This commit is contained in:
Jon Koops 2022-01-07 13:56:27 +01:00 committed by GitHub
parent 439212c4b5
commit ee29a308e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 241 additions and 11 deletions

View file

@ -0,0 +1,54 @@
import ListingPage from "../support/pages/admin_console/ListingPage";
import RealmSettingsPage from "../support/pages/admin_console/manage/realm_settings/RealmSettingsPage";
import SidebarPage from "../support/pages/admin_console/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import AdminClient from "../support/util/AdminClient";
import { keycloakBefore } from "../support/util/keycloak_before";
import ModalUtils from "../support/util/ModalUtils";
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const realmSettingsPage = new RealmSettingsPage();
const adminClient = new AdminClient();
const listingPage = new ListingPage();
const modalUtils = new ModalUtils();
// Selectors
const getUserProfileTab = () =>
cy.findByTestId(realmSettingsPage.userProfileTab);
const getAttributesGroupTab = () => cy.findByTestId("attributesGroupTab");
describe("User profile tabs", () => {
const realmName = "Realm_" + (Math.random() + 1).toString(36).substring(7);
before(() =>
adminClient.createRealm(realmName, {
attributes: { userProfileEnabled: "true" },
})
);
after(() => adminClient.deleteRealm(realmName));
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
sidebarPage.goToRealm(realmName);
sidebarPage.goToRealmSettings();
});
describe("Attribute groups", () => {
it("deletes an attributes group", () => {
cy.wrap(null).then(() =>
adminClient.patchUserProfile(realmName, {
groups: [{ name: "Test" }],
})
);
getUserProfileTab().click();
getAttributesGroupTab().click();
listingPage.deleteItem("Test");
modalUtils.confirmModal();
listingPage.itemExist("Test", false);
});
});
});

View file

@ -2,6 +2,9 @@ import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import type UserProfileConfig from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import { merge } from "lodash";
export default class AdminClient {
private client: KeycloakAdminClient;
@ -21,9 +24,14 @@ export default class AdminClient {
});
}
async createRealm(realm: string) {
async createRealm(realm: string, payload?: RealmRepresentation) {
await this.login();
await this.client.realms.create({ realm });
await this.client.realms.create({ realm, ...payload });
}
async updateRealm(realm: string, payload: RealmRepresentation) {
await this.login();
await this.client.realms.update({ realm }, payload);
}
async deleteRealm(realm: string) {
@ -129,4 +137,14 @@ export default class AdminClient {
clientScopeId: scope?.id!,
});
}
async patchUserProfile(realm: string, payload: UserProfileConfig) {
await this.login();
const currentProfile = await this.client.users.getProfile({ realm });
await this.client.users.updateProfile(
merge(currentProfile, payload, { realm })
);
}
}

View file

@ -33,6 +33,7 @@ export default {
clearFileExplain: "Are you sure you want to clear this file?",
on: "On",
off: "Off",
edit: "Edit",
enabled: "Enabled",
disabled: "Disabled",
disable: "Disable",

View file

@ -781,4 +781,19 @@ export default {
exportSuccess: "Realm successfully exported.",
exportFail: "Could not export realm: '{{error}}'",
},
"attributes-group": {
createButtonText: "Create attributes group",
tableTitle: "Attributes groups",
columnName: "Name",
columnDisplayName: "Display name",
columnDisplayDescription: "Display description",
emptyStateMessage: "No attributes groups",
emptyStateInstructions:
"If you want to add an attributes group click the button below.",
deleteDialogTitle: "Delete attribute group?",
deleteDialogDescription:
"Are you sure you want to permanently delete the attributes group <1>{{group}}</1>?",
deleteSuccess: "Attributes group deleted.",
deleteError: "Could not delete user attributes group: {{error}}",
},
};

View file

@ -0,0 +1,120 @@
import type UserProfileConfig from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import type { UserProfileGroup } from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import {
Button,
ButtonVariant,
PageSection,
ToolbarItem,
} from "@patternfly/react-core";
import React, { useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Link, useHistory } from "react-router-dom";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import type { OnSaveCallback } from "./UserProfileTab";
type AttributesGroupTabProps = {
config?: UserProfileConfig;
onSave: OnSaveCallback;
};
export const AttributesGroupTab = ({
config,
onSave,
}: AttributesGroupTabProps) => {
const { t } = useTranslation();
const history = useHistory();
const [key, setKey] = useState(0);
const [groupToDelete, setGroupToDelete] = useState<UserProfileGroup>();
// Refresh data in table when config changes.
useEffect(() => setKey((value) => value + 1), [config]);
async function loader() {
return config?.groups ?? [];
}
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "attributes-group:deleteDialogTitle",
children: (
<Trans i18nKey="attributes-group:deleteDialogDescription">
{" "}
<strong>{{ group: groupToDelete?.name }}</strong>.
</Trans>
),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm() {
const groups = (config?.groups ?? []).filter(
(group) => group !== groupToDelete
);
onSave(
{ ...config, groups },
{
successMessageKey: "attributes-group:deleteSuccess",
errorMessageKey: "attributes-group:deleteError",
}
);
},
});
function deleteAttributeGroup(group: UserProfileGroup) {
setGroupToDelete(group);
toggleDeleteDialog();
}
return (
<PageSection variant="light" className="pf-u-p-0">
<DeleteConfirm />
<KeycloakDataTable
key={key}
loader={loader}
ariaLabelKey="attributes-group:tableTitle"
toolbarItem={
<ToolbarItem>
{/* TODO: Add link to page */}
<Button component={(props) => <Link {...props} to={{}} />}>
{t("attributes-group:createButtonText")}
</Button>
</ToolbarItem>
}
columns={[
{
name: "name",
displayKey: "attributes-group:columnName",
},
{
name: "displayHeader",
displayKey: "attributes-group:columnDisplayName",
},
{
name: "displayDescription",
displayKey: "attributes-group:columnDisplayDescription",
},
]}
actions={[
{
title: t("common:edit"),
// TODO: Add link to page.
onRowClick: () => history.push({}),
},
{
title: t("common:delete"),
onRowClick: deleteAttributeGroup,
},
]}
emptyState={
<ListEmptyState
message={t("attributes-group:emptyStateMessage")}
instructions={t("attributes-group:emptyStateInstructions")}
primaryActionText={t("attributes-group:createButtonText")}
// TODO: Add link to page.
onPrimaryAction={() => history.push({})}
/>
}
/>
</PageSection>
);
};

View file

@ -1,4 +1,3 @@
import type ClientProfilesRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfilesRepresentation";
import type UserProfileConfig from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import { CodeEditor, Language } from "@patternfly/react-code-editor";
import { ActionGroup, Button, Form, PageSection } from "@patternfly/react-core";
@ -7,10 +6,11 @@ import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAlerts } from "../../components/alert/Alerts";
import { prettyPrintJSON } from "../../util";
import type { OnSaveCallback } from "./UserProfileTab";
type JsonEditorTabProps = {
config?: UserProfileConfig;
onSave: (profiles: ClientProfilesRepresentation) => void;
onSave: OnSaveCallback;
isSaving: boolean;
};

View file

@ -1,4 +1,3 @@
import type ClientProfilesRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfilesRepresentation";
import type UserProfileConfig from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import { AlertVariant, Tab, Tabs, TabTitleText } from "@patternfly/react-core";
import React, { useState } from "react";
@ -6,8 +5,19 @@ import { useTranslation } from "react-i18next";
import { useAlerts } from "../../components/alert/Alerts";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { AttributesGroupTab } from "./AttributesGroupTab";
import { JsonEditorTab } from "./JsonEditorTab";
export type OnSaveCallback = (
updatedProfiles: UserProfileConfig,
options?: OnSaveOptions
) => Promise<void>;
export type OnSaveOptions = {
successMessageKey?: string;
errorMessageKey?: string;
};
export const UserProfileTab = () => {
const adminClient = useAdminClient();
const { realm } = useRealm();
@ -24,23 +34,32 @@ export const UserProfileTab = () => {
[refreshCount]
);
async function onSave(updatedProfiles: ClientProfilesRepresentation) {
const onSave: OnSaveCallback = async (
updatedProfiles: UserProfileConfig,
options?: OnSaveOptions
) => {
setIsSaving(true);
try {
await adminClient.clientPolicies.createProfiles({
await adminClient.users.updateProfile({
...updatedProfiles,
realm,
});
setRefreshCount(refreshCount + 1);
addAlert(t("userProfileSuccess"), AlertVariant.success);
addAlert(
t(options?.successMessageKey ?? "userProfileSuccess"),
AlertVariant.success
);
} catch (error) {
addError("realm-settings:userProfileError", error);
addError(
options?.errorMessageKey ?? "realm-settings:userProfileError",
error
);
}
setIsSaving(false);
}
};
return (
<Tabs
@ -55,7 +74,10 @@ export const UserProfileTab = () => {
<Tab
eventKey="attributesGroup"
title={<TabTitleText>{t("attributesGroup")}</TabTitleText>}
></Tab>
data-testid="attributesGroupTab"
>
<AttributesGroupTab config={config} onSave={onSave} />
</Tab>
<Tab
eventKey="jsonEditor"
title={<TabTitleText>{t("jsonEditor")}</TabTitleText>}