diff --git a/cypress/integration/clients_saml_test.spec.ts b/cypress/integration/clients_saml_test.spec.ts index e1e945f224..4db45779ea 100644 --- a/cypress/integration/clients_saml_test.spec.ts +++ b/cypress/integration/clients_saml_test.spec.ts @@ -73,7 +73,7 @@ describe("Clients SAML tests", () => { loginPage.logIn(); sidebarPage.goToClients(); listingPage.searchItem(clientId).goToItemDetails(clientId); - cy.get("#pf-tab-keys-keys").click(); + cy.findByTestId("keysTab").click(); }); it("doesn't disable when no", () => { diff --git a/cypress/integration/clients_test.spec.ts b/cypress/integration/clients_test.spec.ts index d6705cdfc3..564ff9066c 100644 --- a/cypress/integration/clients_test.spec.ts +++ b/cypress/integration/clients_test.spec.ts @@ -279,7 +279,7 @@ describe("Clients test", () => { }); it("add mapping to openid client", () => { - cy.get("#pf-tab-mappers-mappers").click(); + cy.findByTestId("mappersTab").click(); cy.findByText("Add predefined mapper").click(); cy.get("table input").first().click(); cy.findByTestId("modalConfirm").click(); @@ -343,15 +343,13 @@ describe("Clients test", () => { it("displays the correct tabs", () => { cy.findByTestId("client-tabs") - .find("#pf-tab-settings-settings") + .findByTestId("clientSettingsTab") .should("exist"); - cy.findByTestId("client-tabs") - .find("#pf-tab-roles-roles") - .should("exist"); + cy.findByTestId("client-tabs").findByTestId("rolesTab").should("exist"); cy.findByTestId("client-tabs") - .find("#pf-tab-advanced-advanced") + .findByTestId("advancedTab") .should("exist"); cy.findByTestId("client-tabs").find("li").should("have.length", 3); diff --git a/cypress/support/pages/admin_console/manage/RoleMappingTab.ts b/cypress/support/pages/admin_console/manage/RoleMappingTab.ts index a4048b0dbb..53507d3f6e 100644 --- a/cypress/support/pages/admin_console/manage/RoleMappingTab.ts +++ b/cypress/support/pages/admin_console/manage/RoleMappingTab.ts @@ -1,6 +1,6 @@ const expect = chai.expect; export default class RoleMappingTab { - private tab = "#pf-tab-serviceAccount-serviceAccount"; + private tab = "serviceAccountTab"; private scopeTab = "scopeTab"; private assignEmptyRoleBtn = "no-roles-for-this-client-empty-action"; private assignRoleBtn = "assignRole"; @@ -12,7 +12,7 @@ export default class RoleMappingTab { private confirmModalBtn = "modalConfirm"; goToServiceAccountTab() { - cy.get(this.tab).click(); + cy.findByTestId(this.tab).click(); return this; } diff --git a/cypress/support/pages/admin_console/manage/clients/AdvancedTab.ts b/cypress/support/pages/admin_console/manage/clients/AdvancedTab.ts index 0e5da0551f..eb747f4934 100644 --- a/cypress/support/pages/admin_console/manage/clients/AdvancedTab.ts +++ b/cypress/support/pages/admin_console/manage/clients/AdvancedTab.ts @@ -17,10 +17,10 @@ export default class AdvancedTab { private fineGrainSaveBtn = "#fineGrainSave"; private fineGrainRevertBtn = "#fineGrainRevert"; - private advancedTab = "#pf-tab-advanced-advanced"; + private advancedTab = "advancedTab"; goToAdvancedTab() { - cy.get(this.advancedTab).click(); + cy.findByTestId(this.advancedTab).click(); return this; } diff --git a/cypress/support/pages/admin_console/manage/clients/AuthorizationTab.ts b/cypress/support/pages/admin_console/manage/clients/AuthorizationTab.ts index cee7fb2d3b..9f1eb84c01 100644 --- a/cypress/support/pages/admin_console/manage/clients/AuthorizationTab.ts +++ b/cypress/support/pages/admin_console/manage/clients/AuthorizationTab.ts @@ -5,11 +5,11 @@ import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/s type PermissionType = "resource" | "scope"; export default class AuthorizationTab { - private tabName = "#pf-tab-authorization-authorization"; - private resourcesTabName = "#pf-tab-41-resources"; - private scopeTabName = "#pf-tab-42-scopes"; - private policyTabName = "#pf-tab-43-policies"; - private permissionsTabName = "#pf-tab-44-permissions"; + private tabName = "authorizationTab"; + private resourcesTabName = "authorizationResources"; + private scopeTabName = "authorizationScopes"; + private policyTabName = "authorizationPolicies"; + private permissionsTabName = "authorizationPermissions"; private nameColumnPrefix = "name-column-"; private emptyPolicyCreateButton = "no-policies-empty-action"; private createPolicyButton = "createPolicy"; @@ -19,27 +19,27 @@ export default class AuthorizationTab { private permissionResourceDropdown = "#resources"; goToAuthenticationTab() { - cy.get(this.tabName).click(); + cy.findByTestId(this.tabName).click(); return this; } goToResourceSubTab() { - cy.get(this.resourcesTabName).click(); + cy.findByTestId(this.resourcesTabName).click(); return this; } goToScopeSubTab() { - cy.get(this.scopeTabName).click(); + cy.findByTestId(this.scopeTabName).click(); return this; } goToPolicySubTab() { - cy.get(this.policyTabName).click(); + cy.findByTestId(this.policyTabName).click(); return this; } goToPermissionsSubTab() { - cy.get(this.permissionsTabName).click(); + cy.findByTestId(this.permissionsTabName).click(); return this; } diff --git a/cypress/support/pages/admin_console/manage/clients/ClientScopesTab.ts b/cypress/support/pages/admin_console/manage/clients/ClientScopesTab.ts index 73b05a8a91..006f4d766e 100644 --- a/cypress/support/pages/admin_console/manage/clients/ClientScopesTab.ts +++ b/cypress/support/pages/admin_console/manage/clients/ClientScopesTab.ts @@ -1,8 +1,8 @@ export default class ClientScopesTab { - private clientScopesTab = "#pf-tab-clientScopes-clientScopes"; + private clientScopesTab = "clientScopesTab"; goToClientScopesTab() { - cy.get(this.clientScopesTab).click(); + cy.findByTestId(this.clientScopesTab).click(); return this; } } diff --git a/cypress/support/pages/admin_console/manage/clients/KeysTab.ts b/cypress/support/pages/admin_console/manage/clients/KeysTab.ts index e53e59a6d9..dcdb577939 100644 --- a/cypress/support/pages/admin_console/manage/clients/KeysTab.ts +++ b/cypress/support/pages/admin_console/manage/clients/KeysTab.ts @@ -1,5 +1,5 @@ export default class KeysTab { - private tabName = "#pf-tab-keys-keys"; + private tabName = "keysTab"; private useJwksUrl = "useJwksUrl"; private saveKeys = "saveKeys"; private generate = "generate"; @@ -9,7 +9,7 @@ export default class KeysTab { private confirm = "confirm"; goToTab() { - cy.get(this.tabName).click(); + cy.findByTestId(this.tabName).click(); return this; } diff --git a/src/clients/ClientDetails.tsx b/src/clients/ClientDetails.tsx index f5ec622f22..87409cd6f8 100644 --- a/src/clients/ClientDetails.tsx +++ b/src/clients/ClientDetails.tsx @@ -7,7 +7,6 @@ import { Label, PageSection, Tab, - Tabs, TabTitleText, Tooltip, } from "@patternfly/react-core"; @@ -24,7 +23,6 @@ import { useConfirmDialog, } from "../components/confirm-dialog/ConfirmDialog"; import { DownloadDialog } from "../components/download-dialog/DownloadDialog"; -import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import type { MultiLine } from "../components/multi-line-input/multi-line-convert"; import { ViewHeader, @@ -44,7 +42,7 @@ import { AdvancedTab } from "./AdvancedTab"; import { ClientSettings } from "./ClientSettings"; import { Credentials } from "./credentials/Credentials"; import { Keys } from "./keys/Keys"; -import type { ClientParams } from "./routes/Client"; +import { ClientParams, ClientTab, toClient } from "./routes/Client"; import { toClients } from "./routes/Clients"; import { ClientScopes } from "./scopes/ClientScopes"; import { EvaluateScopes } from "./scopes/EvaluateScopes"; @@ -63,6 +61,15 @@ import { AuthorizationPermissions } from "./authorization/Permissions"; import { AuthorizationEvaluate } from "./authorization/AuthorizationEvaluate"; import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation"; +import { + routableTab, + RoutableTabs, +} from "../components/routable-tabs/RoutableTabs"; +import { + AuthorizationTab, + toAuthorizationTab, +} from "./routes/AuthenticationTab"; +import { toClientScopesTab } from "./routes/ClientScopeTab"; type ClientDetailHeaderProps = { onChange: (value: boolean) => void; @@ -185,8 +192,6 @@ export default function ClientDetails() { const [downloadDialogOpen, toggleDownloadDialogOpen] = useToggle(); const [changeAuthenticatorOpen, toggleChangeAuthenticatorOpen] = useToggle(); - const [clientScopeSubTab, setClientScopeSubTab] = useState(30); - const [authorizationSubTab, setAuthorizationSubTab] = useState(40); const form = useForm({ shouldUnregister: false }); const { clientId } = useParams(); @@ -341,6 +346,26 @@ export default function ClientDetails() { return ; } + const route = (tab: ClientTab) => + routableTab({ + to: toClient({ + realm, + clientId, + tab, + }), + history, + }); + + const authenticationRoute = (tab: AuthorizationTab) => + routableTab({ + to: toAuthorizationTab({ + realm, + clientId, + tab, + }), + history, + }); + return ( <> - + {t("common:settings")}} + {...route("settings")} > {t("keys")}} + {...route("keys")} > {client.protocol === "openid-connect" && ( @@ -415,8 +442,8 @@ export default function ClientDetails() { {!client.publicClient && !isRealmClient(client) && ( {t("credentials")}} + {...route("credentials")} > save()} /> @@ -424,8 +451,9 @@ export default function ClientDetails() { {!isRealmClient(client) && ( {t("mappers")}} + {...route("mappers")} > {t("roles")}} + {...route("roles")} > {t("clientScopes")}} + {...route("clientScopes")} > - setClientScopeSubTab(key as number)} + {t("setup")}} + {...routableTab({ + to: toClientScopesTab({ + realm, + clientId, + tab: "setup", + }), + history, + })} > {t("evaluate")}} + {...routableTab({ + to: toClientScopesTab({ + realm, + clientId, + tab: "evaluate", + }), + history, + })} > - + )} {client!.serviceAccountsEnabled && ( {t("authorization")}} + {...route("authorization")} > - setAuthorizationSubTab(key as number)} + {t("settings")}} + {...authenticationRoute("settings")} > {t("resources")}} + {...authenticationRoute("resources")} > {t("scopes")}} + {...authenticationRoute("scopes")} > {t("policies")}} + {...authenticationRoute("policies")} > {t("permissions")}} + {...authenticationRoute("permissions")} > {t("evaluate")}} + {...authenticationRoute("evaluate")} > setupForm(client)} /> - + )} {client!.serviceAccountsEnabled && ( {t("serviceAccount")}} + {...route("serviceAccount")} > )} {t("advanced")}} + {...route("advanced")} > - + diff --git a/src/clients/authorization/PermissionDetails.tsx b/src/clients/authorization/PermissionDetails.tsx index 183f9a06d1..ad5c849a22 100644 --- a/src/clients/authorization/PermissionDetails.tsx +++ b/src/clients/authorization/PermissionDetails.tsx @@ -27,9 +27,9 @@ import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog" import { ViewHeader } from "../../components/view-header/ViewHeader"; import { FormAccess } from "../../components/form-access/FormAccess"; import { useAlerts } from "../../components/alert/Alerts"; -import { toClient } from "../routes/Client"; import { HelpItem } from "../../components/help-enabler/HelpItem"; import { ResourcesPolicySelect } from "./ResourcesPolicySelect"; +import { toAuthorizationTab } from "../routes/AuthenticationTab"; const DECISION_STRATEGIES = ["UNANIMOUS", "AFFIRMATIVE", "CONSENSUS"] as const; @@ -140,7 +140,9 @@ export default function PermissionDetails() { permissionId: permissionId, }); addAlert(t("permissionDeletedSuccess"), AlertVariant.success); - history.push(toClient({ realm, clientId: id, tab: "authorization" })); + history.push( + toAuthorizationTab({ realm, clientId: id, tab: "permissions" }) + ); } catch (error) { addError("clients:permissionDeletedError", error); } @@ -329,10 +331,10 @@ export default function PermissionDetails() { component={(props) => ( )} diff --git a/src/clients/authorization/ResourceDetails.tsx b/src/clients/authorization/ResourceDetails.tsx index 5f2987b089..10296bd973 100644 --- a/src/clients/authorization/ResourceDetails.tsx +++ b/src/clients/authorization/ResourceDetails.tsx @@ -31,7 +31,7 @@ import type { MultiLine } from "../../components/multi-line-input/multi-line-con import type { KeyValueType } from "../../components/attribute-form/attribute-convert"; import { convertFormValuesToObject, convertToFormValues } from "../../util"; import { MultiLineInput } from "../../components/multi-line-input/MultiLineInput"; -import { toClient } from "../routes/Client"; +import { toAuthorizationTab } from "../routes/AuthenticationTab"; import { ScopePicker } from "./ScopePicker"; import { AttributeInput } from "../../components/attribute-input/AttributeInput"; @@ -142,7 +142,9 @@ export default function ResourceDetails() { resourceId: resourceId!, }); addAlert(t("resourceDeletedSuccess"), AlertVariant.success); - history.push(toClient({ realm, clientId: id, tab: "authorization" })); + history.push( + toAuthorizationTab({ realm, clientId: id, tab: "resources" }) + ); } catch (error) { addError("clients:resourceDeletedError", error); } @@ -324,10 +326,10 @@ export default function ResourceDetails() { component={(props) => ( )} diff --git a/src/clients/authorization/ScopeDetails.tsx b/src/clients/authorization/ScopeDetails.tsx index 7453fb6300..2b215866f7 100644 --- a/src/clients/authorization/ScopeDetails.tsx +++ b/src/clients/authorization/ScopeDetails.tsx @@ -19,7 +19,7 @@ import type { ScopeDetailsParams } from "../routes/Scope"; import { FormAccess } from "../../components/form-access/FormAccess"; import { HelpItem } from "../../components/help-enabler/HelpItem"; import { ViewHeader } from "../../components/view-header/ViewHeader"; -import { toClient } from "../routes/Client"; +import { toAuthorizationTab } from "../routes/AuthenticationTab"; import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { useAlerts } from "../../components/alert/Alerts"; import useToggle from "../../utils/useToggle"; @@ -77,7 +77,9 @@ export default function ScopeDetails() { iconUri: scope.iconUri, } ); - history.push(toClient({ realm, clientId: id, tab: "authorization" })); + history.push( + toAuthorizationTab({ realm, clientId: id, tab: "scopes" }) + ); } addAlert( t((scopeId ? "update" : "create") + "ScopeSuccess"), @@ -96,7 +98,9 @@ export default function ScopeDetails() { toggleDialog={toggleDeleteDialog} selectedScope={scope} refresh={() => - history.push(toClient({ realm, clientId: id, tab: "authorization" })) + history.push( + toAuthorizationTab({ realm, clientId: id, tab: "scopes" }) + ) } /> ( )} diff --git a/src/clients/authorization/policy/PolicyDetails.tsx b/src/clients/authorization/policy/PolicyDetails.tsx index 0e12599531..e716c1e04b 100644 --- a/src/clients/authorization/policy/PolicyDetails.tsx +++ b/src/clients/authorization/policy/PolicyDetails.tsx @@ -22,7 +22,7 @@ import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDial import { useAdminClient, useFetch } from "../../../context/auth/AdminClient"; import { FormAccess } from "../../../components/form-access/FormAccess"; import { useAlerts } from "../../../components/alert/Alerts"; -import { toClient } from "../../routes/Client"; +import { toAuthorizationTab } from "../../routes/AuthenticationTab"; import { Aggregate } from "./Aggregate"; import { Client } from "./Client"; import { User } from "./User"; @@ -150,7 +150,9 @@ export default function PolicyDetails() { policyId, }); addAlert(t("policyDeletedSuccess"), AlertVariant.success); - history.push(toClient({ realm, clientId: id, tab: "authorization" })); + history.push( + toAuthorizationTab({ realm, clientId: id, tab: "policies" }) + ); } catch (error) { addError("clients:policyDeletedError", error); } @@ -212,10 +214,10 @@ export default function PolicyDetails() { component={(props) => ( )} diff --git a/src/clients/routes.ts b/src/clients/routes.ts index 3aeaeec6cc..38a8fb4ee3 100644 --- a/src/clients/routes.ts +++ b/src/clients/routes.ts @@ -5,6 +5,8 @@ import { ClientsRoute } from "./routes/Clients"; import { CreateInitialAccessTokenRoute } from "./routes/CreateInitialAccessToken"; import { ImportClientRoute } from "./routes/ImportClient"; import { MapperRoute } from "./routes/Mapper"; +import { ClientScopesRoute } from "./routes/ClientScopeTab"; +import { AuthorizationRoute } from "./routes/AuthenticationTab"; import { NewResourceRoute } from "./routes/NewResource"; import { ResourceDetailsRoute } from "./routes/Resource"; import { NewScopeRoute } from "./routes/NewScope"; @@ -21,6 +23,8 @@ const routes: RouteDef[] = [ CreateInitialAccessTokenRoute, ClientRoute, MapperRoute, + ClientScopesRoute, + AuthorizationRoute, NewResourceRoute, ResourceDetailsRoute, NewScopeRoute, diff --git a/src/clients/routes/AuthenticationTab.ts b/src/clients/routes/AuthenticationTab.ts new file mode 100644 index 0000000000..adf3371072 --- /dev/null +++ b/src/clients/routes/AuthenticationTab.ts @@ -0,0 +1,30 @@ +import type { LocationDescriptorObject } from "history"; +import { lazy } from "react"; +import { generatePath } from "react-router-dom"; +import type { RouteDef } from "../../route-config"; + +export type AuthorizationTab = + | "settings" + | "resources" + | "scopes" + | "policies" + | "permissions" + | "evaluate"; + +export type AuthorizationParams = { + realm: string; + clientId: string; + tab: AuthorizationTab; +}; +export const AuthorizationRoute: RouteDef = { + path: "/:realm/clients/:clientId/authorization/:tab", + component: lazy(() => import("../ClientDetails")), + breadcrumb: (t) => t("clients:clientSettings"), + access: "view-clients", +}; + +export const toAuthorizationTab = ( + params: AuthorizationParams +): LocationDescriptorObject => ({ + pathname: generatePath(AuthorizationRoute.path, params), +}); diff --git a/src/clients/routes/Client.ts b/src/clients/routes/Client.ts index 1f495060c5..5c72e38fac 100644 --- a/src/clients/routes/Client.ts +++ b/src/clients/routes/Client.ts @@ -5,11 +5,14 @@ import type { RouteDef } from "../../route-config"; export type ClientTab = | "settings" + | "keys" + | "credentials" | "roles" | "clientScopes" | "advanced" | "mappers" - | "authorization"; + | "authorization" + | "serviceAccount"; export type ClientParams = { realm: string; diff --git a/src/clients/routes/ClientScopeTab.ts b/src/clients/routes/ClientScopeTab.ts new file mode 100644 index 0000000000..adb9ffa5ef --- /dev/null +++ b/src/clients/routes/ClientScopeTab.ts @@ -0,0 +1,24 @@ +import type { LocationDescriptorObject } from "history"; +import { lazy } from "react"; +import { generatePath } from "react-router-dom"; +import type { RouteDef } from "../../route-config"; + +export type ClientScopesTab = "setup" | "evaluate"; + +export type ClientScopesParams = { + realm: string; + clientId: string; + tab: ClientScopesTab; +}; +export const ClientScopesRoute: RouteDef = { + path: "/:realm/clients/:clientId/clientScopes/:tab", + component: lazy(() => import("../ClientDetails")), + breadcrumb: (t) => t("clients:clientSettings"), + access: "view-clients", +}; + +export const toClientScopesTab = ( + params: ClientScopesParams +): LocationDescriptorObject => ({ + pathname: generatePath(ClientScopesRoute.path, params), +});