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
This commit is contained in:
parent
f3511d0be1
commit
6dd314c768
30 changed files with 941 additions and 29 deletions
91
cypress/integration/identity_providers.spec.ts
Normal file
91
cypress/integration/identity_providers.spec.ts
Normal file
|
@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -185,7 +185,7 @@ export const AuthenticationSection = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ViewHeader titleKey="authentication:title" subKey="" divider={false} />
|
<ViewHeader titleKey="authentication:title" divider={false} />
|
||||||
<PageSection variant="light" className="pf-u-p-0">
|
<PageSection variant="light" className="pf-u-p-0">
|
||||||
<KeycloakTabs isBox>
|
<KeycloakTabs isBox>
|
||||||
<Tab
|
<Tab
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
import { boolFormatter, emptyFormatter } from "../util";
|
import { upperCaseFormatter, emptyFormatter } from "../util";
|
||||||
import {
|
import {
|
||||||
CellDropdown,
|
CellDropdown,
|
||||||
ClientScope,
|
ClientScope,
|
||||||
|
@ -268,7 +268,7 @@ export const ClientScopesSection = () => {
|
||||||
{
|
{
|
||||||
name: "protocol",
|
name: "protocol",
|
||||||
displayKey: "client-scopes:protocol",
|
displayKey: "client-scopes:protocol",
|
||||||
cellFormatters: [boolFormatter()],
|
cellFormatters: [upperCaseFormatter()],
|
||||||
transforms: [cellWidth(15)],
|
transforms: [cellWidth(15)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -75,7 +75,7 @@ export const ClientsSection = () => {
|
||||||
{client.clientId}
|
{client.clientId}
|
||||||
{!client.enabled && (
|
{!client.enabled && (
|
||||||
<Badge key={`${client.id}-disabled`} isRead className="pf-u-ml-sm">
|
<Badge key={`${client.id}-disabled`} isRead className="pf-u-ml-sm">
|
||||||
Disabled
|
{t("common:disabled")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -54,6 +54,8 @@
|
||||||
"priority": "Priority",
|
"priority": "Priority",
|
||||||
"unexpectedError": "An unexpected error occurred: '{{error}}'",
|
"unexpectedError": "An unexpected error occurred: '{{error}}'",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
|
"plus": "Plus",
|
||||||
|
"minus": "Minus",
|
||||||
|
|
||||||
"clientScope": {
|
"clientScope": {
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
|
|
|
@ -27,7 +27,7 @@ export type ViewHeaderProps = {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
badgeId?: string;
|
badgeId?: string;
|
||||||
badgeIsRead?: boolean;
|
badgeIsRead?: boolean;
|
||||||
subKey: string | ReactNode;
|
subKey?: string | ReactNode;
|
||||||
actionsDropdownId?: string;
|
actionsDropdownId?: string;
|
||||||
subKeyLinkProps?: FormattedLinkProps;
|
subKeyLinkProps?: FormattedLinkProps;
|
||||||
dropdownItems?: ReactElement[];
|
dropdownItems?: ReactElement[];
|
||||||
|
@ -133,7 +133,11 @@ export const ViewHeader = ({
|
||||||
{enabled && (
|
{enabled && (
|
||||||
<TextContent id="view-header-subkey">
|
<TextContent id="view-header-subkey">
|
||||||
<Text>
|
<Text>
|
||||||
{React.isValidElement(subKey) ? subKey : t(subKey as string)}
|
{React.isValidElement(subKey)
|
||||||
|
? subKey
|
||||||
|
: subKey
|
||||||
|
? t(subKey as string)
|
||||||
|
: ""}
|
||||||
{subKeyLinkProps && (
|
{subKeyLinkProps && (
|
||||||
<FormattedLink
|
<FormattedLink
|
||||||
{...subKeyLinkProps}
|
{...subKeyLinkProps}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import authentication from "./authentication/messages.json";
|
||||||
import storybook from "./stories/messages.json";
|
import storybook from "./stories/messages.json";
|
||||||
import userFederation from "./user-federation/messages.json";
|
import userFederation from "./user-federation/messages.json";
|
||||||
import userFederationHelp from "./user-federation/help.json";
|
import userFederationHelp from "./user-federation/help.json";
|
||||||
|
import identityProviders from "./identity-providers/messages.json";
|
||||||
|
import identityProvidersHelp from "./identity-providers/help.json";
|
||||||
|
|
||||||
const initOptions = {
|
const initOptions = {
|
||||||
defaultNS: "common",
|
defaultNS: "common",
|
||||||
|
@ -44,9 +46,11 @@ const initOptions = {
|
||||||
...realmSettings,
|
...realmSettings,
|
||||||
...realmSettingsHelp,
|
...realmSettingsHelp,
|
||||||
...authentication,
|
...authentication,
|
||||||
...storybook,
|
...identityProviders,
|
||||||
|
...identityProvidersHelp,
|
||||||
...userFederation,
|
...userFederation,
|
||||||
...userFederationHelp,
|
...userFederationHelp,
|
||||||
|
...storybook,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
lng: "en",
|
lng: "en",
|
||||||
|
|
|
@ -1,6 +1,257 @@
|
||||||
import React from "react";
|
import React, { Fragment, useEffect, useState } from "react";
|
||||||
import { WorkInProgress } from "../components/work-in-progress/WorkInProgress";
|
import { Link, useHistory, useRouteMatch } from "react-router-dom";
|
||||||
|
import { useErrorHandler } from "react-error-boundary";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import _ from "lodash";
|
||||||
|
import {
|
||||||
|
AlertVariant,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
ButtonVariant,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
Dropdown,
|
||||||
|
DropdownGroup,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownToggle,
|
||||||
|
Gallery,
|
||||||
|
PageSection,
|
||||||
|
Split,
|
||||||
|
SplitItem,
|
||||||
|
Text,
|
||||||
|
TextContent,
|
||||||
|
TextVariants,
|
||||||
|
ToolbarItem,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
export const IdentityProvidersSection = () => (
|
import IdentityProviderRepresentation from "keycloak-admin/lib/defs/identityProviderRepresentation";
|
||||||
<WorkInProgress marvelLink="https://marvelapp.com/prototype/55c2d8f/screen/75697040" />
|
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<IdentityProviderRepresentation[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
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) => (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
key={identityProvider.providerId}
|
||||||
|
to={`/${realm}/identity-providers/${identityProvider.providerId}/settings`}
|
||||||
|
>
|
||||||
|
{identityProvider.alias}
|
||||||
|
{!identityProvider.enabled && (
|
||||||
|
<Badge
|
||||||
|
key={`${identityProvider.providerId}-disabled`}
|
||||||
|
isRead
|
||||||
|
className="pf-u-ml-sm"
|
||||||
|
>
|
||||||
|
{t("common:disabled")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const navigateToCreate = (providerId: string) =>
|
||||||
|
history.push(`${url}/${providerId}`);
|
||||||
|
|
||||||
|
const identityProviderOptions = () =>
|
||||||
|
Object.keys(identityProviders).map((group) => (
|
||||||
|
<DropdownGroup key={group} label={group}>
|
||||||
|
{_.sortBy(identityProviders[group], "name").map((provider) => (
|
||||||
|
<DropdownItem
|
||||||
|
key={provider.id}
|
||||||
|
value={provider.id}
|
||||||
|
data-testid={provider.id}
|
||||||
|
onClick={() => navigateToCreate(provider.id)}
|
||||||
|
>
|
||||||
|
{provider.name}
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownGroup>
|
||||||
|
));
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<DeleteConfirm />
|
||||||
|
{manageDisplayDialog && (
|
||||||
|
<ManageOderDialog
|
||||||
|
onClose={() => setManageDisplayDialog(false)}
|
||||||
|
providers={providers!}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ViewHeader
|
||||||
|
titleKey="common:identityProviders"
|
||||||
|
subKey="identity-providers:listExplain"
|
||||||
|
divider={false}
|
||||||
|
/>
|
||||||
|
<PageSection
|
||||||
|
variant={providers.length === 0 ? "default" : "light"}
|
||||||
|
className={providers.length === 0 ? "" : "pf-u-p-0"}
|
||||||
|
>
|
||||||
|
{providers.length === 0 && (
|
||||||
|
<>
|
||||||
|
<TextContent>
|
||||||
|
<Text component={TextVariants.p}>{t("getStarted")}</Text>
|
||||||
|
</TextContent>
|
||||||
|
{Object.keys(identityProviders).map((group) => (
|
||||||
|
<Fragment key={group}>
|
||||||
|
<TextContent>
|
||||||
|
<Text className="pf-u-mt-lg" component={TextVariants.h2}>
|
||||||
|
{group}:
|
||||||
|
</Text>
|
||||||
|
</TextContent>
|
||||||
|
<hr className="pf-u-mb-lg" />
|
||||||
|
<Gallery hasGutter>
|
||||||
|
{_.sortBy(identityProviders[group], "name").map(
|
||||||
|
(provider) => (
|
||||||
|
<Card
|
||||||
|
key={provider.id}
|
||||||
|
isHoverable
|
||||||
|
data-testid={`${provider.id}-card`}
|
||||||
|
onClick={() => navigateToCreate(provider.id)}
|
||||||
|
>
|
||||||
|
<CardTitle>
|
||||||
|
<Split hasGutter>
|
||||||
|
<SplitItem>
|
||||||
|
<ProviderIconMapper provider={provider} />
|
||||||
|
</SplitItem>
|
||||||
|
<SplitItem isFilled>{provider.name}</SplitItem>
|
||||||
|
</Split>
|
||||||
|
</CardTitle>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Gallery>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{providers.length !== 0 && (
|
||||||
|
<KeycloakDataTable
|
||||||
|
key={key}
|
||||||
|
loader={loader}
|
||||||
|
ariaLabelKey="common:identityProviders"
|
||||||
|
searchPlaceholderKey="identity-providers:searchForProvider"
|
||||||
|
toolbarItem={
|
||||||
|
<>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Dropdown
|
||||||
|
data-testid="addProviderDropdown"
|
||||||
|
onSelect={() => {}}
|
||||||
|
toggle={
|
||||||
|
<DropdownToggle
|
||||||
|
onToggle={() => setAddProviderOpen(!addProviderOpen)}
|
||||||
|
isPrimary
|
||||||
|
>
|
||||||
|
{t("addProvider")}
|
||||||
|
</DropdownToggle>
|
||||||
|
}
|
||||||
|
isOpen={addProviderOpen}
|
||||||
|
dropdownItems={identityProviderOptions()}
|
||||||
|
/>
|
||||||
|
</ToolbarItem>
|
||||||
|
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
data-testid="manageDisplayOrder"
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setManageDisplayDialog(true)}
|
||||||
|
>
|
||||||
|
{t("manageDisplayOrder")}
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
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()],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
146
src/identity-providers/ManageOrderDialog.tsx
Normal file
146
src/identity-providers/ManageOrderDialog.tsx
Normal file
|
@ -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 (
|
||||||
|
<Modal
|
||||||
|
variant={ModalVariant.small}
|
||||||
|
title={t("manageDisplayOrder")}
|
||||||
|
isOpen={true}
|
||||||
|
onClose={onClose}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
id="modal-confirm"
|
||||||
|
key="confirm"
|
||||||
|
onClick={() => {
|
||||||
|
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")}
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
id="modal-cancel"
|
||||||
|
key="cancel"
|
||||||
|
variant={ButtonVariant.link}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t("common:cancel")}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TextContent className="pf-u-pb-lg">
|
||||||
|
<Text>{t("oderDialogIntro")}</Text>
|
||||||
|
</TextContent>
|
||||||
|
|
||||||
|
<DataList
|
||||||
|
aria-label={t("manageOrderTableAria")}
|
||||||
|
data-testid="manageOrderDataList"
|
||||||
|
isCompact
|
||||||
|
onDragFinish={onDragFinish}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragMove={onDragMove}
|
||||||
|
onDragCancel={onDragCancel}
|
||||||
|
itemOrder={order}
|
||||||
|
>
|
||||||
|
{_.sortBy(providers, "config.guiOrder").map((provider) => (
|
||||||
|
<DataListItem
|
||||||
|
aria-labelledby={provider.alias}
|
||||||
|
id={provider.alias}
|
||||||
|
key={provider.alias}
|
||||||
|
>
|
||||||
|
<DataListItemRow>
|
||||||
|
<DataListControl>
|
||||||
|
<DataListDragButton
|
||||||
|
aria-label="Reorder"
|
||||||
|
aria-labelledby={provider.alias}
|
||||||
|
aria-describedby={t("manageOrderItemAria")}
|
||||||
|
aria-pressed="false"
|
||||||
|
/>
|
||||||
|
</DataListControl>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell
|
||||||
|
key={`${provider.alias}-cell`}
|
||||||
|
data-testid={provider.alias}
|
||||||
|
>
|
||||||
|
<span id={provider.alias}>{provider.alias}</span>
|
||||||
|
</DataListCell>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
))}
|
||||||
|
</DataList>
|
||||||
|
<div className="pf-screen-reader" aria-live="assertive">
|
||||||
|
{liveText}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
50
src/identity-providers/ProviderIconMapper.tsx
Normal file
50
src/identity-providers/ProviderIconMapper.tsx
Normal file
|
@ -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 <GithubIcon {...defaultProps} />;
|
||||||
|
case "facebook":
|
||||||
|
return <FacebookSquareIcon {...defaultProps} />;
|
||||||
|
case "gitlab":
|
||||||
|
return <GitlabIcon {...defaultProps} />;
|
||||||
|
case "google":
|
||||||
|
return <GoogleIcon {...defaultProps} />;
|
||||||
|
case "linkedin":
|
||||||
|
return <LinkedinIcon {...defaultProps} />;
|
||||||
|
|
||||||
|
case "openshift-v3":
|
||||||
|
case "openshift-v4":
|
||||||
|
return <OpenshiftIcon {...defaultProps} />;
|
||||||
|
case "stackoverflow":
|
||||||
|
return <StackOverflowIcon {...defaultProps} />;
|
||||||
|
case "twitter":
|
||||||
|
return <TwitterIcon {...defaultProps} />;
|
||||||
|
case "microsoft":
|
||||||
|
case "bitbucket":
|
||||||
|
case "instagram":
|
||||||
|
case "paypal":
|
||||||
|
return <FontAwesomeIcon icon={provider.id} />;
|
||||||
|
default:
|
||||||
|
return <CubeIcon {...defaultProps} />;
|
||||||
|
}
|
||||||
|
};
|
190
src/identity-providers/add/AddIdentityProvider.tsx
Normal file
190
src/identity-providers/add/AddIdentityProvider.tsx
Normal file
|
@ -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<IdentityProviderRepresentation>();
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<ViewHeader
|
||||||
|
titleKey={t("addIdentityProvider", { provider: toUpperCase(id) })}
|
||||||
|
/>
|
||||||
|
<PageSection variant="light">
|
||||||
|
<FormAccess
|
||||||
|
role="manage-identity-providers"
|
||||||
|
isHorizontal
|
||||||
|
onSubmit={handleSubmit(save)}
|
||||||
|
>
|
||||||
|
<FormGroup
|
||||||
|
label={t("redirectURI")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText={th("redirectURI")}
|
||||||
|
forLabel={t("redirectURI")}
|
||||||
|
forID="kc-redirect-uri"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="kc-redirect-uri"
|
||||||
|
>
|
||||||
|
<ClipboardCopy
|
||||||
|
isReadOnly
|
||||||
|
>{`${callbackUrl}/${id}/endpoint`}</ClipboardCopy>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("clientId")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText={th("clientId")}
|
||||||
|
forLabel={t("clientId")}
|
||||||
|
forID="kc-client-id"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="kc-client-id"
|
||||||
|
isRequired
|
||||||
|
validated={
|
||||||
|
errors.config && errors.config.clientId
|
||||||
|
? ValidatedOptions.error
|
||||||
|
: ValidatedOptions.default
|
||||||
|
}
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
isRequired
|
||||||
|
type="text"
|
||||||
|
id="kc-client-id"
|
||||||
|
data-testid="clientId"
|
||||||
|
name="config.clientId"
|
||||||
|
ref={register({ required: true })}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("clientSecret")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText={th("clientSecret")}
|
||||||
|
forLabel={t("clientSecret")}
|
||||||
|
forID="kc-client-secret"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="kc-client-secret"
|
||||||
|
isRequired
|
||||||
|
validated={
|
||||||
|
errors.config && errors.config.clientSecret
|
||||||
|
? ValidatedOptions.error
|
||||||
|
: ValidatedOptions.default
|
||||||
|
}
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
isRequired
|
||||||
|
type="password"
|
||||||
|
id="kc-client-secret"
|
||||||
|
data-testid="clientSecret"
|
||||||
|
name="config.clientSecret"
|
||||||
|
ref={register({ required: true })}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("displayOrder")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText={th("displayOrder")}
|
||||||
|
forLabel={t("displayOrder")}
|
||||||
|
forID="kc-display-order"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="kc-display-order"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="config.guiOrder"
|
||||||
|
control={control}
|
||||||
|
defaultValue={0}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<NumberInput
|
||||||
|
value={value}
|
||||||
|
data-testid="displayOrder"
|
||||||
|
onMinus={() => onChange(value - 1)}
|
||||||
|
onChange={onChange}
|
||||||
|
onPlus={() => onChange(value + 1)}
|
||||||
|
inputName="input"
|
||||||
|
inputAriaLabel={t("displayOrder")}
|
||||||
|
minusBtnAriaLabel={t("common:minus")}
|
||||||
|
plusBtnAriaLabel={t("common:plus")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<ActionGroup>
|
||||||
|
<Button
|
||||||
|
isDisabled={!isDirty}
|
||||||
|
variant="primary"
|
||||||
|
type="submit"
|
||||||
|
data-testid="createProvider"
|
||||||
|
>
|
||||||
|
{t("common:add")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
data-testid="cancel"
|
||||||
|
onClick={() => history.push(`/${realm}/identity-providers`)}
|
||||||
|
>
|
||||||
|
{t("common:cancel")}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</FormAccess>
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
8
src/identity-providers/help.json
Normal file
8
src/identity-providers/help.json
Normal file
|
@ -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)."
|
||||||
|
}
|
||||||
|
}
|
24
src/identity-providers/icons/FontAwesomeIcon.tsx
Normal file
24
src/identity-providers/icons/FontAwesomeIcon.tsx
Normal file
|
@ -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 <img src={bitbucketIcon} {...styles} />;
|
||||||
|
case "microsoft":
|
||||||
|
return <img src={microsoftIcon} {...styles} />;
|
||||||
|
case "instagram":
|
||||||
|
return <img src={instagramIcon} {...styles} />;
|
||||||
|
case "paypal":
|
||||||
|
return <img src={paypalIcon} {...styles} />;
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
1
src/identity-providers/icons/bitbucket-brands.svg
Normal file
1
src/identity-providers/icons/bitbucket-brands.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="bitbucket" class="svg-inline--fa fa-bitbucket fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M22.2 32A16 16 0 0 0 6 47.8a26.35 26.35 0 0 0 .2 2.8l67.9 412.1a21.77 21.77 0 0 0 21.3 18.2h325.7a16 16 0 0 0 16-13.4L505 50.7a16 16 0 0 0-13.2-18.3 24.58 24.58 0 0 0-2.8-.2L22.2 32zm285.9 297.8h-104l-28.1-147h157.3l-25.2 147z"></path></svg>
|
After Width: | Height: | Size: 464 B |
1
src/identity-providers/icons/instagram-brands.svg
Normal file
1
src/identity-providers/icons/instagram-brands.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="instagram" class="svg-inline--fa fa-instagram fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
src/identity-providers/icons/microsoft-brands.svg
Normal file
1
src/identity-providers/icons/microsoft-brands.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="windows" class="svg-inline--fa fa-windows fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"></path></svg>
|
After Width: | Height: | Size: 369 B |
1
src/identity-providers/icons/paypal-brands.svg
Normal file
1
src/identity-providers/icons/paypal-brands.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="paypal" class="svg-inline--fa fa-paypal fa-w-12" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M111.4 295.9c-3.5 19.2-17.4 108.7-21.5 134-.3 1.8-1 2.5-3 2.5H12.3c-7.6 0-13.1-6.6-12.1-13.9L58.8 46.6c1.5-9.6 10.1-16.9 20-16.9 152.3 0 165.1-3.7 204 11.4 60.1 23.3 65.6 79.5 44 140.3-21.5 62.6-72.5 89.5-140.1 90.3-43.4.7-69.5-7-75.3 24.2zM357.1 152c-1.8-1.3-2.5-1.8-3 1.3-2 11.4-5.1 22.5-8.8 33.6-39.9 113.8-150.5 103.9-204.5 103.9-6.1 0-10.1 3.3-10.9 9.4-22.6 140.4-27.1 169.7-27.1 169.7-1 7.1 3.5 12.9 10.6 12.9h63.5c8.6 0 15.7-6.3 17.4-14.9.7-5.4-1.1 6.1 14.4-91.3 4.6-22 14.3-19.7 29.3-19.7 71 0 126.4-28.8 142.9-112.3 6.5-34.8 4.6-71.4-23.8-92.6z"></path></svg>
|
After Width: | Height: | Size: 785 B |
30
src/identity-providers/messages.json
Normal file
30
src/identity-providers/messages.json
Normal file
|
@ -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}}"
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
import { emptyFormatter, boolFormatter } from "../util";
|
import { emptyFormatter, upperCaseFormatter } from "../util";
|
||||||
|
|
||||||
type RolesListProps = {
|
type RolesListProps = {
|
||||||
paginated?: boolean;
|
paginated?: boolean;
|
||||||
|
@ -111,7 +111,7 @@ export const RolesList = ({
|
||||||
{
|
{
|
||||||
name: "composite",
|
name: "composite",
|
||||||
displayKey: "roles:composite",
|
displayKey: "roles:composite",
|
||||||
cellFormatters: [boolFormatter(), emptyFormatter()],
|
cellFormatters: [upperCaseFormatter(), emptyFormatter()],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "description",
|
name: "description",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||||
import { Button, PageSection, Popover } from "@patternfly/react-core";
|
import { Button, PageSection, Popover } from "@patternfly/react-core";
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||||
import { boolFormatter, emptyFormatter } from "../util";
|
import { upperCaseFormatter, emptyFormatter } from "../util";
|
||||||
import { useAdminClient } from "../context/auth/AdminClient";
|
import { useAdminClient } from "../context/auth/AdminClient";
|
||||||
import { QuestionCircleIcon } from "@patternfly/react-icons";
|
import { QuestionCircleIcon } from "@patternfly/react-icons";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
|
@ -124,7 +124,7 @@ export const UsersInRoleTab = () => {
|
||||||
{
|
{
|
||||||
name: "firstName",
|
name: "firstName",
|
||||||
displayKey: "roles:firstName",
|
displayKey: "roles:firstName",
|
||||||
cellFormatters: [boolFormatter(), emptyFormatter()],
|
cellFormatters: [upperCaseFormatter(), emptyFormatter()],
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -82,7 +82,6 @@ const RealmSettingsHeader = ({
|
||||||
/>
|
/>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
titleKey={toUpperCase(realmName)}
|
titleKey={toUpperCase(realmName)}
|
||||||
subKey=""
|
|
||||||
divider={false}
|
divider={false}
|
||||||
dropdownItems={[
|
dropdownItems={[
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { SearchGroups } from "./groups/SearchGroups";
|
||||||
import { CreateInitialAccessToken } from "./clients/initial-access/CreateInitialAccessToken";
|
import { CreateInitialAccessToken } from "./clients/initial-access/CreateInitialAccessToken";
|
||||||
import { RealmSettingsTabs } from "./realm-settings/RealmSettingsTabs";
|
import { RealmSettingsTabs } from "./realm-settings/RealmSettingsTabs";
|
||||||
import { LdapMapperDetails } from "./user-federation/ldap/mappers/LdapMapperDetails";
|
import { LdapMapperDetails } from "./user-federation/ldap/mappers/LdapMapperDetails";
|
||||||
|
import { AddIdentityProvider } from "./identity-providers/add/AddIdentityProvider";
|
||||||
|
|
||||||
export type RouteDef = BreadcrumbsRoute & {
|
export type RouteDef = BreadcrumbsRoute & {
|
||||||
access: AccessType;
|
access: AccessType;
|
||||||
|
@ -196,6 +197,12 @@ export const routes: RoutesFn = (t: TFunction) => [
|
||||||
breadcrumb: t("identityProviders"),
|
breadcrumb: t("identityProviders"),
|
||||||
access: "view-identity-providers",
|
access: "view-identity-providers",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/:realm/identity-providers/:id",
|
||||||
|
component: AddIdentityProvider,
|
||||||
|
breadcrumb: t("identity-providers:provider"),
|
||||||
|
access: "manage-identity-providers",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:realm/user-federation",
|
path: "/:realm/user-federation",
|
||||||
component: UserFederationSection,
|
component: UserFederationSection,
|
||||||
|
|
|
@ -51,11 +51,10 @@ const KerberosSettingsHeader = ({
|
||||||
<>
|
<>
|
||||||
<DisableConfirm />
|
<DisableConfirm />
|
||||||
{id === "new" ? (
|
{id === "new" ? (
|
||||||
<ViewHeader titleKey="Kerberos" subKey="" />
|
<ViewHeader titleKey="Kerberos" />
|
||||||
) : (
|
) : (
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
titleKey="Kerberos"
|
titleKey="Kerberos"
|
||||||
subKey=""
|
|
||||||
dropdownItems={[
|
dropdownItems={[
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="delete"
|
key="delete"
|
||||||
|
|
|
@ -124,11 +124,10 @@ const LdapSettingsHeader = ({
|
||||||
<>
|
<>
|
||||||
<DisableConfirm />
|
<DisableConfirm />
|
||||||
{!id ? (
|
{!id ? (
|
||||||
<ViewHeader titleKey="LDAP" subKey="" />
|
<ViewHeader titleKey="LDAP" />
|
||||||
) : (
|
) : (
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
titleKey="LDAP"
|
titleKey="LDAP"
|
||||||
subKey=""
|
|
||||||
dropdownItems={[
|
dropdownItems={[
|
||||||
<DropdownItem key="sync" onClick={syncChangedUsers}>
|
<DropdownItem key="sync" onClick={syncChangedUsers}>
|
||||||
{t("syncChangedUsers")}
|
{t("syncChangedUsers")}
|
||||||
|
|
|
@ -124,7 +124,6 @@ export const LdapMapperDetails = () => {
|
||||||
<>
|
<>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
titleKey={mapping ? mapping.name! : t("common:createNewMapper")}
|
titleKey={mapping ? mapping.name! : t("common:createNewMapper")}
|
||||||
subKey=""
|
|
||||||
/>
|
/>
|
||||||
<PageSection variant="light" isFilled>
|
<PageSection variant="light" isFilled>
|
||||||
<FormAccess role="manage-realm" isHorizontal>
|
<FormAccess role="manage-realm" isHorizontal>
|
||||||
|
|
|
@ -175,7 +175,7 @@ export const UsersSection = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
<ViewHeader titleKey="users:title" subKey="" />
|
<ViewHeader titleKey="users:title" />
|
||||||
<PageSection
|
<PageSection
|
||||||
data-testid="users-page"
|
data-testid="users-page"
|
||||||
variant="light"
|
variant="light"
|
||||||
|
|
|
@ -61,7 +61,7 @@ export const UsersTabs = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeader titleKey={user! || t("users:createUser")} subKey="" />
|
<ViewHeader titleKey={user! || t("users:createUser")} />
|
||||||
<PageSection variant="light">
|
<PageSection variant="light">
|
||||||
{id && (
|
{id && (
|
||||||
<KeycloakTabs isBox>
|
<KeycloakTabs isBox>
|
||||||
|
|
10
src/util.ts
10
src/util.ts
|
@ -78,12 +78,12 @@ export const emptyFormatter = (): IFormatter => (
|
||||||
return data ? data : "—";
|
return data ? data : "—";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const boolFormatter = (): IFormatter => (data?: IFormatterValueType) => {
|
export const upperCaseFormatter = (): IFormatter => (
|
||||||
const boolVal = data?.toString();
|
data?: IFormatterValueType
|
||||||
|
) => {
|
||||||
|
const value = data?.toString();
|
||||||
|
|
||||||
return (boolVal
|
return (value ? toUpperCase(value) : undefined) as string;
|
||||||
? boolVal.charAt(0).toUpperCase() + boolVal.slice(1)
|
|
||||||
: undefined) as string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBaseUrl = (adminClient: KeycloakAdminClient) => {
|
export const getBaseUrl = (adminClient: KeycloakAdminClient) => {
|
||||||
|
|
Loading…
Reference in a new issue