Realm-settings -> User Profile -> Attributes (#2107)

* attributes tab - wip

* attributes tab - wip

* attributes tab - wip

* attributes tab - wip

* added dropdown for each attribute row

* added kebab logic

* added delete dialog - wip

* added delete dialog - wip

* added delete dialog - wip

* draggable rows - wip

* draggable rows - wip

* draggable rows - wip

* refactored draggable rows

* refactored draggable rows

* added tests

* added tests

* refactor

* refactor

* refactor

* renamed css file to realm-settings-section.css

Co-authored-by: Agnieszka Gancarczyk <agancarc@redhat.com>
This commit is contained in:
agagancarczyk 2022-02-16 11:39:08 +00:00 committed by GitHub
parent 42d8c31e84
commit a6904be9ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 347 additions and 23 deletions

View file

@ -1,5 +1,5 @@
import ListingPage from "../support/pages/admin_console/ListingPage"; import ListingPage from "../support/pages/admin_console/ListingPage";
import RealmSettingsPage from "../support/pages/admin_console/manage/realm_settings/RealmSettingsPage"; import UserProfile from "../support/pages/admin_console/manage/realm_settings/UserProfile";
import SidebarPage from "../support/pages/admin_console/SidebarPage"; import SidebarPage from "../support/pages/admin_console/SidebarPage";
import LoginPage from "../support/pages/LoginPage"; import LoginPage from "../support/pages/LoginPage";
import AdminClient from "../support/util/AdminClient"; import AdminClient from "../support/util/AdminClient";
@ -8,15 +8,18 @@ import ModalUtils from "../support/util/ModalUtils";
const loginPage = new LoginPage(); const loginPage = new LoginPage();
const sidebarPage = new SidebarPage(); const sidebarPage = new SidebarPage();
const realmSettingsPage = new RealmSettingsPage(); const userProfileTab = new UserProfile();
const adminClient = new AdminClient(); const adminClient = new AdminClient();
const listingPage = new ListingPage(); const listingPage = new ListingPage();
const modalUtils = new ModalUtils(); const modalUtils = new ModalUtils();
// Selectors // Selectors
const getUserProfileTab = () => const getUserProfileTab = () => userProfileTab.goToTab();
cy.findByTestId(realmSettingsPage.userProfileTab); const getAttributesTab = () => userProfileTab.goToAttributesTab();
const getAttributesGroupTab = () => cy.findByTestId("attributesGroupTab"); const getAttributesGroupTab = () => userProfileTab.goToAttributesGroupTab();
const getJsonEditorTab = () => userProfileTab.goToJsonEditorTab();
const clickCreateAttributeButton = () =>
userProfileTab.createAttributeButtonClick();
describe("User profile tabs", () => { describe("User profile tabs", () => {
const realmName = "Realm_" + (Math.random() + 1).toString(36).substring(7); const realmName = "Realm_" + (Math.random() + 1).toString(36).substring(7);
@ -36,19 +39,35 @@ describe("User profile tabs", () => {
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
}); });
describe("Attribute groups", () => { describe("Attributes sub tab tests", () => {
it("deletes an attributes group", () => { it("Goes to create attribute page", () => {
getUserProfileTab();
getAttributesTab();
clickCreateAttributeButton();
cy.get("p").should("have.text", "Create attribute");
});
});
describe("Attribute groups sub tab tests", () => {
it("Deletes an attributes group", () => {
cy.wrap(null).then(() => cy.wrap(null).then(() =>
adminClient.patchUserProfile(realmName, { adminClient.patchUserProfile(realmName, {
groups: [{ name: "Test" }], groups: [{ name: "Test" }],
}) })
); );
getUserProfileTab().click(); getUserProfileTab();
getAttributesGroupTab().click(); getAttributesGroupTab();
listingPage.deleteItem("Test"); listingPage.deleteItem("Test");
modalUtils.confirmModal(); modalUtils.confirmModal();
listingPage.itemExist("Test", false); listingPage.itemExist("Test", false);
}); });
}); });
describe("Json Editor sub tab tests", () => {
it("Goes to Json Editor tab", () => {
getUserProfileTab();
getJsonEditorTab();
});
});
}); });

View file

@ -0,0 +1,50 @@
export default class UserProfile {
private userProfileTab = "rs-user-profile-tab";
private attributesTab = "attributesTab";
private attributesGroupTab = "attributesGroupTab";
private jsonEditorTab = "jsonEditorTab";
private createAttributeButton = "createAttributeBtn";
private actionsDrpDwn = "actions-dropdown";
private deleteDrpDwnOption = "deleteDropdownAttributeItem";
private editDrpDwnOption = "editDropdownAttributeItem";
goToTab() {
cy.findByTestId(this.userProfileTab).click();
return this;
}
goToAttributesTab() {
cy.findByTestId(this.attributesTab).click();
return this;
}
goToAttributesGroupTab() {
cy.findByTestId(this.attributesGroupTab).click();
return this;
}
goToJsonEditorTab() {
cy.findByTestId(this.jsonEditorTab).click();
return this;
}
createAttributeButtonClick() {
cy.findByTestId(this.createAttributeButton).click();
return this;
}
selectDropdown() {
cy.findByTestId(this.actionsDrpDwn).click();
return this;
}
selectDeleteOption() {
cy.findByTestId(this.deleteDrpDwnOption).click();
return this;
}
selectEditOption() {
cy.findByTestId(this.editDrpDwnOption).click();
return this;
}
}

View file

@ -31,7 +31,7 @@ import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation"; import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
import { HelpItem } from "../components/help-enabler/HelpItem"; import { HelpItem } from "../components/help-enabler/HelpItem";
import { PlusCircleIcon, TrashIcon } from "@patternfly/react-icons"; import { PlusCircleIcon, TrashIcon } from "@patternfly/react-icons";
import "./RealmSettingsSection.css"; import "./realm-settings-section.css";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { toAddExecutor } from "./routes/AddExecutor"; import { toAddExecutor } from "./routes/AddExecutor";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";

View file

