Add RoutableTabs component for routed tabs (#1852)
This commit is contained in:
parent
811131518e
commit
919d07c90b
6 changed files with 186 additions and 69 deletions
|
@ -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");
|
||||
|
|
62
package-lock.json
generated
62
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
70
src/components/routable-tabs/RoutableTabs.tsx
Normal file
70
src/components/routable-tabs/RoutableTabs.tsx
Normal file
|
@ -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<TabProps, JSXElementConstructor<TabProps>>;
|
||||
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 (
|
||||
<Tabs
|
||||
activeKey={exactMatch ?? nearestMatch ?? pathname}
|
||||
component={TabsComponent.nav}
|
||||
inset={{
|
||||
default: "insetNone",
|
||||
md: "insetSm",
|
||||
xl: "inset2xl",
|
||||
"2xl": "insetLg",
|
||||
}}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
type RoutableTabParams = {
|
||||
to: LocationDescriptorObject;
|
||||
history: History<unknown>;
|
||||
};
|
||||
|
||||
export const routableTab = ({ to, history }: RoutableTabParams) => ({
|
||||
eventKey: to.pathname ?? "",
|
||||
href: history.createHref(to),
|
||||
});
|
|
@ -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 = ({
|
|||
/>
|
||||
<PageSection variant="light" className="pf-u-p-0">
|
||||
<FormProvider {...form}>
|
||||
<KeycloakTabs isBox mountOnEnter>
|
||||
<RoutableTabs isBox mountOnEnter>
|
||||
<Tab
|
||||
eventKey="general"
|
||||
title={<TabTitleText>{t("general")}</TabTitleText>}
|
||||
data-testid="rs-general-tab"
|
||||
aria-label="general-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({ realm: realmName }),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<RealmSettingsGeneralTab
|
||||
save={save}
|
||||
|
@ -260,10 +265,12 @@ export const RealmSettingsTabs = ({
|
|||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="login"
|
||||
title={<TabTitleText>{t("login")}</TabTitleText>}
|
||||
data-testid="rs-login-tab"
|
||||
aria-label="login-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({ realm: realmName, tab: "login" }),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<RealmSettingsLoginTab
|
||||
refresh={refresh}
|
||||
|
@ -272,18 +279,22 @@ export const RealmSettingsTabs = ({
|
|||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="email"
|
||||
title={<TabTitleText>{t("email")}</TabTitleText>}
|
||||
data-testid="rs-email-tab"
|
||||
aria-label="email-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({ realm: realmName, tab: "email" }),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<RealmSettingsEmailTab realm={realm} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="themes"
|
||||
title={<TabTitleText>{t("themes")}</TabTitleText>}
|
||||
data-testid="rs-themes-tab"
|
||||
aria-label="themes-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({ realm: realmName, tab: "themes" }),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<RealmSettingsThemesTab
|
||||
save={save}
|
||||
|
@ -291,10 +302,12 @@ export const RealmSettingsTabs = ({
|
|||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="keys"
|
||||
title={<TabTitleText>{t("realm-settings:keys")}</TabTitleText>}
|
||||
data-testid="rs-keys-tab"
|
||||
aria-label="keys-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({ realm: realmName, tab: "keys" }),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
|
@ -325,18 +338,22 @@ export const RealmSettingsTabs = ({
|
|||
</Tabs>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="events"
|
||||
title={<TabTitleText>{t("events")}</TabTitleText>}
|
||||
data-testid="rs-realm-events-tab"
|
||||
aria-label="realm-events-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({ realm: realmName, tab: "events" }),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<EventsTab />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="localization"
|
||||
eventKey="localization"
|
||||
data-testid="rs-localization-tab"
|
||||
title={<TabTitleText>{t("localization")}</TabTitleText>}
|
||||
data-testid="rs-localization-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({ realm: realmName, tab: "localization" }),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<LocalizationTab
|
||||
key={key}
|
||||
|
@ -347,29 +364,43 @@ export const RealmSettingsTabs = ({
|
|||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="securityDefences"
|
||||
eventKey="securityDefences"
|
||||
title={<TabTitleText>{t("securityDefences")}</TabTitleText>}
|
||||
data-testid="rs-security-defenses-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({
|
||||
realm: realmName,
|
||||
tab: "securityDefences",
|
||||
}),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<SecurityDefences save={save} reset={() => resetForm(realm)} />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="sessions"
|
||||
eventKey="sessions"
|
||||
data-testid="rs-sessions-tab"
|
||||
aria-label="sessions-tab"
|
||||
title={
|
||||
<TabTitleText>{t("realm-settings:sessions")}</TabTitleText>
|
||||
}
|
||||
data-testid="rs-sessions-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({
|
||||
realm: realmName,
|
||||
tab: "sessions",
|
||||
}),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<RealmSettingsSessionsTab key={key} realm={realm} save={save} />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="tokens"
|
||||
eventKey="tokens"
|
||||
data-testid="rs-tokens-tab"
|
||||
aria-label="tokens-tab"
|
||||
title={<TabTitleText>{t("realm-settings:tokens")}</TabTitleText>}
|
||||
data-testid="rs-tokens-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({
|
||||
realm: realmName,
|
||||
tab: "tokens",
|
||||
}),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<RealmSettingsTokensTab
|
||||
save={save}
|
||||
|
@ -378,14 +409,19 @@ export const RealmSettingsTabs = ({
|
|||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="clientPolicies"
|
||||
title={
|
||||
<TabTitleText>
|
||||
{t("realm-settings:clientPolicies")}
|
||||
</TabTitleText>
|
||||
}
|
||||
data-testid="rs-clientPolicies-tab"
|
||||
aria-label={t("clientPoliciesTab")}
|
||||
{...routableTab({
|
||||
to: toRealmSettings({
|
||||
realm: realmName,
|
||||
tab: "clientPolicies",
|
||||
}),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
|
@ -434,26 +470,37 @@ export const RealmSettingsTabs = ({
|
|||
{isFeatureEnabled(Feature.DeclarativeUserProfile) &&
|
||||
userProfileEnabled === "true" && (
|
||||
<Tab
|
||||
eventKey="userProfile"
|
||||
data-testid="rs-user-profile-tab"
|
||||
title={
|
||||
<TabTitleText>
|
||||
{t("realm-settings:userProfile")}
|
||||
</TabTitleText>
|
||||
}
|
||||
data-testid="rs-user-profile-tab"
|
||||
{...routableTab({
|
||||
to: toRealmSettings({
|
||||
realm: realmName,
|
||||
tab: "userProfile",
|
||||
}),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<UserProfileTab />
|
||||
</Tab>
|
||||
)}
|
||||
<Tab
|
||||
eventKey="userRegistration"
|
||||
title={<TabTitleText>{t("userRegistration")}</TabTitleText>}
|
||||
data-testid="rs-userRegistration-tab"
|
||||
aria-label={t("userRegistrationTab")}
|
||||
{...routableTab({
|
||||
to: toRealmSettings({
|
||||
realm: realmName,
|
||||
tab: "userRegistration",
|
||||
}),
|
||||
history,
|
||||
})}
|
||||
>
|
||||
<UserRegistration />
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
</RoutableTabs>
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
</>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue