From 6dd314c76876c240b1d41e6ff1207bc7cbf13103 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Wed, 21 Apr 2021 15:18:45 +0200 Subject: [PATCH] Initial version of the identity providers section (#537) * initial version identity providers section * added order change dialog * added tests * added missing brand icons * removed need for providerCount * fixed refresh * back to list after create * format merge * fixed merge error --- .../integration/identity_providers.spec.ts | 91 ++++++ .../identity_providers/CreateProviderPage.ts | 68 +++++ .../manage/identity_providers/OrderDialog.ts | 37 +++ src/authentication/AuthenticationSection.tsx | 2 +- src/client-scopes/ClientScopesSection.tsx | 4 +- src/clients/ClientsSection.tsx | 2 +- src/common-messages.json | 2 + src/components/view-header/ViewHeader.tsx | 8 +- src/i18n.ts | 6 +- .../IdentityProvidersSection.tsx | 261 +++++++++++++++++- src/identity-providers/ManageOrderDialog.tsx | 146 ++++++++++ src/identity-providers/ProviderIconMapper.tsx | 50 ++++ .../add/AddIdentityProvider.tsx | 190 +++++++++++++ src/identity-providers/help.json | 8 + .../icons/FontAwesomeIcon.tsx | 24 ++ .../icons/bitbucket-brands.svg | 1 + .../icons/instagram-brands.svg | 1 + .../icons/microsoft-brands.svg | 1 + .../icons/paypal-brands.svg | 1 + src/identity-providers/messages.json | 30 ++ src/realm-roles/RolesList.tsx | 4 +- src/realm-roles/UsersInRoleTab.tsx | 4 +- src/realm-settings/RealmSettingsSection.tsx | 1 - src/route-config.ts | 7 + .../UserFederationKerberosSettings.tsx | 3 +- .../UserFederationLdapSettings.tsx | 3 +- .../ldap/mappers/LdapMapperDetails.tsx | 1 - src/user/UsersSection.tsx | 2 +- src/user/UsersTabs.tsx | 2 +- src/util.ts | 10 +- 30 files changed, 941 insertions(+), 29 deletions(-) create mode 100644 cypress/integration/identity_providers.spec.ts create mode 100644 cypress/support/pages/admin_console/manage/identity_providers/CreateProviderPage.ts create mode 100644 cypress/support/pages/admin_console/manage/identity_providers/OrderDialog.ts create mode 100644 src/identity-providers/ManageOrderDialog.tsx create mode 100644 src/identity-providers/ProviderIconMapper.tsx create mode 100644 src/identity-providers/add/AddIdentityProvider.tsx create mode 100644 src/identity-providers/help.json create mode 100644 src/identity-providers/icons/FontAwesomeIcon.tsx create mode 100644 src/identity-providers/icons/bitbucket-brands.svg create mode 100644 src/identity-providers/icons/instagram-brands.svg create mode 100644 src/identity-providers/icons/microsoft-brands.svg create mode 100644 src/identity-providers/icons/paypal-brands.svg create mode 100644 src/identity-providers/messages.json diff --git a/cypress/integration/identity_providers.spec.ts b/cypress/integration/identity_providers.spec.ts new file mode 100644 index 0000000000..047bfed698 --- /dev/null +++ b/cypress/integration/identity_providers.spec.ts @@ -0,0 +1,91 @@ +import Masthead from "../support/pages/admin_console/Masthead"; +import SidebarPage from "../support/pages/admin_console/SidebarPage"; +import LoginPage from "../support/pages/LoginPage"; +import { keycloakBefore } from "../support/util/keycloak_before"; +import ListingPage from "../support/pages/admin_console/ListingPage"; + +import CreateProviderPage from "../support/pages/admin_console/manage/identity_providers/CreateProviderPage"; +import ModalUtils from "../support/util/ModalUtils"; +import OrderDialog from "../support/pages/admin_console/manage/identity_providers/OrderDialog"; + +describe("Identity provider test", () => { + const loginPage = new LoginPage(); + const sidebarPage = new SidebarPage(); + const masthead = new Masthead(); + const listingPage = new ListingPage(); + const createProviderPage = new CreateProviderPage(); + + describe("Identity provider creation", () => { + const identityProviderName = "github"; + + beforeEach(function () { + keycloakBefore(); + loginPage.logIn(); + sidebarPage.goToIdentityProviders(); + }); + + it("should create provider", () => { + createProviderPage.checkGitHubCardVisible().clickGitHubCard(); + + createProviderPage.checkAddButtonDisabled(); + createProviderPage + .fill(identityProviderName) + .clickAdd() + .checkClientIdRequiredMessage(true); + createProviderPage.fill(identityProviderName, "123").clickAdd(); + masthead.checkNotificationMessage( + "Identity provider successfully created" + ); + + //TODO temporary refresh + sidebarPage.goToAuthentication().goToIdentityProviders(); + + listingPage.itemExist(identityProviderName); + }); + + it("should delete provider", () => { + const modalUtils = new ModalUtils(); + listingPage.deleteItem(identityProviderName); + modalUtils.checkModalTitle("Delete provider?").confirmModal(); + + masthead.checkNotificationMessage("Provider successfully deleted"); + + createProviderPage.checkGitHubCardVisible(); + }); + + it("should change order of providers", () => { + const orderDialog = new OrderDialog(); + const providers = ["facebook", identityProviderName, "bitbucket"]; + + createProviderPage + .clickCard("facebook") + .fill("facebook", "123") + .clickAdd(); + sidebarPage.goToIdentityProviders(); + listingPage.itemExist("facebook"); + + createProviderPage + .clickCreateDropdown() + .clickItem(identityProviderName) + .fill(identityProviderName, "123") + .clickAdd(); + sidebarPage.goToIdentityProviders(); + createProviderPage + .clickCreateDropdown() + .clickItem("bitbucket") + .fill("bitbucket", "123") + .clickAdd(); + sidebarPage.goToIdentityProviders(); + + orderDialog.openDialog().checkOrder(providers); + orderDialog.moveRowTo("facebook", identityProviderName); + + orderDialog.checkOrder(["facebook", "bitbucket", identityProviderName]); + + orderDialog.clickSave(); + masthead.checkNotificationMessage( + "Successfully changed display order of identity providers" + ); + }); + }); +}); diff --git a/cypress/support/pages/admin_console/manage/identity_providers/CreateProviderPage.ts b/cypress/support/pages/admin_console/manage/identity_providers/CreateProviderPage.ts new file mode 100644 index 0000000000..a2a8a85958 --- /dev/null +++ b/cypress/support/pages/admin_console/manage/identity_providers/CreateProviderPage.ts @@ -0,0 +1,68 @@ +export default class CreateProviderPage { + private github = "github"; + private addProviderDropdown = "addProviderDropdown"; + private clientIdField = "clientId"; + private clientIdError = "#kc-client-secret-helper"; + private clientSecretField = "clientSecret"; + private addButton = "createProvider"; + + checkVisible(name: string) { + cy.getId(`${name}-card`).should("exist"); + return this; + } + + clickCard(name: string) { + cy.getId(`${name}-card`).click(); + return this; + } + + clickGitHubCard() { + this.clickCard(this.github); + return this; + } + + checkGitHubCardVisible() { + this.checkVisible(this.github); + return this; + } + + checkClientIdRequiredMessage(exist = true) { + cy.get(this.clientIdError).should((!exist ? "not." : "") + "exist"); + + return this; + } + + checkAddButtonDisabled(disabled = true) { + cy.getId(this.addButton).should(!disabled ? "not." : "" + "be.disabled"); + return this; + } + + clickAdd() { + cy.getId(this.addButton).click(); + return this; + } + + clickCreateDropdown() { + cy.getId(this.addProviderDropdown).click(); + return this; + } + + clickItem(item: string) { + cy.getId(item).click(); + return this; + } + + fill(id: string, secret = "") { + cy.getId(this.clientIdField).clear(); + + if (id) { + cy.getId(this.clientIdField).type(id); + } + + if (secret) { + cy.getId(this.clientSecretField).type(secret); + } + + return this; + } +} diff --git a/cypress/support/pages/admin_console/manage/identity_providers/OrderDialog.ts b/cypress/support/pages/admin_console/manage/identity_providers/OrderDialog.ts new file mode 100644 index 0000000000..e1bd6e70bd --- /dev/null +++ b/cypress/support/pages/admin_console/manage/identity_providers/OrderDialog.ts @@ -0,0 +1,37 @@ +const expect = chai.expect; + +export default class OrderDialog { + private manageDisplayOrder = "manageDisplayOrder"; + private list = "manageOrderDataList"; + + openDialog() { + cy.getId(this.manageDisplayOrder).click({ force: true }); + return this; + } + + moveRowTo(from: string, to: string) { + cy.getId(from).trigger("dragstart").trigger("dragleave"); + + cy.getId(to) + .trigger("dragenter") + .trigger("dragover") + .trigger("drop") + .trigger("dragend"); + + return this; + } + + clickSave() { + cy.get("#modal-confirm").click(); + return this; + } + + checkOrder(providerNames: string[]) { + cy.get(`[data-testid=${this.list}] li`).should((providers) => { + expect(providers).to.have.length(providerNames.length); + for (let index = 0; index < providerNames.length; index++) { + expect(providers.eq(index)).to.contain(providerNames[index]); + } + }); + } +} diff --git a/src/authentication/AuthenticationSection.tsx b/src/authentication/AuthenticationSection.tsx index 06b84cca0f..0d59cad2b5 100644 --- a/src/authentication/AuthenticationSection.tsx +++ b/src/authentication/AuthenticationSection.tsx @@ -185,7 +185,7 @@ export const AuthenticationSection = () => { }} /> )} - + { { name: "protocol", displayKey: "client-scopes:protocol", - cellFormatters: [boolFormatter()], + cellFormatters: [upperCaseFormatter()], transforms: [cellWidth(15)], }, { diff --git a/src/clients/ClientsSection.tsx b/src/clients/ClientsSection.tsx index 4f255f8ddf..214724c881 100644 --- a/src/clients/ClientsSection.tsx +++ b/src/clients/ClientsSection.tsx @@ -75,7 +75,7 @@ export const ClientsSection = () => { {client.clientId} {!client.enabled && ( - Disabled + {t("common:disabled")} )} diff --git a/src/common-messages.json b/src/common-messages.json index 4ff765dca8..6c016b8297 100644 --- a/src/common-messages.json +++ b/src/common-messages.json @@ -54,6 +54,8 @@ "priority": "Priority", "unexpectedError": "An unexpected error occurred: '{{error}}'", "retry": "Retry", + "plus": "Plus", + "minus": "Minus", "clientScope": { "default": "Default", diff --git a/src/components/view-header/ViewHeader.tsx b/src/components/view-header/ViewHeader.tsx index 2d06b33fa0..0eec0490f6 100644 --- a/src/components/view-header/ViewHeader.tsx +++ b/src/components/view-header/ViewHeader.tsx @@ -27,7 +27,7 @@ export type ViewHeaderProps = { badge?: string; badgeId?: string; badgeIsRead?: boolean; - subKey: string | ReactNode; + subKey?: string | ReactNode; actionsDropdownId?: string; subKeyLinkProps?: FormattedLinkProps; dropdownItems?: ReactElement[]; @@ -133,7 +133,11 @@ export const ViewHeader = ({ {enabled && ( - {React.isValidElement(subKey) ? subKey : t(subKey as string)} + {React.isValidElement(subKey) + ? subKey + : subKey + ? t(subKey as string) + : ""} {subKeyLinkProps && ( ( - -); +import IdentityProviderRepresentation from "keycloak-admin/lib/defs/identityProviderRepresentation"; +import { ViewHeader } from "../components/view-header/ViewHeader"; +import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient"; +import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; +import { useRealm } from "../context/realm-context/RealmContext"; +import { useAlerts } from "../components/alert/Alerts"; +import { useServerInfo } from "../context/server-info/ServerInfoProvider"; +import { upperCaseFormatter } from "../util"; +import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; +import { ProviderIconMapper } from "./ProviderIconMapper"; +import { ManageOderDialog } from "./ManageOrderDialog"; + +export const IdentityProvidersSection = () => { + const { t } = useTranslation("identity-providers"); + const identityProviders = _.groupBy( + useServerInfo().identityProviders, + "groupName" + ); + const { realm } = useRealm(); + const { url } = useRouteMatch(); + const history = useHistory(); + const [key, setKey] = useState(0); + const refresh = () => setKey(new Date().getTime()); + + const [addProviderOpen, setAddProviderOpen] = useState(false); + const [manageDisplayDialog, setManageDisplayDialog] = useState(false); + const [providers, setProviders] = useState( + [] + ); + const [selectedProvider, setSelectedProvider] = useState< + IdentityProviderRepresentation + >(); + + const adminClient = useAdminClient(); + const errorHandler = useErrorHandler(); + const { addAlert } = useAlerts(); + + useEffect( + () => + asyncStateFetch( + async () => + (await adminClient.realms.findOne({ realm })).identityProviders!, + (providers) => { + setProviders(providers); + }, + errorHandler + ), + [] + ); + + const loader = () => Promise.resolve(_.sortBy(providers, "alias")); + + const DetailLink = (identityProvider: IdentityProviderRepresentation) => ( + <> + + {identityProvider.alias} + {!identityProvider.enabled && ( + + {t("common:disabled")} + + )} + + + ); + + const navigateToCreate = (providerId: string) => + history.push(`${url}/${providerId}`); + + const identityProviderOptions = () => + Object.keys(identityProviders).map((group) => ( + + {_.sortBy(identityProviders[group], "name").map((provider) => ( + navigateToCreate(provider.id)} + > + {provider.name} + + ))} + + )); + + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ + titleKey: "identity-providers:deleteProvider", + messageKey: t("deleteConfirm", { provider: selectedProvider?.alias }), + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + await adminClient.identityProviders.del({ + alias: selectedProvider!.alias!, + }); + setProviders([ + ...providers.filter((p) => p.alias !== selectedProvider?.alias), + ]); + refresh(); + addAlert(t("deletedSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("deleteError", { error }), AlertVariant.danger); + } + }, + }); + + return ( + <> + + {manageDisplayDialog && ( + setManageDisplayDialog(false)} + providers={providers!} + /> + )} + + + {providers.length === 0 && ( + <> + + {t("getStarted")} + + {Object.keys(identityProviders).map((group) => ( + + + + {group}: + + +
+ + {_.sortBy(identityProviders[group], "name").map( + (provider) => ( + navigateToCreate(provider.id)} + > + + + + + + {provider.name} + + + + ) + )} + +
+ ))} + + )} + {providers.length !== 0 && ( + + + {}} + toggle={ + setAddProviderOpen(!addProviderOpen)} + isPrimary + > + {t("addProvider")} + + } + isOpen={addProviderOpen} + dropdownItems={identityProviderOptions()} + /> + + + + + + + } + actions={[ + { + title: t("common:delete"), + onRowClick: (provider) => { + setSelectedProvider(provider); + toggleDeleteDialog(); + }, + }, + ]} + columns={[ + { + name: "alias", + displayKey: "common:name", + cellRenderer: DetailLink, + }, + { + name: "providerId", + displayKey: "identity-providers:provider", + cellFormatters: [upperCaseFormatter()], + }, + ]} + /> + )} +
+ + ); +}; diff --git a/src/identity-providers/ManageOrderDialog.tsx b/src/identity-providers/ManageOrderDialog.tsx new file mode 100644 index 0000000000..b664cba882 --- /dev/null +++ b/src/identity-providers/ManageOrderDialog.tsx @@ -0,0 +1,146 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import _ from "lodash"; +import { + AlertVariant, + Button, + ButtonVariant, + DataList, + DataListCell, + DataListControl, + DataListDragButton, + DataListItem, + DataListItemCells, + DataListItemRow, + Modal, + ModalVariant, + TextContent, + Text, +} from "@patternfly/react-core"; +import IdentityProviderRepresentation from "keycloak-admin/lib/defs/identityProviderRepresentation"; +import { useAdminClient } from "../context/auth/AdminClient"; +import { useAlerts } from "../components/alert/Alerts"; + +type ManageOderDialogProps = { + providers: IdentityProviderRepresentation[]; + onClose: () => void; +}; + +export const ManageOderDialog = ({ + providers, + onClose, +}: ManageOderDialogProps) => { + const { t } = useTranslation("identity-providers"); + const adminClient = useAdminClient(); + const { addAlert } = useAlerts(); + + const [alias, setAlias] = useState(""); + const [liveText, setLiveText] = useState(""); + const [order, setOrder] = useState( + providers.map((provider) => provider.alias!) + ); + + const onDragStart = (id: string) => { + setAlias(id); + setLiveText(t("onDragStart", { id })); + }; + + const onDragMove = () => { + setLiveText(t("onDragMove", { alias })); + }; + + const onDragCancel = () => { + setLiveText(t("onDragCancel")); + }; + + const onDragFinish = (providerOrder: string[]) => { + setLiveText(t("onDragFinish", { list: providerOrder })); + setOrder(providerOrder); + }; + + return ( + { + order.map(async (alias, index) => { + const provider = providers.find((p) => p.alias === alias)!; + provider.config!.guiOrder = index; + try { + await adminClient.identityProviders.update({ alias }, provider); + addAlert(t("orderChangeSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("orderChangeError", { error }), AlertVariant.danger); + } + }); + + onClose(); + }} + > + {t("common:save")} + , + , + ]} + > + + {t("oderDialogIntro")} + + + + {_.sortBy(providers, "config.guiOrder").map((provider) => ( + + + + + + + {provider.alias} + , + ]} + /> + + + ))} + +
+ {liveText} +
+
+ ); +}; diff --git a/src/identity-providers/ProviderIconMapper.tsx b/src/identity-providers/ProviderIconMapper.tsx new file mode 100644 index 0000000000..1d879c67f2 --- /dev/null +++ b/src/identity-providers/ProviderIconMapper.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { + CubeIcon, + FacebookSquareIcon, + GithubIcon, + GitlabIcon, + GoogleIcon, + LinkedinIcon, + OpenshiftIcon, + StackOverflowIcon, + TwitterIcon, +} from "@patternfly/react-icons"; +import { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon"; + +import { FontAwesomeIcon } from "./icons/FontAwesomeIcon"; + +type ProviderIconMapperProps = { + provider: { [index: string]: string }; +}; + +export const ProviderIconMapper = ({ provider }: ProviderIconMapperProps) => { + const defaultProps: SVGIconProps = { size: "lg" }; + switch (provider.id) { + case "github": + return ; + case "facebook": + return ; + case "gitlab": + return ; + case "google": + return ; + case "linkedin": + return ; + + case "openshift-v3": + case "openshift-v4": + return ; + case "stackoverflow": + return ; + case "twitter": + return ; + case "microsoft": + case "bitbucket": + case "instagram": + case "paypal": + return ; + default: + return ; + } +}; diff --git a/src/identity-providers/add/AddIdentityProvider.tsx b/src/identity-providers/add/AddIdentityProvider.tsx new file mode 100644 index 0000000000..bcdb9fbcc4 --- /dev/null +++ b/src/identity-providers/add/AddIdentityProvider.tsx @@ -0,0 +1,190 @@ +import React from "react"; +import { useHistory, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Controller, useForm } from "react-hook-form"; +import { + ActionGroup, + AlertVariant, + Button, + ClipboardCopy, + FormGroup, + NumberInput, + PageSection, + TextInput, + ValidatedOptions, +} from "@patternfly/react-core"; + +import IdentityProviderRepresentation from "keycloak-admin/lib/defs/identityProviderRepresentation"; +import { ViewHeader } from "../../components/view-header/ViewHeader"; +import { getBaseUrl, toUpperCase } from "../../util"; +import { FormAccess } from "../../components/form-access/FormAccess"; +import { HelpItem } from "../../components/help-enabler/HelpItem"; +import { useAdminClient } from "../../context/auth/AdminClient"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import { useAlerts } from "../../components/alert/Alerts"; + +export const AddIdentityProvider = () => { + const { t } = useTranslation("identity-providers"); + const { t: th } = useTranslation("identity-providers-help"); + const { id } = useParams<{ id: string }>(); + const { + handleSubmit, + register, + errors, + control, + formState: { isDirty }, + } = useForm(); + + const adminClient = useAdminClient(); + const { addAlert } = useAlerts(); + const history = useHistory(); + const { realm } = useRealm(); + + const callbackUrl = `${getBaseUrl(adminClient)}/realms/${realm}/broker`; + + const save = async (provider: IdentityProviderRepresentation) => { + try { + await adminClient.identityProviders.create({ + ...provider, + providerId: id, + alias: id, + }); + addAlert(t("createSuccess"), AlertVariant.success); + history.push(`/${realm}/identity-providers`); + } catch (error) { + addAlert(t("createError", { error }), AlertVariant.danger); + } + }; + + return ( + <> + + + + + } + fieldId="kc-redirect-uri" + > + {`${callbackUrl}/${id}/endpoint`} + + + } + fieldId="kc-client-id" + isRequired + validated={ + errors.config && errors.config.clientId + ? ValidatedOptions.error + : ValidatedOptions.default + } + helperTextInvalid={t("common:required")} + > + + + + } + fieldId="kc-client-secret" + isRequired + validated={ + errors.config && errors.config.clientSecret + ? ValidatedOptions.error + : ValidatedOptions.default + } + helperTextInvalid={t("common:required")} + > + + + + } + fieldId="kc-display-order" + > + ( + onChange(value - 1)} + onChange={onChange} + onPlus={() => onChange(value + 1)} + inputName="input" + inputAriaLabel={t("displayOrder")} + minusBtnAriaLabel={t("common:minus")} + plusBtnAriaLabel={t("common:plus")} + /> + )} + /> + + + + + + + + + ); +}; diff --git a/src/identity-providers/help.json b/src/identity-providers/help.json new file mode 100644 index 0000000000..1a62905f52 --- /dev/null +++ b/src/identity-providers/help.json @@ -0,0 +1,8 @@ +{ + "identity-providers-help": { + "redirectURI": "The redirect uri to use when configuring the identity provider.", + "clientId": "The client identifier registered with the identity provider.", + "clientSecret": "The client secret registered with the identity provider. This field is able to obtain its value from vault, use ${vault.ID} format.", + "displayOrder": "Number defining order of the provider in GUI (for example, on Login page)." + } +} diff --git a/src/identity-providers/icons/FontAwesomeIcon.tsx b/src/identity-providers/icons/FontAwesomeIcon.tsx new file mode 100644 index 0000000000..d0f6d45dd2 --- /dev/null +++ b/src/identity-providers/icons/FontAwesomeIcon.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import bitbucketIcon from "./bitbucket-brands.svg"; +import microsoftIcon from "./microsoft-brands.svg"; +import instagramIcon from "./instagram-brands.svg"; +import paypalIcon from "./paypal-brands.svg"; + +type FontAwesomeIconProps = { + icon: "bitbucket" | "microsoft" | "instagram" | "paypal"; +}; +export const FontAwesomeIcon = ({ icon }: FontAwesomeIconProps) => { + const styles = { style: { height: "2em", width: "2em" } }; + switch (icon) { + case "bitbucket": + return ; + case "microsoft": + return ; + case "instagram": + return ; + case "paypal": + return ; + default: + return <>; + } +}; diff --git a/src/identity-providers/icons/bitbucket-brands.svg b/src/identity-providers/icons/bitbucket-brands.svg new file mode 100644 index 0000000000..ebdb00f863 --- /dev/null +++ b/src/identity-providers/icons/bitbucket-brands.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/identity-providers/icons/instagram-brands.svg b/src/identity-providers/icons/instagram-brands.svg new file mode 100644 index 0000000000..53ab31190b --- /dev/null +++ b/src/identity-providers/icons/instagram-brands.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/identity-providers/icons/microsoft-brands.svg b/src/identity-providers/icons/microsoft-brands.svg new file mode 100644 index 0000000000..8f20650494 --- /dev/null +++ b/src/identity-providers/icons/microsoft-brands.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/identity-providers/icons/paypal-brands.svg b/src/identity-providers/icons/paypal-brands.svg new file mode 100644 index 0000000000..0d8cb966d1 --- /dev/null +++ b/src/identity-providers/icons/paypal-brands.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/identity-providers/messages.json b/src/identity-providers/messages.json new file mode 100644 index 0000000000..930dcacdae --- /dev/null +++ b/src/identity-providers/messages.json @@ -0,0 +1,30 @@ +{ + "identity-providers": { + "listExplain": "Through Identity Brokering it's easy to allow users to authenticate to Keycloak using external Identity Provider or Social Networks.", + "searchForProvider": "Search for provider", + "provider": "Provider", + "addProvider": "Add provider", + "manageDisplayOrder": "Manage display order", + "deleteProvider": "Delete provider?", + "deleteConfirm": "Are you sure you want to permanently delete the provider '{{provider}}'", + "deletedSuccess": "Provider successfully deleted", + "deleteError": "Could not delete the provider {{error}}", + "getStarted": "To get started, select a provider from the list below.", + "addIdentityProvider": "Add {{provider}} provider", + "redirectURI": "Redirect URI", + "clientId": "Client ID", + "clientSecret": "Client Secret", + "displayOrder": "Display order", + "createSuccess": "Identity provider successfully created", + "createError": "Could not create the identity provider provider {{error}}", + "oderDialogIntro": "The order that the providers are listed in the login page or the account console. You can drag the row handles to change the order.", + "manageOrderTableAria": "List of identity providers in the order listed on the login page", + "manageOrderItemAria": "Press space or enter to begin dragging, and use the arrow keys to navigate up or down. Press enter to confirm the drag, or any other key to cancel the drag operation.", + "onDragStart": "Dragging started for item {{id}}", + "onDragMove": "Dragging item {{id}}", + "onDragCancel": "Dragging cancelled. List is unchanged.", + "onDragFinish": "Dragging finished {{list}}", + "orderChangeSuccess": "Successfully changed display order of identity providers", + "orderChangeError": "Could not change display order of identity providers {{error}}" + } +} diff --git a/src/realm-roles/RolesList.tsx b/src/realm-roles/RolesList.tsx index 44ddd45e25..8c047aef84 100644 --- a/src/realm-roles/RolesList.tsx +++ b/src/realm-roles/RolesList.tsx @@ -9,7 +9,7 @@ import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { useAlerts } from "../components/alert/Alerts"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; -import { emptyFormatter, boolFormatter } from "../util"; +import { emptyFormatter, upperCaseFormatter } from "../util"; type RolesListProps = { paginated?: boolean; @@ -111,7 +111,7 @@ export const RolesList = ({ { name: "composite", displayKey: "roles:composite", - cellFormatters: [boolFormatter(), emptyFormatter()], + cellFormatters: [upperCaseFormatter(), emptyFormatter()], }, { name: "description", diff --git a/src/realm-roles/UsersInRoleTab.tsx b/src/realm-roles/UsersInRoleTab.tsx index 16d9686fea..32a249ebe0 100644 --- a/src/realm-roles/UsersInRoleTab.tsx +++ b/src/realm-roles/UsersInRoleTab.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import { Button, PageSection, Popover } from "@patternfly/react-core"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; -import { boolFormatter, emptyFormatter } from "../util"; +import { upperCaseFormatter, emptyFormatter } from "../util"; import { useAdminClient } from "../context/auth/AdminClient"; import { QuestionCircleIcon } from "@patternfly/react-icons"; import { useRealm } from "../context/realm-context/RealmContext"; @@ -124,7 +124,7 @@ export const UsersInRoleTab = () => { { name: "firstName", displayKey: "roles:firstName", - cellFormatters: [boolFormatter(), emptyFormatter()], + cellFormatters: [upperCaseFormatter(), emptyFormatter()], }, ]} /> diff --git a/src/realm-settings/RealmSettingsSection.tsx b/src/realm-settings/RealmSettingsSection.tsx index 48dc386278..4addbd4036 100644 --- a/src/realm-settings/RealmSettingsSection.tsx +++ b/src/realm-settings/RealmSettingsSection.tsx @@ -82,7 +82,6 @@ const RealmSettingsHeader = ({ /> [ breadcrumb: t("identityProviders"), access: "view-identity-providers", }, + { + path: "/:realm/identity-providers/:id", + component: AddIdentityProvider, + breadcrumb: t("identity-providers:provider"), + access: "manage-identity-providers", + }, { path: "/:realm/user-federation", component: UserFederationSection, diff --git a/src/user-federation/UserFederationKerberosSettings.tsx b/src/user-federation/UserFederationKerberosSettings.tsx index 37d4a00d27..0bce4a7589 100644 --- a/src/user-federation/UserFederationKerberosSettings.tsx +++ b/src/user-federation/UserFederationKerberosSettings.tsx @@ -51,11 +51,10 @@ const KerberosSettingsHeader = ({ <> {id === "new" ? ( - + ) : ( {!id ? ( - + ) : ( {t("syncChangedUsers")} diff --git a/src/user-federation/ldap/mappers/LdapMapperDetails.tsx b/src/user-federation/ldap/mappers/LdapMapperDetails.tsx index 40de80a8ee..63a0ae54a5 100644 --- a/src/user-federation/ldap/mappers/LdapMapperDetails.tsx +++ b/src/user-federation/ldap/mappers/LdapMapperDetails.tsx @@ -124,7 +124,6 @@ export const LdapMapperDetails = () => { <> diff --git a/src/user/UsersSection.tsx b/src/user/UsersSection.tsx index 95343ee0b8..e9d315c9ef 100644 --- a/src/user/UsersSection.tsx +++ b/src/user/UsersSection.tsx @@ -175,7 +175,7 @@ export const UsersSection = () => { return ( <> - + { return ( <> - + {id && ( diff --git a/src/util.ts b/src/util.ts index 763a4558d8..1eeff112a7 100644 --- a/src/util.ts +++ b/src/util.ts @@ -78,12 +78,12 @@ export const emptyFormatter = (): IFormatter => ( return data ? data : "—"; }; -export const boolFormatter = (): IFormatter => (data?: IFormatterValueType) => { - const boolVal = data?.toString(); +export const upperCaseFormatter = (): IFormatter => ( + data?: IFormatterValueType +) => { + const value = data?.toString(); - return (boolVal - ? boolVal.charAt(0).toUpperCase() + boolVal.slice(1) - : undefined) as string; + return (value ? toUpperCase(value) : undefined) as string; }; export const getBaseUrl = (adminClient: KeycloakAdminClient) => {