From 919d07c90b4366a1acd62bd413c383db365cb3a6 Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Fri, 14 Jan 2022 11:05:50 +0100 Subject: [PATCH] Add RoutableTabs component for routed tabs (#1852) --- .../realm_settings_events_test.spec.ts | 4 +- package-lock.json | 62 +++++----- package.json | 2 +- src/components/routable-tabs/RoutableTabs.tsx | 70 +++++++++++ src/realm-settings/RealmSettingsTabs.tsx | 115 ++++++++++++------ src/realm-settings/routes/RealmSettings.ts | 2 +- 6 files changed, 186 insertions(+), 69 deletions(-) create mode 100644 src/components/routable-tabs/RoutableTabs.tsx diff --git a/cypress/integration/realm_settings_events_test.spec.ts b/cypress/integration/realm_settings_events_test.spec.ts index 344cedfe72..461c385a23 100644 --- a/cypress/integration/realm_settings_events_test.spec.ts +++ b/cypress/integration/realm_settings_events_test.spec.ts @@ -197,7 +197,7 @@ describe("Realm settings events tab tests", () => { it("Realm header settings", () => { sidebarPage.goToRealmSettings(); - cy.get("#pf-tab-securityDefences-securityDefences").click(); + cy.findByTestId("rs-security-defenses-tab").click(); cy.findByTestId("headers-form-tab-save").should("be.disabled"); cy.get("#xFrameOptions").clear().type("DENY"); cy.findByTestId("headers-form-tab-save").should("be.enabled").click(); @@ -207,7 +207,7 @@ describe("Realm settings events tab tests", () => { it("Brute force detection", () => { sidebarPage.goToRealmSettings(); - cy.get("#pf-tab-securityDefences-securityDefences").click(); + cy.findAllByTestId("rs-security-defenses-tab").click(); cy.get("#pf-tab-20-bruteForce").click(); cy.findByTestId("brute-force-tab-save").should("be.disabled"); diff --git a/package-lock.json b/package-lock.json index 6fd031f7ad..d07bb00104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@keycloak/keycloak-admin-client": "^17.0.0-dev.11", "@patternfly/patternfly": "^4.164.2", "@patternfly/react-code-editor": "^4.22.1", - "@patternfly/react-core": "^4.181.1", + "@patternfly/react-core": "^4.183.10", "@patternfly/react-icons": "^4.32.1", "@patternfly/react-table": "^4.50.1", "dagre": "^0.8.5", @@ -3714,13 +3714,13 @@ } }, "node_modules/@patternfly/react-core": { - "version": "4.181.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.181.1.tgz", - "integrity": "sha512-5pt4R8Cg8CkA6xCY4v6wurpwn8WIS8N8NqQEtp56WABMVpZC+TE7k1TSsrDDR1WhJlxsl48VbsANkwMYLlsSJg==", + "version": "4.183.10", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.183.10.tgz", + "integrity": "sha512-d87q/WCvqWYMffq+zqRuNWXUWx6nQRdrJucJBhmjnYGnyQm9Agr0EgqBFyjQA8cnk+uTubCC+wYsmjDdDMUm1A==", "dependencies": { - "@patternfly/react-icons": "^4.32.1", - "@patternfly/react-styles": "^4.31.1", - "@patternfly/react-tokens": "^4.33.1", + "@patternfly/react-icons": "^4.34.10", + "@patternfly/react-styles": "^4.33.10", + "@patternfly/react-tokens": "^4.35.10", "focus-trap": "6.2.2", "react-dropzone": "9.0.0", "tippy.js": "5.1.2", @@ -3732,18 +3732,18 @@ } }, "node_modules/@patternfly/react-icons": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.32.1.tgz", - "integrity": "sha512-E7Fpnvax37e2ow8Xc2GngQBM7IuOpRyphZXCIUAk/NGsqpvFX27YhIVZiOUIAuBXAI5VXQBwueW/tmhmlXP7+w==", + "version": "4.34.10", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.34.10.tgz", + "integrity": "sha512-oEDFfyzBWeMO9EQYC2P5MrvR6mxq/kaQKHoC8xFbY449HnC3aloT+dk5kvMqpjcjV2teCSV/qE/4K+m9XZVm9w==", "peerDependencies": { "react": "^16.8.0 || ^17.0.0", "react-dom": "^16.8.0 || ^17.0.0" } }, "node_modules/@patternfly/react-styles": { - "version": "4.31.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.31.1.tgz", - "integrity": "sha512-Yw2hgg3T2qEqPYej5xprYD0iTTkzFMNjaF8u/YFyZOBNwfhjK8QQDZx8Du7Z6em8zGtajXFG5rZqxDiiz8bDfQ==" + "version": "4.33.10", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.33.10.tgz", + "integrity": "sha512-pMvpJmQyLRrqlspxp8CPgbIWquEdH/zIBHOQj5XWc8Zk83tZxYi+9tLyTO+RxG5kvtUVxsP+3vncrGxqmmzlQA==" }, "node_modules/@patternfly/react-table": { "version": "4.50.1", @@ -3763,9 +3763,9 @@ } }, "node_modules/@patternfly/react-tokens": { - "version": "4.33.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.33.1.tgz", - "integrity": "sha512-n06+BYDviOEuoo+qE9f0Jm1DkMDpwtXWyaJFHsvi8w8uddQEsBGGoP7lpkwZj15u6jc+F9IDgk/j+EkTScrvUA==" + "version": "4.35.10", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.35.10.tgz", + "integrity": "sha512-yb2m8tsLeWr5oU7UGh2NUcrJ+V6vZrGroerYMwFvcCzh1GWmxOU1ElYlsSdt+TPPLv+ZscbpSlolQmrQtAaueg==" }, "node_modules/@rollup/plugin-commonjs": { "version": "16.0.0", @@ -24191,13 +24191,13 @@ } }, "@patternfly/react-core": { - "version": "4.181.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.181.1.tgz", - "integrity": "sha512-5pt4R8Cg8CkA6xCY4v6wurpwn8WIS8N8NqQEtp56WABMVpZC+TE7k1TSsrDDR1WhJlxsl48VbsANkwMYLlsSJg==", + "version": "4.183.10", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.183.10.tgz", + "integrity": "sha512-d87q/WCvqWYMffq+zqRuNWXUWx6nQRdrJucJBhmjnYGnyQm9Agr0EgqBFyjQA8cnk+uTubCC+wYsmjDdDMUm1A==", "requires": { - "@patternfly/react-icons": "^4.32.1", - "@patternfly/react-styles": "^4.31.1", - "@patternfly/react-tokens": "^4.33.1", + "@patternfly/react-icons": "^4.34.10", + "@patternfly/react-styles": "^4.33.10", + "@patternfly/react-tokens": "^4.35.10", "focus-trap": "6.2.2", "react-dropzone": "9.0.0", "tippy.js": "5.1.2", @@ -24205,15 +24205,15 @@ } }, "@patternfly/react-icons": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.32.1.tgz", - "integrity": "sha512-E7Fpnvax37e2ow8Xc2GngQBM7IuOpRyphZXCIUAk/NGsqpvFX27YhIVZiOUIAuBXAI5VXQBwueW/tmhmlXP7+w==", + "version": "4.34.10", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.34.10.tgz", + "integrity": "sha512-oEDFfyzBWeMO9EQYC2P5MrvR6mxq/kaQKHoC8xFbY449HnC3aloT+dk5kvMqpjcjV2teCSV/qE/4K+m9XZVm9w==", "requires": {} }, "@patternfly/react-styles": { - "version": "4.31.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.31.1.tgz", - "integrity": "sha512-Yw2hgg3T2qEqPYej5xprYD0iTTkzFMNjaF8u/YFyZOBNwfhjK8QQDZx8Du7Z6em8zGtajXFG5rZqxDiiz8bDfQ==" + "version": "4.33.10", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.33.10.tgz", + "integrity": "sha512-pMvpJmQyLRrqlspxp8CPgbIWquEdH/zIBHOQj5XWc8Zk83tZxYi+9tLyTO+RxG5kvtUVxsP+3vncrGxqmmzlQA==" }, "@patternfly/react-table": { "version": "4.50.1", @@ -24229,9 +24229,9 @@ } }, "@patternfly/react-tokens": { - "version": "4.33.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.33.1.tgz", - "integrity": "sha512-n06+BYDviOEuoo+qE9f0Jm1DkMDpwtXWyaJFHsvi8w8uddQEsBGGoP7lpkwZj15u6jc+F9IDgk/j+EkTScrvUA==" + "version": "4.35.10", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.35.10.tgz", + "integrity": "sha512-yb2m8tsLeWr5oU7UGh2NUcrJ+V6vZrGroerYMwFvcCzh1GWmxOU1ElYlsSdt+TPPLv+ZscbpSlolQmrQtAaueg==" }, "@rollup/plugin-commonjs": { "version": "16.0.0", diff --git a/package.json b/package.json index 4285eb129b..369c6003a7 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@keycloak/keycloak-admin-client": "^17.0.0-dev.11", "@patternfly/patternfly": "^4.164.2", "@patternfly/react-code-editor": "^4.22.1", - "@patternfly/react-core": "^4.181.1", + "@patternfly/react-core": "^4.183.10", "@patternfly/react-icons": "^4.32.1", "@patternfly/react-table": "^4.50.1", "dagre": "^0.8.5", diff --git a/src/components/routable-tabs/RoutableTabs.tsx b/src/components/routable-tabs/RoutableTabs.tsx new file mode 100644 index 0000000000..c7bcc6ad42 --- /dev/null +++ b/src/components/routable-tabs/RoutableTabs.tsx @@ -0,0 +1,70 @@ +import { + TabProps, + Tabs, + TabsComponent, + TabsProps, +} from "@patternfly/react-core"; +import type { History, LocationDescriptorObject } from "history"; +import React, { + Children, + isValidElement, + JSXElementConstructor, + ReactElement, +} from "react"; +import { useLocation } from "react-router-dom"; + +type ChildElement = ReactElement>; +type Child = ChildElement | boolean | null | undefined; + +// TODO: Figure out why we need to omit 'ref' from the props. +type RoutableTabsProps = { children: Child | Child[] } & Omit< + TabsProps, + "ref" | "activeKey" | "component" | "children" +>; + +export const RoutableTabs = ({ + children, + ...otherProps +}: RoutableTabsProps) => { + const { pathname } = useLocation(); + + // Extract event keys from children. + const eventKeys = Children.toArray(children) + .filter((child): child is ChildElement => isValidElement(child)) + .map((child) => child.props.eventKey.toString()); + + // Determine if there is an exact match. + const exactMatch = eventKeys.find((eventKey) => eventKey === pathname); + + // Determine which event keys at least partially match the current path, then sort them so the nearest match ends up on top. + const nearestMatch = eventKeys + .filter((eventKey) => pathname.includes(eventKey)) + .sort((a, b) => a.length - b.length) + .pop(); + + return ( + + {children} + + ); +}; + +type RoutableTabParams = { + to: LocationDescriptorObject; + history: History; +}; + +export const routableTab = ({ to, history }: RoutableTabParams) => ({ + eventKey: to.pathname ?? "", + href: history.createHref(to), +}); diff --git a/src/realm-settings/RealmSettingsTabs.tsx b/src/realm-settings/RealmSettingsTabs.tsx index 2310c0c4ad..679f959b4b 100644 --- a/src/realm-settings/RealmSettingsTabs.tsx +++ b/src/realm-settings/RealmSettingsTabs.tsx @@ -17,7 +17,10 @@ import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/r import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; -import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; +import { + routableTab, + RoutableTabs, +} from "../components/routable-tabs/RoutableTabs"; import { useRealm } from "../context/realm-context/RealmContext"; import { useRealms } from "../context/RealmsContext"; import { ViewHeader } from "../components/view-header/ViewHeader"; @@ -247,12 +250,14 @@ export const RealmSettingsTabs = ({ /> - + {t("general")}} data-testid="rs-general-tab" - aria-label="general-tab" + {...routableTab({ + to: toRealmSettings({ realm: realmName }), + history, + })} > {t("login")}} data-testid="rs-login-tab" - aria-label="login-tab" + {...routableTab({ + to: toRealmSettings({ realm: realmName, tab: "login" }), + history, + })} > {t("email")}} data-testid="rs-email-tab" - aria-label="email-tab" + {...routableTab({ + to: toRealmSettings({ realm: realmName, tab: "email" }), + history, + })} > {t("themes")}} data-testid="rs-themes-tab" - aria-label="themes-tab" + {...routableTab({ + to: toRealmSettings({ realm: realmName, tab: "themes" }), + history, + })} > {t("realm-settings:keys")}} data-testid="rs-keys-tab" - aria-label="keys-tab" + {...routableTab({ + to: toRealmSettings({ realm: realmName, tab: "keys" }), + history, + })} > {t("events")}} data-testid="rs-realm-events-tab" - aria-label="realm-events-tab" + {...routableTab({ + to: toRealmSettings({ realm: realmName, tab: "events" }), + history, + })} > {t("localization")}} + data-testid="rs-localization-tab" + {...routableTab({ + to: toRealmSettings({ realm: realmName, tab: "localization" }), + history, + })} > {t("securityDefences")}} + data-testid="rs-security-defenses-tab" + {...routableTab({ + to: toRealmSettings({ + realm: realmName, + tab: "securityDefences", + }), + history, + })} > resetForm(realm)} /> {t("realm-settings:sessions")} } + data-testid="rs-sessions-tab" + {...routableTab({ + to: toRealmSettings({ + realm: realmName, + tab: "sessions", + }), + history, + })} > {t("realm-settings:tokens")}} + data-testid="rs-tokens-tab" + {...routableTab({ + to: toRealmSettings({ + realm: realmName, + tab: "tokens", + }), + history, + })} > {t("realm-settings:clientPolicies")} } data-testid="rs-clientPolicies-tab" - aria-label={t("clientPoliciesTab")} + {...routableTab({ + to: toRealmSettings({ + realm: realmName, + tab: "clientPolicies", + }), + history, + })} > {t("realm-settings:userProfile")} } + data-testid="rs-user-profile-tab" + {...routableTab({ + to: toRealmSettings({ + realm: realmName, + tab: "userProfile", + }), + history, + })} > )} {t("userRegistration")}} data-testid="rs-userRegistration-tab" - aria-label={t("userRegistrationTab")} + {...routableTab({ + to: toRealmSettings({ + realm: realmName, + tab: "userRegistration", + }), + history, + })} > - + diff --git a/src/realm-settings/routes/RealmSettings.ts b/src/realm-settings/routes/RealmSettings.ts index 4728b8ae6b..3ea3ca1e82 100644 --- a/src/realm-settings/routes/RealmSettings.ts +++ b/src/realm-settings/routes/RealmSettings.ts @@ -4,12 +4,12 @@ import { generatePath } from "react-router-dom"; import type { RouteDef } from "../../route-config"; export type RealmSettingsTab = - | "general" | "login" | "email" | "themes" | "keys" | "events" + | "localization" | "securityDefences" | "sessions" | "tokens"