Add overview for attributes groups to realm settings (#1822)
This commit is contained in:
parent
439212c4b5
commit
ee29a308e8
7 changed files with 241 additions and 11 deletions
54
cypress/integration/user_profile_tab.spec.ts
Normal file
54
cypress/integration/user_profile_tab.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}}",
|
||||
},
|
||||
};
|
||||
|
|
120
src/realm-settings/user-profile/AttributesGroupTab.tsx
Normal file
120
src/realm-settings/user-profile/AttributesGroupTab.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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>}
|
||||
|
|
Loading…
Reference in a new issue