@ -21,7 +21,7 @@ import { useRealm } from "../context/realm-context/RealmContext";
import { useWhoAmI } from "../context/whoami/WhoAmI"; import { useWhoAmI } from "../context/whoami/WhoAmI";
import { emailRegexPattern } from "../util"; import { emailRegexPattern } from "../util";
import { AddUserEmailModal } from "./AddUserEmailModal"; import { AddUserEmailModal } from "./AddUserEmailModal";
import "./RealmSettingsSection.css"; import "./realm-settings-section.css";
type RealmSettingsEmailTabProps = { type RealmSettingsEmailTabProps = {
realm: RealmRepresentation; realm: RealmRepresentation;

View file

@ -20,7 +20,7 @@ import { emptyFormatter } from "../util";
import { useAdminClient } from "../context/auth/AdminClient"; import { useAdminClient } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import "./RealmSettingsSection.css"; import "./realm-settings-section.css";
import { FilterIcon } from "@patternfly/react-icons"; import { FilterIcon } from "@patternfly/react-icons";
type KeyData = KeyMetadataRepresentation & { type KeyData = KeyMetadataRepresentation & {
@ -72,7 +72,7 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
const activeKeysCopy = keys!.filter((i) => i.status === "ACTIVE"); const activeKeysCopy = keys!.filter((i) => i.status === "ACTIVE");
return activeKeysCopy?.map((key) => { return activeKeysCopy.map((key) => {
const provider = realmComponents.find( const provider = realmComponents.find(
(component: ComponentRepresentation) => component.id === key.providerId (component: ComponentRepresentation) => component.id === key.providerId
); );
@ -88,7 +88,7 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
const passiveKeys = keys!.filter((i) => i.status === "PASSIVE"); const passiveKeys = keys!.filter((i) => i.status === "PASSIVE");
return passiveKeys?.map((key) => { return passiveKeys.map((key) => {
const provider = realmComponents.find( const provider = realmComponents.find(
(component: ComponentRepresentation) => component.id === key.providerId (component: ComponentRepresentation) => component.id === key.providerId
); );
@ -104,7 +104,7 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
const disabledKeys = keys!.filter((i) => i.status === "DISABLED"); const disabledKeys = keys!.filter((i) => i.status === "DISABLED");
return disabledKeys?.map((key) => { return disabledKeys.map((key) => {
const provider = realmComponents!.find( const provider = realmComponents!.find(
(component: ComponentRepresentation) => component.id === key.providerId (component: ComponentRepresentation) => component.id === key.providerId
); );

View file

@ -31,7 +31,7 @@ import type { KeyMetadataRepresentation } from "@keycloak/keycloak-admin-client/
import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation"; import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation";
import type ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentTypeRepresentation"; import type ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentTypeRepresentation";
import "./RealmSettingsSection.css"; import "./realm-settings-section.css";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { useAdminClient } from "../context/auth/AdminClient"; import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
@ -450,7 +450,7 @@ export const KeysProvidersTab = ({
}: KeysProps) => { }: KeysProps) => {
return ( return (
<KeysTabInner <KeysTabInner
components={realmComponents?.map((component) => { components={realmComponents.map((component) => {
const provider = keyProviderComponentTypes.find( const provider = keyProviderComponentTypes.find(
(componentType: ComponentTypeRepresentation) => (componentType: ComponentTypeRepresentation) =>
component.providerId === componentType.id component.providerId === componentType.id

View file

@ -0,0 +1,23 @@
import { PageSection } from "@patternfly/react-core";
import React from "react";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../components/form-access/FormAccess";
import "./realm-settings-section.css";
export default function NewAttributeSettings() {
const { t } = useTranslation("realm-settings");
return (
<PageSection variant="light">
<FormAccess
onSubmit={() => console.log("TODO handle submit")}
isHorizontal
role="view-realm"
className="pf-u-mt-lg"
>
<p>{t("createAttribute")}</p>
</FormAccess>
</PageSection>
);
}

View file

@ -44,7 +44,7 @@ import { AddClientProfileModal } from "./AddClientProfileModal";
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation"; import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
import { toClientPolicies } from "./routes/ClientPolicies"; import { toClientPolicies } from "./routes/ClientPolicies";
import "./RealmSettingsSection.css"; import "./realm-settings-section.css";
type NewClientPolicyForm = Required<ClientPolicyRepresentation>; type NewClientPolicyForm = Required<ClientPolicyRepresentation>;

View file

@ -24,7 +24,7 @@ import type ClientPolicyRepresentation from "@keycloak/keycloak-admin-client/lib
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import "./RealmSettingsSection.css"; import "./realm-settings-section.css";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { toAddClientPolicy } from "./routes/AddClientPolicy"; import { toAddClientPolicy } from "./routes/AddClientPolicy";
import { toEditClientPolicy } from "./routes/EditClientPolicy"; import { toEditClientPolicy } from "./routes/EditClientPolicy";

View file

@ -26,7 +26,7 @@ import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/li
import { toClientProfile } from "./routes/ClientProfile"; import { toClientProfile } from "./routes/ClientProfile";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import "./RealmSettingsSection.css"; import "./realm-settings-section.css";
type ClientProfile = ClientProfileRepresentation & { type ClientProfile = ClientProfileRepresentation & {
global: boolean; global: boolean;

View file

@ -15,7 +15,7 @@ import { HelpItem } from "../components/help-enabler/HelpItem";
import { FormPanel } from "../components/scroll-form/FormPanel"; import { FormPanel } from "../components/scroll-form/FormPanel";
import { TimeSelector } from "../components/time-selector/TimeSelector"; import { TimeSelector } from "../components/time-selector/TimeSelector";
import "./RealmSettingsSection.css"; import "./realm-settings-section.css";
type RealmSettingsSessionsTabProps = { type RealmSettingsSessionsTabProps = {
realm: RealmRepresentation; realm: RealmRepresentation;

View file

@ -23,7 +23,7 @@ import { TimeSelector } from "../components/time-selector/TimeSelector";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { forHumans, interpolateTimespan } from "../util"; import { forHumans, interpolateTimespan } from "../util";
import "./RealmSettingsSection.css"; import "./realm-settings-section.css";
type RealmSettingsSessionsTabProps = { type RealmSettingsSessionsTabProps = {
realm: RealmRepresentation; realm: RealmRepresentation;

View file

@ -367,6 +367,18 @@ export default {
updateMessageBundleSuccess: "Success! Message bundle updated.", updateMessageBundleSuccess: "Success! Message bundle updated.",
updateMessageBundleError: "Error updating message bundle.", updateMessageBundleError: "Error updating message bundle.",
addMessageBundleError: "Error creating message bundle, {{error}}", addMessageBundleError: "Error creating message bundle, {{error}}",
attributeName: "Name",
attributeDisplayName: "Display name",
attributeGroup: "Attribute group",
updatedUserProfileSuccess: "User Profile configuration has been saved",
updatedUserProfileError: "User Profile configuration hasn't been saved",
createAttribute: "Create attribute",
attributesDropdown: "Attributes dropdown",
deleteAttributeConfirmTitle: "Delete attribute?",
deleteAttributeConfirm:
"Are you sure you want to permanently delete the attribute {{attributeName}}?",
deleteAttributeSuccess: "Attribute deleted",
deleteAttributeError: "",
eventType: "Event saved type", eventType: "Event saved type",
searchEventType: "Search saved event type", searchEventType: "Search saved event type",
addSavedTypes: "Add saved types", addSavedTypes: "Add saved types",

View file

@ -16,6 +16,7 @@ import { EditClientPolicyRoute } from "./routes/EditClientPolicy";
import { NewClientPolicyConditionRoute } from "./routes/AddCondition"; import { NewClientPolicyConditionRoute } from "./routes/AddCondition";
import { EditClientPolicyConditionRoute } from "./routes/EditCondition"; import { EditClientPolicyConditionRoute } from "./routes/EditCondition";
import { UserProfileRoute } from "./routes/UserProfile"; import { UserProfileRoute } from "./routes/UserProfile";
import { AddAttributeRoute } from "./routes/AddAttribute";
import { KeysRoute } from "./routes/KeysTab"; import { KeysRoute } from "./routes/KeysTab";
const routes: RouteDef[] = [ const routes: RouteDef[] = [
@ -37,6 +38,7 @@ const routes: RouteDef[] = [
NewClientPolicyConditionRoute, NewClientPolicyConditionRoute,
EditClientPolicyConditionRoute, EditClientPolicyConditionRoute,
UserProfileRoute, UserProfileRoute,
AddAttributeRoute,
]; ];
export default routes; export default routes;

View file

@ -0,0 +1,21 @@
import type { LocationDescriptorObject } from "history";
import { lazy } from "react";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
export type AddAttributeParams = {
realm: string;
};
export const AddAttributeRoute: RouteDef = {
path: "/:realm/realm-settings/userProfile/attributes/add-attribute",
component: lazy(() => import("../NewAttributeSettings")),
breadcrumb: (t) => t("realmSettings"),
access: "view-realm",
};
export const toAddAttribute = (
params: AddAttributeParams
): LocationDescriptorObject => ({
pathname: generatePath(AddAttributeRoute.path, params),
});

View file

@ -0,0 +1,192 @@
import React, { Fragment, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
ButtonVariant,
Divider,
Dropdown,
DropdownItem,
KebabToggle,
ToolbarItem,
} from "@patternfly/react-core";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
import { DraggableTable } from "../../authentication/components/DraggableTable";
import type { UserProfileAttribute } from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import { useHistory } from "react-router-dom";
import { toAddAttribute } from "../routes/AddAttribute";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useUserProfile } from "./UserProfileContext";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
type movedAttributeType = UserProfileAttribute;
export const AttributesTab = () => {
const { config, save } = useUserProfile();
const { realm: realmName } = useRealm();
const { t } = useTranslation("realm-settings");
const history = useHistory();
const [attributeToDelete, setAttributeToDelete] =
useState<{ name: string }>();
const [kebabOpen, setKebabOpen] = useState({
status: false,
rowKey: "",
});
const executeMove = async (
attribute: UserProfileAttribute,
newIndex: number
) => {
const fromIndex = config?.attributes!.findIndex((attr) => {
return attr.name === attribute.name;
});
let movedAttribute: movedAttributeType = {};
movedAttribute = config?.attributes![fromIndex!]!;
config?.attributes!.splice(fromIndex!, 1);
config?.attributes!.splice(newIndex, 0, movedAttribute);
save(
{ attributes: config?.attributes! },
{
successMessageKey: "realm-settings:updatedUserProfileSuccess",
errorMessageKey: "realm-settings:updatedUserProfileError",
}
);
};
const goToCreate = () => history.push(toAddAttribute({ realm: realmName }));
const updatedAttributes = config?.attributes!.filter(
(attribute) => attribute.name !== attributeToDelete?.name
);
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteAttributeConfirmTitle"),
messageKey: t("deleteAttributeConfirm", {
attributeName: attributeToDelete?.name!,
}),
continueButtonLabel: t("common:delete"),
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
save(
{ attributes: updatedAttributes! },
{
successMessageKey: "realm-settings:deleteAttributeSuccess",
errorMessageKey: "realm-settings:deleteAttributeError",
}
);
setAttributeToDelete({
name: "",
});
},
});
if (!config) {
return <KeycloakSpinner />;
}
return (
<>
<div className="pf-u-mt-md pf-u-mb-md pf-u-ml-md">
<ToolbarItem className="kc-toolbar-attributesTab">
<Button
data-testid="createAttributeBtn"
variant="primary"
onClick={goToCreate}
>
{t("createAttribute")}
</Button>
</ToolbarItem>
</div>
<Divider />
<DeleteConfirm />
<DraggableTable
keyField="name"
onDragFinish={async (nameDragged, items) => {
const keys = config.attributes!.map((e) => e.name);
const newIndex = items.indexOf(nameDragged);
const oldIndex = keys.indexOf(nameDragged);
const dragged = config.attributes![oldIndex];
if (!dragged.name) return;
executeMove(dragged, newIndex);
}}
columns={[
{
name: "name",
displayKey: t("attributeName"),
},
{
name: "displayName",
displayKey: t("attributeDisplayName"),
},
{
name: "group",
displayKey: t("attributeGroup"),
},
{
name: "",
displayKey: "",
cellRenderer: (row) => (
<Dropdown
id={`${row.name}`}
label={t("attributesDropdown")}
data-testid="actions-dropdown"
toggle={
<KebabToggle
onToggle={(status) =>
setKebabOpen({
status,
rowKey: row.name!,
})
}
id={`toggle-${row.name}`}
/>
}
isOpen={kebabOpen.status && kebabOpen.rowKey === row.name}
isPlain
dropdownItems={[
<DropdownItem
key={`edit-dropdown-item-${row.name}`}
data-testid="editDropdownAttributeItem"
onClick={() => {
setKebabOpen({
status: false,
rowKey: row.name!,
});
}}
>
{t("common:edit")}
</DropdownItem>,
<Fragment key={`delete-dropdown-${row.name}`}>
{row.name !== "email" && row.name !== "username"
? [
<DropdownItem
key={`delete-dropdown-item-${row.name}`}
data-testid="deleteDropdownAttributeItem"
onClick={() => {
toggleDeleteDialog();
setAttributeToDelete({
name: row.name!,
});
setKebabOpen({
status: false,
rowKey: row.name!,
});
}}
>
{t("common:delete")}
</DropdownItem>,
]
: []}
</Fragment>,
]}
/>
),
},
]}
data={config.attributes!}
/>
</>
);
};

View file

@ -9,6 +9,7 @@ import {
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import { toUserProfile } from "../routes/UserProfile"; import { toUserProfile } from "../routes/UserProfile";
import { AttributesGroupTab } from "./AttributesGroupTab"; import { AttributesGroupTab } from "./AttributesGroupTab";
import { AttributesTab } from "./AttributesTab";
import { JsonEditorTab } from "./JsonEditorTab"; import { JsonEditorTab } from "./JsonEditorTab";
import { UserProfileProvider } from "./UserProfileContext"; import { UserProfileProvider } from "./UserProfileContext";
@ -25,11 +26,14 @@ export const UserProfileTab = () => {
> >
<Tab <Tab
title={<TabTitleText>{t("attributes")}</TabTitleText>} title={<TabTitleText>{t("attributes")}</TabTitleText>}
data-testid="attributesTab"
{...routableTab({ {...routableTab({
to: toUserProfile({ realm, tab: "attributes" }), to: toUserProfile({ realm, tab: "attributes" }),
history, history,
})} })}
></Tab> >
<AttributesTab />
</Tab>
<Tab <Tab
title={<TabTitleText>{t("attributesGroup")}</TabTitleText>} title={<TabTitleText>{t("attributesGroup")}</TabTitleText>}
data-testid="attributesGroupTab" data-testid="attributesGroupTab"
@ -42,6 +46,7 @@ export const UserProfileTab = () => {
</Tab> </Tab>
<Tab <Tab
title={<TabTitleText>{t("jsonEditor")}</TabTitleText>} title={<TabTitleText>{t("jsonEditor")}</TabTitleText>}
data-testid="jsonEditorTab"
{...routableTab({ {...routableTab({
to: toUserProfile({ realm, tab: "jsonEditor" }), to: toUserProfile({ realm, tab: "jsonEditor" }),
history, history,