diff --git a/cypress/integration/realm_settings_test.spec.ts b/cypress/integration/realm_settings_test.spec.ts index d89fe90e5e..631f82a1d9 100644 --- a/cypress/integration/realm_settings_test.spec.ts +++ b/cypress/integration/realm_settings_test.spec.ts @@ -118,7 +118,7 @@ describe("Realm settings tests", () => { masthead.checkNotificationMessage("Realm successfully updated"); }); - it.skip("Go to login tab", () => { + it("Go to login tab", () => { sidebarPage.goToRealmSettings(); cy.findByTestId("rs-login-tab").click(); realmSettingsPage.toggleSwitch(realmSettingsPage.userRegSwitch); @@ -126,7 +126,7 @@ describe("Realm settings tests", () => { realmSettingsPage.toggleSwitch(realmSettingsPage.rememberMeSwitch); }); - it.skip("Check login tab values", () => { + it("Check login tab values", () => { sidebarPage.goToRealmSettings(); cy.findByTestId("rs-login-tab").click(); @@ -605,20 +605,32 @@ describe("Realm settings tests", () => { realmSettingsPage.shouldCancelAddingCondition(); }); - it("Should add a new condition to a client profile", () => { - realmSettingsPage.shouldAddCondition(); + it("Should add a new client-roles condition to a client profile", () => { + realmSettingsPage.shouldAddClientRolesCondition(); }); - it("Should edit the condition of a client profile", () => { - realmSettingsPage.shouldEditCondition(); + it("Should add a new client-scopes condition to a client profile", () => { + realmSettingsPage.shouldAddClientScopesCondition(); + }); + + it("Should edit the client-roles condition of a client profile", () => { + realmSettingsPage.shouldEditClientRolesCondition(); + }); + + it("Should edit the client-scopes condition of a client profile", () => { + realmSettingsPage.shouldEditClientScopesCondition(); }); it("Should cancel deleting condition from a client profile", () => { realmSettingsPage.shouldCancelDeletingCondition(); }); - it("Should delete condition from a client profile", () => { - realmSettingsPage.shouldDeleteCondition(); + it("Should delete client-roles condition from a client profile", () => { + realmSettingsPage.shouldDeleteClientRolesCondition(); + }); + + it("Should delete client-scopes condition from a client profile", () => { + realmSettingsPage.shouldDeleteClientScopesCondition(); }); it("Check cancelling the client policy deletion", () => { diff --git a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts index bd7f3137b1..4bb10b33a9 100644 --- a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts +++ b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts @@ -197,6 +197,8 @@ export default class RealmSettingsPage { private addConditionCancelBtn = "addCondition-cancelBtn"; private addConditionSaveBtn = "addCondition-saveBtn"; private conditionTypeLink = "condition-type-link"; + private clientRolesConditionLink = "client-roles-condition-link"; + private clientScopesConditionLink = "client-scopes-condition-link"; private eventListenersFormLabel = ".pf-c-form__label-text"; private eventListenersDrpDwn = ".pf-c-select.kc_eventListeners_select"; private eventListenersSaveBtn = "saveEventListenerBtn"; @@ -208,6 +210,9 @@ export default class RealmSettingsPage { ".pf-c-button.pf-c-select__toggle-button.pf-m-plain"; private eventListenerRemove = '[data-ouia-component-id="Remove"]'; private roleSelect = ".pf-c-select.kc-role-select"; + private selectScopeButton = "select-scope-button"; + private deleteClientRolesCondition = "delete-client-roles-condition"; + private deleteClientScopesCondition = "delete-client-scopes-condition"; selectLoginThemeType(themeType: string) { cy.get(this.selectLoginTheme).click(); @@ -966,7 +971,7 @@ export default class RealmSettingsPage { ); } - shouldAddCondition() { + shouldAddClientRolesCondition() { cy.get(this.clientPolicy).click(); cy.findByTestId(this.addCondition).click(); cy.get(this.addConditionDrpDwn).click(); @@ -989,10 +994,37 @@ export default class RealmSettingsPage { cy.get('ul[class*="pf-c-data-list"]').should("have.text", "client-roles"); } - shouldEditCondition() { + addClientScopes() { + cy.findByTestId(this.selectScopeButton).click(); + cy.get(".pf-c-table__check > input[name=checkrow0]").click(); + cy.get(".pf-c-table__check > input[name=checkrow1]").click(); + cy.get(".pf-c-table__check > input[name=checkrow2]").click(); + + cy.findByTestId("modalConfirm").contains("Add").click(); + } + + shouldAddClientScopesCondition() { + cy.get(this.clientPolicy).click(); + cy.findByTestId(this.addCondition).click(); + cy.get(this.addConditionDrpDwn).click(); + cy.findByTestId(this.addConditionDrpDwnOption) + .contains("client-scopes") + .click(); + + this.addClientScopes(); + + cy.findByTestId(this.addConditionSaveBtn).click(); + cy.get(this.alertMessage).should( + "be.visible", + "Success! Condition created successfully" + ); + cy.get('ul[class*="pf-c-data-list"]').contains("client-scopes"); + } + + shouldEditClientRolesCondition() { cy.get(this.clientPolicy).click(); - cy.findByTestId(this.conditionTypeLink).contains("client-roles").click(); + cy.findByTestId(this.clientRolesConditionLink).click(); cy.get(this.roleSelect).click(); cy.get(this.roleSelect).contains("create-client").click(); @@ -1006,26 +1038,53 @@ export default class RealmSettingsPage { ); } + shouldEditClientScopesCondition() { + cy.get(this.clientPolicy).click(); + + cy.findByTestId(this.clientScopesConditionLink).click(); + + cy.wait(200); + + this.addClientScopes(); + + cy.findByTestId(this.addConditionSaveBtn).click(); + cy.get(this.alertMessage).should( + "be.visible", + "Success! Condition updated successfully" + ); + } + shouldCancelDeletingCondition() { cy.get(this.clientPolicy).click(); - cy.get('svg[class*="kc-conditionType-trash-icon"]').click(); + cy.findByTestId(this.deleteClientRolesCondition).click(); cy.get(this.deleteDialogTitle).contains("Delete condition?"); cy.get(this.deleteDialogBodyText).contains( "This action will permanently delete client-roles. This cannot be undone." ); cy.findByTestId("modalConfirm").contains("Delete"); cy.get(this.deleteDialogCancelBtn).contains("Cancel").click(); - cy.get('ul[class*="pf-c-data-list"]').should("have.text", "client-roles"); + cy.get('ul[class*="pf-c-data-list"]').contains("client-roles"); } - shouldDeleteCondition() { + shouldDeleteClientRolesCondition() { cy.get(this.clientPolicy).click(); - cy.get('svg[class*="kc-conditionType-trash-icon"]').click(); + cy.findByTestId(this.deleteClientRolesCondition).click(); cy.get(this.deleteDialogTitle).contains("Delete condition?"); cy.get(this.deleteDialogBodyText).contains( "This action will permanently delete client-roles. This cannot be undone." ); cy.findByTestId("modalConfirm").contains("Delete").click(); + cy.get('ul[class*="pf-c-data-list"]').contains("client-scopes"); + } + + shouldDeleteClientScopesCondition() { + cy.get(this.clientPolicy).click(); + cy.findByTestId(this.deleteClientScopesCondition).click(); + cy.get(this.deleteDialogTitle).contains("Delete condition?"); + cy.get(this.deleteDialogBodyText).contains( + "This action will permanently delete client-scopes. This cannot be undone." + ); + cy.findByTestId("modalConfirm").contains("Delete").click(); cy.get('h6[class*="kc-emptyConditions"]').should( "have.text", "No conditions configured" diff --git a/src/client-scopes/ClientScopesSection.tsx b/src/client-scopes/ClientScopesSection.tsx index 7de0fc594e..342ded148f 100644 --- a/src/client-scopes/ClientScopesSection.tsx +++ b/src/client-scopes/ClientScopesSection.tsx @@ -19,7 +19,7 @@ import { useAlerts } from "../components/alert/Alerts"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useRealm } from "../context/realm-context/RealmContext"; -import { upperCaseFormatter, emptyFormatter } from "../util"; +import { emptyFormatter } from "../util"; import { CellDropdown, ClientScope, @@ -44,6 +44,7 @@ import { typeFilter, } from "./details/SearchFilter"; import type { Row } from "../clients/scopes/ClientScopes"; +import { getProtocolName } from "../clients/utils"; export default function ClientScopesSection() { const { realm } = useRealm(); @@ -267,7 +268,8 @@ export default function ClientScopesSection() { { name: "protocol", displayKey: "client-scopes:protocol", - cellFormatters: [upperCaseFormatter()], + cellRenderer: (client) => + getProtocolName(t, client.protocol ?? "openid-connect"), transforms: [cellWidth(15)], }, { diff --git a/src/clients/ClientDetails.tsx b/src/clients/ClientDetails.tsx index a06e8fb9c0..cde0c902fa 100644 --- a/src/clients/ClientDetails.tsx +++ b/src/clients/ClientDetails.tsx @@ -458,6 +458,7 @@ export default function ClientDetails() { title={{t("setup")}} > diff --git a/src/clients/messages.ts b/src/clients/messages.ts index 52c888b428..3fcc3fb193 100644 --- a/src/clients/messages.ts +++ b/src/clients/messages.ts @@ -1,9 +1,11 @@ export default { clients: { - protocol: { + protocolTypes: { openIdConnect: "OpenID Connect", saml: "SAML", + all: "All", }, + protocol: "Protocol", clientType: "Client type", clientAuthorization: "Authorization", implicitFlow: "Implicit flow", @@ -27,7 +29,7 @@ export default { "You haven't created any roles for this client. Create a role to get started.", clientScopes: "Client scopes", addClientScope: "Add client scope", - addClientScopesTo: "Add client scopes to {{clientId}}", + addClientScopesTo: "Add client scopes to {{clientName}}", clientScopeRemoveSuccess: "Scope mapping successfully removed", clientScopeRemoveError: "Could not remove the scope mapping {{error}}", clientScopeSuccess: "Scope mapping successfully updated", diff --git a/src/clients/scopes/AddScopeDialog.tsx b/src/clients/scopes/AddScopeDialog.tsx index 8c8025806c..afe0fecfe9 100644 --- a/src/clients/scopes/AddScopeDialog.tsx +++ b/src/clients/scopes/AddScopeDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, @@ -8,8 +8,17 @@ import { Modal, ModalVariant, DropdownDirection, + DropdownItem, + Select, + SelectOption, + SelectVariant, + SelectDirection, } from "@patternfly/react-core"; -import { CaretUpIcon } from "@patternfly/react-icons"; +import { + CaretDownIcon, + CaretUpIcon, + FilterIcon, +} from "@patternfly/react-icons"; import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; import { @@ -19,27 +28,65 @@ import { import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; import "./client-scopes.css"; +import { getProtocolName } from "../utils"; export type AddScopeDialogProps = { clientScopes: ClientScopeRepresentation[]; + clientName?: string; open: boolean; toggleDialog: () => void; onAdd: ( - scopes: { scope: ClientScopeRepresentation; type: ClientScopeType }[] + scopes: { scope: ClientScopeRepresentation; type?: ClientScopeType }[] ) => void; + isClientScopesConditionType?: boolean; }; +enum FilterType { + Name = "Name", + Protocol = "Protocol", +} + +enum ProtocolType { + All = "All", + SAML = "SAML", + OpenIDConnect = "OpenID Connect", +} + export const AddScopeDialog = ({ clientScopes, + clientName, open, toggleDialog, onAdd, + isClientScopesConditionType, }: AddScopeDialogProps) => { const { t } = useTranslation("clients"); const [addToggle, setAddToggle] = useState(false); const [rows, setRows] = useState([]); + const [filterType, setFilterType] = useState(FilterType.Name); + const [protocolType, setProtocolType] = useState(ProtocolType.All); + const [key, setKey] = useState(0); + const refresh = () => setKey(key + 1); - const loader = () => Promise.resolve(clientScopes); + const [isFilterTypeDropdownOpen, setIsFilterTypeDropdownOpen] = + useState(false); + + const [isProtocolTypeDropdownOpen, setIsProtocolTypeDropdownOpen] = + useState(false); + + useEffect(() => { + refresh(); + }, [filterType, protocolType]); + + const loader = async () => { + if (protocolType === ProtocolType.OpenIDConnect) { + return clientScopes.filter((item) => item.protocol === "openid-connect"); + } else if (protocolType === ProtocolType.SAML) { + return clientScopes.filter((item) => item.protocol === "saml"); + } + + return clientScopes; + }; const action = (scope: ClientScopeType) => { const scopes = rows.map((row) => { @@ -50,55 +97,213 @@ export const AddScopeDialog = ({ toggleDialog(); }; + const onFilterTypeDropdownToggle = () => { + setIsFilterTypeDropdownOpen(!isFilterTypeDropdownOpen); + }; + + const onProtocolTypeDropdownToggle = () => { + setIsProtocolTypeDropdownOpen(!isProtocolTypeDropdownOpen); + }; + + const onFilterTypeDropdownSelect = (filterType: string) => { + if (filterType === FilterType.Name) { + setFilterType(FilterType.Protocol); + } else if (filterType === FilterType.Protocol) { + setFilterType(FilterType.Name); + } + + setIsFilterTypeDropdownOpen(!isFilterTypeDropdownOpen); + }; + + const onProtocolTypeDropdownSelect = (protocolType: string) => { + if (protocolType === ProtocolType.SAML) { + setProtocolType(ProtocolType.SAML); + } else if (protocolType === ProtocolType.OpenIDConnect) { + setProtocolType(ProtocolType.OpenIDConnect); + } else if (protocolType === ProtocolType.All) { + setProtocolType(ProtocolType.All); + } + + setIsProtocolTypeDropdownOpen(!isProtocolTypeDropdownOpen); + }; + + const protocolTypeOptions = [ + + {t("protocolTypes.saml")} + , + + {t("protocolTypes.openIdConnect")} + , + + {t("protocolTypes.all")} + , + ]; + return ( setAddToggle(!addToggle)} - isPrimary - toggleIndicator={CaretUpIcon} - id="add-scope-toggle" - > - {t("common:add")} - - } - dropdownItems={clientScopeTypesDropdown(t, action)} - />, - , - ]} + actions={ + isClientScopesConditionType + ? [ + , + , + ] + : [ + setAddToggle(!addToggle)} + isPrimary + toggleIndicator={CaretUpIcon} + id="add-scope-toggle" + > + {t("common:add")} + + } + dropdownItems={clientScopeTypesDropdown(t, action)} + />, + , + ] + } > { + onFilterTypeDropdownSelect(filterType); + }} + data-testid="filter-type-dropdown" + toggle={ + } + > + {filterType} + + } + isOpen={isFilterTypeDropdownOpen} + dropdownItems={[ + + {filterType === FilterType.Name + ? t("protocol") + : t("common:name")} + , + ]} + /> + } + key={key} + toolbarItem={ + filterType === FilterType.Protocol && ( + <> + { + onFilterTypeDropdownSelect(filterType); + }} + data-testid="filter-type-dropdown" + toggle={ + } + > + {filterType} + + } + isOpen={isFilterTypeDropdownOpen} + dropdownItems={[ + + {t("common:name")} + , + ]} + /> + + + ) + } canSelectAll onSelect={(rows) => setRows(rows)} columns={[ { name: "name", }, + { + name: "protocol", + displayKey: "clients:protocol", + cellRenderer: (client) => + getProtocolName(t, client.protocol ?? "openid-connect"), + }, { name: "description", }, diff --git a/src/clients/scopes/ClientScopes.tsx b/src/clients/scopes/ClientScopes.tsx index 8e659eb787..659fa2a4e6 100644 --- a/src/clients/scopes/ClientScopes.tsx +++ b/src/clients/scopes/ClientScopes.tsx @@ -38,6 +38,7 @@ import { ChangeTypeDropdown } from "../../client-scopes/ChangeTypeDropdown"; export type ClientScopesProps = { clientId: string; protocol: string; + clientName: string; }; export type Row = ClientScopeRepresentation & { @@ -45,7 +46,11 @@ export type Row = ClientScopeRepresentation & { description?: string; }; -export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { +export const ClientScopes = ({ + clientId, + protocol, + clientName, +}: ClientScopesProps) => { const { t } = useTranslation("clients"); const adminClient = useAdminClient(); const { addAlert, addError } = useAlerts(); @@ -135,6 +140,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { {rest && ( setAddDialogOpen(!addDialogOpen)} onAdd={async (scopes) => { @@ -146,7 +152,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { adminClient, clientId, scope.scope, - scope.type + scope.type! ) ) ); diff --git a/src/clients/scopes/client-scopes.css b/src/clients/scopes/client-scopes.css index 4286158636..48ebef95fc 100644 --- a/src/clients/scopes/client-scopes.css +++ b/src/clients/scopes/client-scopes.css @@ -5,4 +5,8 @@ .keycloak__client-scopes-add__add-dropdown { margin-right: var(--pf-global--spacer--md); +} + +.kc-protocolType-select { + max-width: 25%; } \ No newline at end of file diff --git a/src/clients/utils.ts b/src/clients/utils.ts index 3a48759ca6..6c18ee724c 100644 --- a/src/clients/utils.ts +++ b/src/clients/utils.ts @@ -12,9 +12,9 @@ export const isRealmClient = (client: ClientRepresentation) => !client.protocol; export const getProtocolName = (t: TFunction<"clients">, protocol: string) => { switch (protocol) { case "openid-connect": - return t("clients:protocol:openIdConnect"); + return t("clients:protocolTypes:openIdConnect"); case "saml": - return t("clients:protocol:saml"); + return t("clients:protocolTypes:saml"); } return protocol; diff --git a/src/components/dynamic/MultivaluedScopesComponent.tsx b/src/components/dynamic/MultivaluedScopesComponent.tsx new file mode 100644 index 0000000000..d0ce381916 --- /dev/null +++ b/src/components/dynamic/MultivaluedScopesComponent.tsx @@ -0,0 +1,129 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Controller, useFormContext } from "react-hook-form"; +import { + Button, + Chip, + ChipGroup, + FormGroup, + TextInput, +} from "@patternfly/react-core"; + +import { HelpItem } from "../help-enabler/HelpItem"; +import type { ComponentProps } from "./components"; +import { AddScopeDialog } from "../../clients/scopes/AddScopeDialog"; +import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; +import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; +import { useParams } from "react-router"; +import type { EditClientPolicyConditionParams } from "../../realm-settings/routes/EditCondition"; + +export const MultivaluedScopesComponent = ({ + defaultValue, + name, +}: ComponentProps) => { + const { t } = useTranslation("dynamic"); + const { control } = useFormContext(); + const { conditionName } = useParams(); + const adminClient = useAdminClient(); + const [open, setOpen] = useState(false); + const [clientScopes, setClientScopes] = useState( + [] + ); + + useFetch( + () => adminClient.clientScopes.find(), + (clientScopes) => { + setClientScopes(clientScopes); + }, + [] + ); + + const toggleModal = () => { + setOpen(!open); + }; + + return ( + + } + fieldId={name!} + > + { + return ( + <> + {open && ( + !value.includes(scope.name!) + )} + isClientScopesConditionType + open={open} + toggleDialog={() => setOpen(!open)} + onAdd={(scopes) => { + onChange([ + ...value, + ...scopes + .map((scope) => scope.scope) + .map((item) => item.name!), + ]); + }} + /> + )} + {value.length === 0 && !conditionName && ( + + )} + { + onChange([]); + }} + > + {value.map((currentChip: string) => ( + { + onChange( + value.filter((item: string) => item !== currentChip) + ); + }} + > + {currentChip} + + ))} + + + + ); + }} + /> + + ); +}; diff --git a/src/realm-settings/NewClientPolicyCondition.tsx b/src/realm-settings/NewClientPolicyCondition.tsx index c10fc836a8..6a2ee6eefe 100644 --- a/src/realm-settings/NewClientPolicyCondition.tsx +++ b/src/realm-settings/NewClientPolicyCondition.tsx @@ -34,12 +34,12 @@ import { convertToMultiline, toValue, } from "../components/multi-line-input/MultiLineInput"; -import { MultivaluedRoleComponent } from "../components/dynamic/MultivaluedRoleComponent"; import { COMPONENTS, isValidComponentType, } from "../components/dynamic/components"; - +import { MultivaluedScopesComponent } from "../components/dynamic/MultivaluedScopesComponent"; +import { MultivaluedRoleComponent } from "../components/dynamic/MultivaluedRoleComponent"; export type ItemType = { value: string }; type ConfigProperty = ConfigPropertyRepresentation & { @@ -54,6 +54,7 @@ export default function NewClientPolicyCondition() { const [openConditionType, setOpenConditionType] = useState(false); const [policies, setPolicies] = useState([]); + const [condition, setCondition] = useState< ClientPolicyConditionRepresentation[] >([]); @@ -87,9 +88,15 @@ export default function NewClientPolicyCondition() { Object.entries(condition.configuration!).map(([key, value]) => { const formKey = `config.${key}`; + const property = properties.find((p) => p.name === key); - if (property?.type === "MultivaluedString") { + if ( + property?.type === "MultivaluedString" && + property.name !== "scopes" + ) { form.setValue(formKey, convertToMultiline(value)); + } else if (property?.name === "client-scopes") { + form.setValue("config.scopes", value); } else { form.setValue(formKey, value); } @@ -98,8 +105,10 @@ export default function NewClientPolicyCondition() { useFetch( () => adminClient.clientPolicies.listPolicies(), + (policies) => { setPolicies(policies.policies ?? []); + if (conditionName) { const currentPolicy = policies.policies?.find( (item) => item.name === policyName @@ -124,13 +133,14 @@ export default function NewClientPolicyCondition() { const save = async (configPolicy: ConfigProperty) => { const configValues = configPolicy.config; - const writeConfig = () => - conditionProperties.reduce((r: any, p) => { - p.type === "MultivaluedString" + const writeConfig = () => { + return conditionProperties.reduce((r: any, p) => { + p.type === "MultivaluedString" && p.name !== "scopes" ? (r[p.name!] = toValue(configValues[p.name!])) : (r[p.name!] = configValues[p.name!]); return r; }, {}); + }; const updatedPolicies = policies.map((policy) => { if (policy.name !== policyName) { @@ -284,6 +294,17 @@ export default function NewClientPolicyCondition() { conditionName === "client-roles") ) { return ; + } else if ( + property.name === "scopes" && + (conditionType === "client-scopes" || + conditionName === "client-scopes") + ) { + return ( + + ); } else if (isValidComponentType(componentType)) { const Component = COMPONENTS[componentType]; return ; diff --git a/src/realm-settings/NewClientPolicyForm.tsx b/src/realm-settings/NewClientPolicyForm.tsx index 273300f70f..a669224b57 100644 --- a/src/realm-settings/NewClientPolicyForm.tsx +++ b/src/realm-settings/NewClientPolicyForm.tsx @@ -551,7 +551,7 @@ export default function NewClientPolicyForm() { 0 ? ( { toggleDeleteConditionDialog(); setConditionToDelete({ diff --git a/src/realm-settings/RealmSettingsSection.css b/src/realm-settings/RealmSettingsSection.css index 9708734e60..a70ce217fb 100644 --- a/src/realm-settings/RealmSettingsSection.css +++ b/src/realm-settings/RealmSettingsSection.css @@ -252,3 +252,16 @@ article.pf-c-card.pf-m-flat.kc-login-settings-template .kc_eventListeners_select { width: 35rem; } + +input#kc-scopes { + width: 630px; + + margin-right: 24px; +} + +.kc-client-scopes-chip-group { + margin-right: var(--pf-global--spacer--2xl); + max-width: 585px; + min-width: 585px; + padding-left: none; +}