Add RoutableTabs component for routed tabs (#1852)

This commit is contained in:
Jon Koops 2022-01-14 11:05:50 +01:00 committed by GitHub
parent 811131518e
commit 919d07c90b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 186 additions and 69 deletions

View file

@ -197,7 +197,7 @@ describe("Realm settings events tab tests", () => {
it("Realm header settings", () => { it("Realm header settings", () => {
sidebarPage.goToRealmSettings(); 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.findByTestId("headers-form-tab-save").should("be.disabled");
cy.get("#xFrameOptions").clear().type("DENY"); cy.get("#xFrameOptions").clear().type("DENY");
cy.findByTestId("headers-form-tab-save").should("be.enabled").click(); cy.findByTestId("headers-form-tab-save").should("be.enabled").click();
@ -207,7 +207,7 @@ describe("Realm settings events tab tests", () => {
it("Brute force detection", () => { it("Brute force detection", () => {
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
cy.get("#pf-tab-securityDefences-securityDefences").click(); cy.findAllByTestId("rs-security-defenses-tab").click();
cy.get("#pf-tab-20-bruteForce").click(); cy.get("#pf-tab-20-bruteForce").click();
cy.findByTestId("brute-force-tab-save").should("be.disabled"); cy.findByTestId("brute-force-tab-save").should("be.disabled");

62
package-lock.json generated
View file

@ -10,7 +10,7 @@
"@keycloak/keycloak-admin-client": "^17.0.0-dev.11", "@keycloak/keycloak-admin-client": "^17.0.0-dev.11",
"@patternfly/patternfly": "^4.164.2", "@patternfly/patternfly": "^4.164.2",
"@patternfly/react-code-editor": "^4.22.1", "@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-icons": "^4.32.1",
"@patternfly/react-table": "^4.50.1", "@patternfly/react-table": "^4.50.1",
"dagre": "^0.8.5", "dagre": "^0.8.5",
@ -3714,13 +3714,13 @@
} }
}, },
"node_modules/@patternfly/react-core": { "node_modules/@patternfly/react-core": {
"version": "4.181.1", "version": "4.183.10",
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.181.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.183.10.tgz",
"integrity": "sha512-5pt4R8Cg8CkA6xCY4v6wurpwn8WIS8N8NqQEtp56WABMVpZC+TE7k1TSsrDDR1WhJlxsl48VbsANkwMYLlsSJg==", "integrity": "sha512-d87q/WCvqWYMffq+zqRuNWXUWx6nQRdrJucJBhmjnYGnyQm9Agr0EgqBFyjQA8cnk+uTubCC+wYsmjDdDMUm1A==",
"dependencies": { "dependencies": {
"@patternfly/react-icons": "^4.32.1", "@patternfly/react-icons": "^4.34.10",
"@patternfly/react-styles": "^4.31.1", "@patternfly/react-styles": "^4.33.10",
"@patternfly/react-tokens": "^4.33.1", "@patternfly/react-tokens": "^4.35.10",
"focus-trap": "6.2.2", "focus-trap": "6.2.2",
"react-dropzone": "9.0.0", "react-dropzone": "9.0.0",
"tippy.js": "5.1.2", "tippy.js": "5.1.2",
@ -3732,18 +3732,18 @@
} }
}, },
"node_modules/@patternfly/react-icons": { "node_modules/@patternfly/react-icons": {
"version": "4.32.1", "version": "4.34.10",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.34.10.tgz",
"integrity": "sha512-E7Fpnvax37e2ow8Xc2GngQBM7IuOpRyphZXCIUAk/NGsqpvFX27YhIVZiOUIAuBXAI5VXQBwueW/tmhmlXP7+w==", "integrity": "sha512-oEDFfyzBWeMO9EQYC2P5MrvR6mxq/kaQKHoC8xFbY449HnC3aloT+dk5kvMqpjcjV2teCSV/qE/4K+m9XZVm9w==",
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0", "react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0" "react-dom": "^16.8.0 || ^17.0.0"
} }
}, },
"node_modules/@patternfly/react-styles": { "node_modules/@patternfly/react-styles": {
"version": "4.31.1", "version": "4.33.10",
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.31.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.33.10.tgz",
"integrity": "sha512-Yw2hgg3T2qEqPYej5xprYD0iTTkzFMNjaF8u/YFyZOBNwfhjK8QQDZx8Du7Z6em8zGtajXFG5rZqxDiiz8bDfQ==" "integrity": "sha512-pMvpJmQyLRrqlspxp8CPgbIWquEdH/zIBHOQj5XWc8Zk83tZxYi+9tLyTO+RxG5kvtUVxsP+3vncrGxqmmzlQA=="
}, },
"node_modules/@patternfly/react-table": { "node_modules/@patternfly/react-table": {
"version": "4.50.1", "version": "4.50.1",
@ -3763,9 +3763,9 @@
} }
}, },
"node_modules/@patternfly/react-tokens": { "node_modules/@patternfly/react-tokens": {
"version": "4.33.1", "version": "4.35.10",
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.33.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.35.10.tgz",
"integrity": "sha512-n06+BYDviOEuoo+qE9f0Jm1DkMDpwtXWyaJFHsvi8w8uddQEsBGGoP7lpkwZj15u6jc+F9IDgk/j+EkTScrvUA==" "integrity": "sha512-yb2m8tsLeWr5oU7UGh2NUcrJ+V6vZrGroerYMwFvcCzh1GWmxOU1ElYlsSdt+TPPLv+ZscbpSlolQmrQtAaueg=="
}, },
"node_modules/@rollup/plugin-commonjs": { "node_modules/@rollup/plugin-commonjs": {
"version": "16.0.0", "version": "16.0.0",
@ -24191,13 +24191,13 @@
} }
}, },
"@patternfly/react-core": { "@patternfly/react-core": {
"version": "4.181.1", "version": "4.183.10",
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.181.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.183.10.tgz",
"integrity": "sha512-5pt4R8Cg8CkA6xCY4v6wurpwn8WIS8N8NqQEtp56WABMVpZC+TE7k1TSsrDDR1WhJlxsl48VbsANkwMYLlsSJg==", "integrity": "sha512-d87q/WCvqWYMffq+zqRuNWXUWx6nQRdrJucJBhmjnYGnyQm9Agr0EgqBFyjQA8cnk+uTubCC+wYsmjDdDMUm1A==",
"requires": { "requires": {
"@patternfly/react-icons": "^4.32.1", "@patternfly/react-icons": "^4.34.10",
"@patternfly/react-styles": "^4.31.1", "@patternfly/react-styles": "^4.33.10",
"@patternfly/react-tokens": "^4.33.1", "@patternfly/react-tokens": "^4.35.10",
"focus-trap": "6.2.2", "focus-trap": "6.2.2",
"react-dropzone": "9.0.0", "react-dropzone": "9.0.0",
"tippy.js": "5.1.2", "tippy.js": "5.1.2",
@ -24205,15 +24205,15 @@
} }
}, },
"@patternfly/react-icons": { "@patternfly/react-icons": {
"version": "4.32.1", "version": "4.34.10",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.34.10.tgz",
"integrity": "sha512-E7Fpnvax37e2ow8Xc2GngQBM7IuOpRyphZXCIUAk/NGsqpvFX27YhIVZiOUIAuBXAI5VXQBwueW/tmhmlXP7+w==", "integrity": "sha512-oEDFfyzBWeMO9EQYC2P5MrvR6mxq/kaQKHoC8xFbY449HnC3aloT+dk5kvMqpjcjV2teCSV/qE/4K+m9XZVm9w==",
"requires": {} "requires": {}
}, },
"@patternfly/react-styles": { "@patternfly/react-styles": {
"version": "4.31.1", "version": "4.33.10",
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.31.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.33.10.tgz",
"integrity": "sha512-Yw2hgg3T2qEqPYej5xprYD0iTTkzFMNjaF8u/YFyZOBNwfhjK8QQDZx8Du7Z6em8zGtajXFG5rZqxDiiz8bDfQ==" "integrity": "sha512-pMvpJmQyLRrqlspxp8CPgbIWquEdH/zIBHOQj5XWc8Zk83tZxYi+9tLyTO+RxG5kvtUVxsP+3vncrGxqmmzlQA=="
}, },
"@patternfly/react-table": { "@patternfly/react-table": {
"version": "4.50.1", "version": "4.50.1",
@ -24229,9 +24229,9 @@
} }
}, },
"@patternfly/react-tokens": { "@patternfly/react-tokens": {
"version": "4.33.1", "version": "4.35.10",
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.33.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.35.10.tgz",
"integrity": "sha512-n06+BYDviOEuoo+qE9f0Jm1DkMDpwtXWyaJFHsvi8w8uddQEsBGGoP7lpkwZj15u6jc+F9IDgk/j+EkTScrvUA==" "integrity": "sha512-yb2m8tsLeWr5oU7UGh2NUcrJ+V6vZrGroerYMwFvcCzh1GWmxOU1ElYlsSdt+TPPLv+ZscbpSlolQmrQtAaueg=="
}, },
"@rollup/plugin-commonjs": { "@rollup/plugin-commonjs": {
"version": "16.0.0", "version": "16.0.0",

View file

@ -26,7 +26,7 @@
"@keycloak/keycloak-admin-client": "^17.0.0-dev.11", "@keycloak/keycloak-admin-client": "^17.0.0-dev.11",
"@patternfly/patternfly": "^4.164.2", "@patternfly/patternfly": "^4.164.2",
"@patternfly/react-code-editor": "^4.22.1", "@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-icons": "^4.32.1",
"@patternfly/react-table": "^4.50.1", "@patternfly/react-table": "^4.50.1",
"dagre": "^0.8.5", "dagre": "^0.8.5",

View 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),
});

