From a6904be9ff21c970f06733fbe0ecc45ab1d8ef65 Mon Sep 17 00:00:00 2001 From: agagancarczyk Date: Wed, 16 Feb 2022 11:39:08 +0000 Subject: [PATCH] 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 --- ...> realm_settings_user_profile_tab.spec.ts} | 37 +++- .../manage/realm_settings/UserProfile.ts | 50 +++++ src/realm-settings/ClientProfileForm.tsx | 2 +- src/realm-settings/EmailTab.tsx | 2 +- src/realm-settings/KeysListTab.tsx | 8 +- src/realm-settings/KeysProvidersTab.tsx | 4 +- src/realm-settings/NewAttributeSettings.tsx | 23 +++ src/realm-settings/NewClientPolicyForm.tsx | 2 +- src/realm-settings/PoliciesTab.tsx | 2 +- src/realm-settings/ProfilesTab.tsx | 2 +- src/realm-settings/SessionsTab.tsx | 2 +- src/realm-settings/TokensTab.tsx | 2 +- src/realm-settings/messages.ts | 12 ++ ...Section.css => realm-settings-section.css} | 0 src/realm-settings/routes.ts | 2 + src/realm-settings/routes/AddAttribute.ts | 21 ++ .../user-profile/AttributesTab.tsx | 192 ++++++++++++++++++ .../user-profile/UserProfileTab.tsx | 7 +- 18 files changed, 347 insertions(+), 23 deletions(-) rename cypress/integration/{user_profile_tab.spec.ts => realm_settings_user_profile_tab.spec.ts} (54%) create mode 100644 cypress/support/pages/admin_console/manage/realm_settings/UserProfile.ts create mode 100644 src/realm-settings/NewAttributeSettings.tsx rename src/realm-settings/{RealmSettingsSection.css => realm-settings-section.css} (100%) create mode 100644 src/realm-settings/routes/AddAttribute.ts create mode 100644 src/realm-settings/user-profile/AttributesTab.tsx diff --git a/cypress/integration/user_profile_tab.spec.ts b/cypress/integration/realm_settings_user_profile_tab.spec.ts similarity index 54% rename from cypress/integration/user_profile_tab.spec.ts rename to cypress/integration/realm_settings_user_profile_tab.spec.ts index abb2ca7f31..b6aa56e79e 100644 --- a/cypress/integration/user_profile_tab.spec.ts +++ b/cypress/integration/realm_settings_user_profile_tab.spec.ts @@ -1,5 +1,5 @@ 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 LoginPage from "../support/pages/LoginPage"; import AdminClient from "../support/util/AdminClient"; @@ -8,15 +8,18 @@ import ModalUtils from "../support/util/ModalUtils"; const loginPage = new LoginPage(); const sidebarPage = new SidebarPage(); -const realmSettingsPage = new RealmSettingsPage(); +const userProfileTab = new UserProfile(); const adminClient = new AdminClient(); const listingPage = new ListingPage(); const modalUtils = new ModalUtils(); // Selectors -const getUserProfileTab = () => - cy.findByTestId(realmSettingsPage.userProfileTab); -const getAttributesGroupTab = () => cy.findByTestId("attributesGroupTab"); +const getUserProfileTab = () => userProfileTab.goToTab(); +const getAttributesTab = () => userProfileTab.goToAttributesTab(); +const getAttributesGroupTab = () => userProfileTab.goToAttributesGroupTab(); +const getJsonEditorTab = () => userProfileTab.goToJsonEditorTab(); +const clickCreateAttributeButton = () => + userProfileTab.createAttributeButtonClick(); describe("User profile tabs", () => { const realmName = "Realm_" + (Math.random() + 1).toString(36).substring(7); @@ -36,19 +39,35 @@ describe("User profile tabs", () => { sidebarPage.goToRealmSettings(); }); - describe("Attribute groups", () => { - it("deletes an attributes group", () => { + describe("Attributes sub tab tests", () => { + 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(() => adminClient.patchUserProfile(realmName, { groups: [{ name: "Test" }], }) ); - getUserProfileTab().click(); - getAttributesGroupTab().click(); + getUserProfileTab(); + getAttributesGroupTab(); listingPage.deleteItem("Test"); modalUtils.confirmModal(); listingPage.itemExist("Test", false); }); }); + + describe("Json Editor sub tab tests", () => { + it("Goes to Json Editor tab", () => { + getUserProfileTab(); + getJsonEditorTab(); + }); + }); }); diff --git a/cypress/support/pages/admin_console/manage/realm_settings/UserProfile.ts b/cypress/support/pages/admin_console/manage/realm_settings/UserProfile.ts new file mode 100644 index 0000000000..7413b5a96c --- /dev/null +++ b/cypress/support/pages/admin_console/manage/realm_settings/UserProfile.ts @@ -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; + } +} diff --git a/src/realm-settings/ClientProfileForm.tsx b/src/realm-settings/ClientProfileForm.tsx index 670bf5bb7d..f088042525 100644 --- a/src/realm-settings/ClientProfileForm.tsx +++ b/src/realm-settings/ClientProfileForm.tsx @@ -31,7 +31,7 @@ import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation"; import { HelpItem } from "../components/help-enabler/HelpItem"; import { PlusCircleIcon, TrashIcon } from "@patternfly/react-icons"; -import "./RealmSettingsSection.css"; +import "./realm-settings-section.css"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { toAddExecutor } from "./routes/AddExecutor"; import { useServerInfo } from "../context/server-info/ServerInfoProvider"; diff --git a/src/realm-settings/EmailTab.tsx b/src/realm-settings/EmailTab.tsx index 2eee817cff..b1b29188ea 100644 --- a/src/realm-settings/EmailTab.tsx +++ b/src/realm-settings/EmailTab.tsx @@ -21,7 +21,7 @@ import { useRealm } from "../context/realm-context/RealmContext"; import { useWhoAmI } from "../context/whoami/WhoAmI"; import { emailRegexPattern } from "../util"; import { AddUserEmailModal } from "./AddUserEmailModal"; -import "./RealmSettingsSection.css"; +import "./realm-settings-section.css"; type RealmSettingsEmailTabProps = { realm: RealmRepresentation; diff --git a/src/realm-settings/KeysListTab.tsx b/src/realm-settings/KeysListTab.tsx index 5f7d866f31..ab8b1b3640 100644 --- a/src/realm-settings/KeysListTab.tsx +++ b/src/realm-settings/KeysListTab.tsx @@ -20,7 +20,7 @@ import { emptyFormatter } from "../util"; import { useAdminClient } from "../context/auth/AdminClient"; import { useRealm } from "../context/realm-context/RealmContext"; -import "./RealmSettingsSection.css"; +import "./realm-settings-section.css"; import { FilterIcon } from "@patternfly/react-icons"; type KeyData = KeyMetadataRepresentation & { @@ -72,7 +72,7 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => { const activeKeysCopy = keys!.filter((i) => i.status === "ACTIVE"); - return activeKeysCopy?.map((key) => { + return activeKeysCopy.map((key) => { const provider = realmComponents.find( (component: ComponentRepresentation) => component.id === key.providerId ); @@ -88,7 +88,7 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => { const passiveKeys = keys!.filter((i) => i.status === "PASSIVE"); - return passiveKeys?.map((key) => { + return passiveKeys.map((key) => { const provider = realmComponents.find( (component: ComponentRepresentation) => component.id === key.providerId ); @@ -104,7 +104,7 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => { const disabledKeys = keys!.filter((i) => i.status === "DISABLED"); - return disabledKeys?.map((key) => { + return disabledKeys.map((key) => { const provider = realmComponents!.find( (component: ComponentRepresentation) => component.id === key.providerId ); diff --git a/src/realm-settings/KeysProvidersTab.tsx b/src/realm-settings/KeysProvidersTab.tsx index ae6cae4aff..d26fd5c574 100644 --- a/src/realm-settings/KeysProvidersTab.tsx +++ b/src/realm-settings/KeysProvidersTab.tsx @@ -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 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 { useAdminClient } from "../context/auth/AdminClient"; import { useAlerts } from "../components/alert/Alerts"; @@ -450,7 +450,7 @@ export const KeysProvidersTab = ({ }: KeysProps) => { return ( { + components={realmComponents.map((component) => { const provider = keyProviderComponentTypes.find( (componentType: ComponentTypeRepresentation) => component.providerId === componentType.id diff --git a/src/realm-settings/NewAttributeSettings.tsx b/src/realm-settings/NewAttributeSettings.tsx new file mode 100644 index 0000000000..a0b108d1e7 --- /dev/null +++ b/src/realm-settings/NewAttributeSettings.tsx @@ -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 ( + + console.log("TODO handle submit")} + isHorizontal + role="view-realm" + className="pf-u-mt-lg" + > +

{t("createAttribute")}

+
+
+ ); +} diff --git a/src/realm-settings/NewClientPolicyForm.tsx b/src/realm-settings/NewClientPolicyForm.tsx index 24b036c6a4..1fae9ee442 100644 --- a/src/realm-settings/NewClientPolicyForm.tsx +++ b/src/realm-settings/NewClientPolicyForm.tsx @@ -44,7 +44,7 @@ import { AddClientProfileModal } from "./AddClientProfileModal"; import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation"; import { toClientPolicies } from "./routes/ClientPolicies"; -import "./RealmSettingsSection.css"; +import "./realm-settings-section.css"; type NewClientPolicyForm = Required; diff --git a/src/realm-settings/PoliciesTab.tsx b/src/realm-settings/PoliciesTab.tsx index e36801b943..92cdac8861 100644 --- a/src/realm-settings/PoliciesTab.tsx +++ b/src/realm-settings/PoliciesTab.tsx @@ -24,7 +24,7 @@ import type ClientPolicyRepresentation from "@keycloak/keycloak-admin-client/lib import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useAlerts } from "../components/alert/Alerts"; -import "./RealmSettingsSection.css"; +import "./realm-settings-section.css"; import { useRealm } from "../context/realm-context/RealmContext"; import { toAddClientPolicy } from "./routes/AddClientPolicy"; import { toEditClientPolicy } from "./routes/EditClientPolicy"; diff --git a/src/realm-settings/ProfilesTab.tsx b/src/realm-settings/ProfilesTab.tsx index 5aecb1498a..3a41d5feeb 100644 --- a/src/realm-settings/ProfilesTab.tsx +++ b/src/realm-settings/ProfilesTab.tsx @@ -26,7 +26,7 @@ import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/li import { toClientProfile } from "./routes/ClientProfile"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; -import "./RealmSettingsSection.css"; +import "./realm-settings-section.css"; type ClientProfile = ClientProfileRepresentation & { global: boolean; diff --git a/src/realm-settings/SessionsTab.tsx b/src/realm-settings/SessionsTab.tsx index d5a6c40fc0..d5be25dc12 100644 --- a/src/realm-settings/SessionsTab.tsx +++ b/src/realm-settings/SessionsTab.tsx @@ -15,7 +15,7 @@ import { HelpItem } from "../components/help-enabler/HelpItem"; import { FormPanel } from "../components/scroll-form/FormPanel"; import { TimeSelector } from "../components/time-selector/TimeSelector"; -import "./RealmSettingsSection.css"; +import "./realm-settings-section.css"; type RealmSettingsSessionsTabProps = { realm: RealmRepresentation; diff --git a/src/realm-settings/TokensTab.tsx b/src/realm-settings/TokensTab.tsx index ac43440e60..bae4f4c407 100644 --- a/src/realm-settings/TokensTab.tsx +++ b/src/realm-settings/TokensTab.tsx @@ -23,7 +23,7 @@ import { TimeSelector } from "../components/time-selector/TimeSelector"; import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { forHumans, interpolateTimespan } from "../util"; -import "./RealmSettingsSection.css"; +import "./realm-settings-section.css"; type RealmSettingsSessionsTabProps = { realm: RealmRepresentation; diff --git a/src/realm-settings/messages.ts b/src/realm-settings/messages.ts index 87f9ee6a43..70770c3bb8 100644 --- a/src/realm-settings/messages.ts +++ b/src/realm-settings/messages.ts @@ -367,6 +367,18 @@ export default { updateMessageBundleSuccess: "Success! Message bundle updated.", updateMessageBundleError: "Error updating message bundle.", 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", searchEventType: "Search saved event type", addSavedTypes: "Add saved types", diff --git a/src/realm-settings/RealmSettingsSection.css b/src/realm-settings/realm-settings-section.css similarity index 100% rename from src/realm-settings/RealmSettingsSection.css rename to src/realm-settings/realm-settings-section.css diff --git a/src/realm-settings/routes.ts b/src/realm-settings/routes.ts index b152970fcd..6a22885664 100644 --- a/src/realm-settings/routes.ts +++ b/src/realm-settings/routes.ts @@ -16,6 +16,7 @@ import { EditClientPolicyRoute } from "./routes/EditClientPolicy"; import { NewClientPolicyConditionRoute } from "./routes/AddCondition"; import { EditClientPolicyConditionRoute } from "./routes/EditCondition"; import { UserProfileRoute } from "./routes/UserProfile"; +import { AddAttributeRoute } from "./routes/AddAttribute"; import { KeysRoute } from "./routes/KeysTab"; const routes: RouteDef[] = [ @@ -37,6 +38,7 @@ const routes: RouteDef[] = [ NewClientPolicyConditionRoute, EditClientPolicyConditionRoute, UserProfileRoute, + AddAttributeRoute, ]; export default routes; diff --git a/src/realm-settings/routes/AddAttribute.ts b/src/realm-settings/routes/AddAttribute.ts new file mode 100644 index 0000000000..2a79f6d73e --- /dev/null +++ b/src/realm-settings/routes/AddAttribute.ts @@ -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), +}); diff --git a/src/realm-settings/user-profile/AttributesTab.tsx b/src/realm-settings/user-profile/AttributesTab.tsx new file mode 100644 index 0000000000..c1b133fe51 --- /dev/null +++ b/src/realm-settings/user-profile/AttributesTab.tsx @@ -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 ; + } + + return ( + <> +
+ + + +
+ + + { + 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) => ( + + setKebabOpen({ + status, + rowKey: row.name!, + }) + } + id={`toggle-${row.name}`} + /> + } + isOpen={kebabOpen.status && kebabOpen.rowKey === row.name} + isPlain + dropdownItems={[ + { + setKebabOpen({ + status: false, + rowKey: row.name!, + }); + }} + > + {t("common:edit")} + , + + {row.name !== "email" && row.name !== "username" + ? [ + { + toggleDeleteDialog(); + setAttributeToDelete({ + name: row.name!, + }); + setKebabOpen({ + status: false, + rowKey: row.name!, + }); + }} + > + {t("common:delete")} + , + ] + : []} + , + ]} + /> + ), + }, + ]} + data={config.attributes!} + /> + + ); +}; diff --git a/src/realm-settings/user-profile/UserProfileTab.tsx b/src/realm-settings/user-profile/UserProfileTab.tsx index 0ca5a4ebf5..88e388f1c9 100644 --- a/src/realm-settings/user-profile/UserProfileTab.tsx +++ b/src/realm-settings/user-profile/UserProfileTab.tsx @@ -9,6 +9,7 @@ import { import { useRealm } from "../../context/realm-context/RealmContext"; import { toUserProfile } from "../routes/UserProfile"; import { AttributesGroupTab } from "./AttributesGroupTab"; +import { AttributesTab } from "./AttributesTab"; import { JsonEditorTab } from "./JsonEditorTab"; import { UserProfileProvider } from "./UserProfileContext"; @@ -25,11 +26,14 @@ export const UserProfileTab = () => { > {t("attributes")}} + data-testid="attributesTab" {...routableTab({ to: toUserProfile({ realm, tab: "attributes" }), history, })} - > + > + + {t("attributesGroup")}} data-testid="attributesGroupTab" @@ -42,6 +46,7 @@ export const UserProfileTab = () => { {t("jsonEditor")}} + data-testid="jsonEditorTab" {...routableTab({ to: toUserProfile({ realm, tab: "jsonEditor" }), history,