View file

@ -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 type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; 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 { useRealm } from "../context/realm-context/RealmContext";
import { useRealms } from "../context/RealmsContext"; import { useRealms } from "../context/RealmsContext";
import { ViewHeader } from "../components/view-header/ViewHeader"; import { ViewHeader } from "../components/view-header/ViewHeader";
@ -247,12 +250,14 @@ export const RealmSettingsTabs = ({
/> />
<PageSection variant="light" className="pf-u-p-0"> <PageSection variant="light" className="pf-u-p-0">
<FormProvider {...form}> <FormProvider {...form}>
<KeycloakTabs isBox mountOnEnter> <RoutableTabs isBox mountOnEnter>
<Tab <Tab
eventKey="general"
title={<TabTitleText>{t("general")}</TabTitleText>} title={<TabTitleText>{t("general")}</TabTitleText>}
data-testid="rs-general-tab" data-testid="rs-general-tab"
aria-label="general-tab" {...routableTab({
to: toRealmSettings({ realm: realmName }),
history,
})}
> >
<RealmSettingsGeneralTab <RealmSettingsGeneralTab
save={save} save={save}
@ -260,10 +265,12 @@ export const RealmSettingsTabs = ({
/> />
</Tab> </Tab>
<Tab <Tab
eventKey="login"
title={<TabTitleText>{t("login")}</TabTitleText>} title={<TabTitleText>{t("login")}</TabTitleText>}
data-testid="rs-login-tab" data-testid="rs-login-tab"
aria-label="login-tab" {...routableTab({
to: toRealmSettings({ realm: realmName, tab: "login" }),
history,
})}
> >
<RealmSettingsLoginTab <RealmSettingsLoginTab
refresh={refresh} refresh={refresh}
@ -272,18 +279,22 @@ export const RealmSettingsTabs = ({
/> />
</Tab> </Tab>
<Tab <Tab
eventKey="email"
title={<TabTitleText>{t("email")}</TabTitleText>} title={<TabTitleText>{t("email")}</TabTitleText>}
data-testid="rs-email-tab" data-testid="rs-email-tab"
aria-label="email-tab" {...routableTab({
to: toRealmSettings({ realm: realmName, tab: "email" }),
history,
})}
> >
<RealmSettingsEmailTab realm={realm} /> <RealmSettingsEmailTab realm={realm} />
</Tab> </Tab>
<Tab <Tab
eventKey="themes"
title={<TabTitleText>{t("themes")}</TabTitleText>} title={<TabTitleText>{t("themes")}</TabTitleText>}
data-testid="rs-themes-tab" data-testid="rs-themes-tab"
aria-label="themes-tab" {...routableTab({
to: toRealmSettings({ realm: realmName, tab: "themes" }),
history,
})}
> >
<RealmSettingsThemesTab <RealmSettingsThemesTab
save={save} save={save}
@ -291,10 +302,12 @@ export const RealmSettingsTabs = ({
/> />
</Tab> </Tab>
<Tab <Tab
eventKey="keys"
title={<TabTitleText>{t("realm-settings:keys")}</TabTitleText>} title={<TabTitleText>{t("realm-settings:keys")}</TabTitleText>}
data-testid="rs-keys-tab" data-testid="rs-keys-tab"
aria-label="keys-tab" {...routableTab({
to: toRealmSettings({ realm: realmName, tab: "keys" }),
history,
})}
> >
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
@ -325,18 +338,22 @@ export const RealmSettingsTabs = ({
</Tabs> </Tabs>
</Tab> </Tab>
<Tab <Tab
eventKey="events"
title={<TabTitleText>{t("events")}</TabTitleText>} title={<TabTitleText>{t("events")}</TabTitleText>}
data-testid="rs-realm-events-tab" data-testid="rs-realm-events-tab"
aria-label="realm-events-tab" {...routableTab({
to: toRealmSettings({ realm: realmName, tab: "events" }),
history,
})}
> >
<EventsTab /> <EventsTab />
</Tab> </Tab>
<Tab <Tab
id="localization"
eventKey="localization"
data-testid="rs-localization-tab"
title={<TabTitleText>{t("localization")}</TabTitleText>} title={<TabTitleText>{t("localization")}</TabTitleText>}
data-testid="rs-localization-tab"
{...routableTab({
to: toRealmSettings({ realm: realmName, tab: "localization" }),
history,
})}
> >
<LocalizationTab <LocalizationTab
key={key} key={key}
@ -347,29 +364,43 @@ export const RealmSettingsTabs = ({
/> />
</Tab> </Tab>
<Tab <Tab
id="securityDefences"
eventKey="securityDefences"
title={<TabTitleText>{t("securityDefences")}</TabTitleText>} 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)} /> <SecurityDefences save={save} reset={() => resetForm(realm)} />
</Tab> </Tab>
<Tab <Tab
id="sessions"
eventKey="sessions"
data-testid="rs-sessions-tab"
aria-label="sessions-tab"
title={ title={
<TabTitleText>{t("realm-settings:sessions")}</TabTitleText> <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} /> <RealmSettingsSessionsTab key={key} realm={realm} save={save} />
</Tab> </Tab>
<Tab <Tab
id="tokens"
eventKey="tokens"
data-testid="rs-tokens-tab"
aria-label="tokens-tab"
title={<TabTitleText>{t("realm-settings:tokens")}</TabTitleText>} title={<TabTitleText>{t("realm-settings:tokens")}</TabTitleText>}
data-testid="rs-tokens-tab"
{...routableTab({
to: toRealmSettings({
realm: realmName,
tab: "tokens",
}),
history,
})}
> >
<RealmSettingsTokensTab <RealmSettingsTokensTab
save={save} save={save}
@ -378,14 +409,19 @@ export const RealmSettingsTabs = ({
/> />
</Tab> </Tab>
<Tab <Tab
eventKey="clientPolicies"
title={ title={
<TabTitleText> <TabTitleText>
{t("realm-settings:clientPolicies")} {t("realm-settings:clientPolicies")}
</TabTitleText> </TabTitleText>
} }
data-testid="rs-clientPolicies-tab" data-testid="rs-clientPolicies-tab"
aria-label={t("clientPoliciesTab")} {...routableTab({
to: toRealmSettings({
realm: realmName,
tab: "clientPolicies",
}),
history,
})}
> >
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
@ -434,26 +470,37 @@ export const RealmSettingsTabs = ({
{isFeatureEnabled(Feature.DeclarativeUserProfile) && {isFeatureEnabled(Feature.DeclarativeUserProfile) &&
userProfileEnabled === "true" && ( userProfileEnabled === "true" && (
<Tab <Tab
eventKey="userProfile"
data-testid="rs-user-profile-tab"
title={ title={
<TabTitleText> <TabTitleText>
{t("realm-settings:userProfile")} {t("realm-settings:userProfile")}
</TabTitleText> </TabTitleText>
} }
data-testid="rs-user-profile-tab"
{...routableTab({
to: toRealmSettings({
realm: realmName,
tab: "userProfile",
}),
history,
})}
> >
<UserProfileTab /> <UserProfileTab />
</Tab> </Tab>
)} )}
<Tab <Tab
eventKey="userRegistration"
title={<TabTitleText>{t("userRegistration")}</TabTitleText>} title={<TabTitleText>{t("userRegistration")}</TabTitleText>}
data-testid="rs-userRegistration-tab" data-testid="rs-userRegistration-tab"
aria-label={t("userRegistrationTab")} {...routableTab({
to: toRealmSettings({
realm: realmName,
tab: "userRegistration",
}),
history,
})}
> >
<UserRegistration /> <UserRegistration />
</Tab> </Tab>
</KeycloakTabs> </RoutableTabs>
</FormProvider> </FormProvider>
</PageSection> </PageSection>
</> </>

View file

@ -4,12 +4,12 @@ import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config"; import type { RouteDef } from "../../route-config";
export type RealmSettingsTab = export type RealmSettingsTab =
| "general"
| "login" | "login"
| "email" | "email"
| "themes" | "themes"
| "keys" | "keys"
| "events" | "events"
| "localization"
| "securityDefences" | "securityDefences"
| "sessions" | "sessions"
| "tokens" | "tokens"