Fixes with realm list (#576)

* only query realms endpoint once

currently the realm list query is expensive and we only need it once.

* fixed refresh issues

* fixed tests

* fixed imports

* fixed types

* removed strange non code

* reload instead

* added more realm tests
This commit is contained in:
Erik Jan de Wit 2021-05-03 18:44:47 +02:00 committed by GitHub
parent e1d53c640e
commit 78f843cdcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 219 additions and 802 deletions

View file

@ -4,16 +4,18 @@ import CreateRealmPage from "../support/pages/admin_console/CreateRealmPage";
import Masthead from "../support/pages/admin_console/Masthead";
import AdminClient from "../support/util/AdminClient";
import { keycloakBefore } from "../support/util/keycloak_before";
import RealmSelector from "../support/pages/admin_console/RealmSelector";
const masthead = new Masthead();
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const createRealmPage = new CreateRealmPage();
const realmSelector = new RealmSelector();
describe("Realms test", function () {
describe("Realms test", () => {
const testRealmName = "Test realm";
describe("Realm creation", function () {
beforeEach(function () {
describe("Realm creation", () => {
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
});
@ -23,7 +25,7 @@ describe("Realms test", function () {
await client.deleteRealm(testRealmName);
});
it("should fail creating Master realm", function () {
it("should fail creating Master realm", () => {
sidebarPage.goToCreateRealm();
createRealmPage.fillRealmName("master").createRealm();
@ -32,14 +34,14 @@ describe("Realms test", function () {
);
});
it("should create Test realm", function () {
it("should create Test realm", () => {
sidebarPage.goToCreateRealm();
createRealmPage.fillRealmName(testRealmName).createRealm();
masthead.checkNotificationMessage("Realm created");
});
it("should change to Test realm", function () {
it("should change to Test realm", () => {
sidebarPage.getCurrentRealm().should("eq", "Master");
sidebarPage
@ -48,4 +50,29 @@ describe("Realms test", function () {
.should("eq", testRealmName);
});
});
describe("More then 5 realms", () => {
const realmNames = ["One", "Two", "Three", "Four", "Five"];
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
for (const realmName of realmNames) {
sidebarPage.goToCreateRealm();
createRealmPage.fillRealmName(realmName).createRealm();
sidebarPage.goToClients();
}
});
afterEach(async () => {
const client = new AdminClient();
for (const realmName of realmNames) {
await client.deleteRealm(realmName);
}
});
it("switch to searchable realm selector", () => {
realmSelector.openRealmContextSelector().shouldContainAll(realmNames);
});
});
});

View file

@ -0,0 +1,27 @@
const expect = chai.expect;
export default class RealmSelector {
private realmSelector = "realmSelector";
private realmContextSelector = ".keycloak__realm_selector__context_selector";
shouldContainAll(realmsList: string[]) {
cy.getId(this.realmSelector)
.scrollIntoView()
.get("ul")
.should((realms) => {
for (let index = 0; index < realmsList.length; index++) {
const realmName = realmsList[index];
expect(realms).to.contain(realmName);
}
});
return this;
}
openRealmContextSelector() {
cy.getId(this.realmSelector).scrollIntoView();
cy.get(this.realmContextSelector).click();
return this;
}
}

View file

@ -1,64 +1,47 @@
export default class SidebarPage {
realmsDrpDwn: string;
realmsList: string;
createRealmBtn: string;
clientsBtn: string;
clientScopesBtn: string;
realmRolesBtn: string;
usersBtn: string;
groupsBtn: string;
sessionsBtn: string;
eventsBtn: string;
realmSettingsBtn: string;
authenticationBtn: string;
identityProvidersBtn: string;
userFederationBtn: string;
private realmsDrpDwn = "realmSelectorToggle";
private realmsList = "realmSelector";
private createRealmBtn = "add-realm";
constructor() {
this.realmsDrpDwn = "#realm-select-toggle";
this.realmsList = "#realm-select ul";
this.createRealmBtn = "#realm-select li:last-child a";
private clientsBtn = "#nav-item-clients";
private clientScopesBtn = "#nav-item-client-scopes";
private realmRolesBtn = "#nav-item-roles";
private usersBtn = "#nav-item-users";
private groupsBtn = "#nav-item-groups";
private sessionsBtn = "#nav-item-sessions";
private eventsBtn = "#nav-item-events";
this.clientsBtn = "#nav-item-clients";
this.clientScopesBtn = "#nav-item-client-scopes";
this.realmRolesBtn = "#nav-item-roles";
this.usersBtn = "#nav-item-users";
this.groupsBtn = "#nav-item-groups";
this.sessionsBtn = "#nav-item-sessions";
this.eventsBtn = "#nav-item-events";
this.realmSettingsBtn = "#nav-item-realm-settings";
this.authenticationBtn = "#nav-item-authentication";
this.identityProvidersBtn = "#nav-item-identity-providers";
this.userFederationBtn = "#nav-item-user-federation";
}
private realmSettingsBtn = "#nav-item-realm-settings";
private authenticationBtn = "#nav-item-authentication";
private identityProvidersBtn = "#nav-item-identity-providers";
private userFederationBtn = "#nav-item-user-federation";
getCurrentRealm() {
return cy.get(this.realmsDrpDwn).invoke("text");
return cy.getId(this.realmsDrpDwn).scrollIntoView().invoke("text");
}
goToRealm(realmName: string) {
cy.get(this.realmsDrpDwn).click();
cy.get(this.realmsList).contains(realmName).click();
cy.getId(this.realmsDrpDwn).scrollIntoView().click();
cy.getId(this.realmsList).get("ul").contains(realmName).click();
return this;
}
goToCreateRealm() {
cy.get(this.realmsDrpDwn).click();
cy.get(this.createRealmBtn).click();
cy.getId(this.realmsDrpDwn).scrollIntoView().click();
cy.getId(this.createRealmBtn).click();
return this;
}
goToClients() {
cy.get(this.clientsBtn).click();
cy.get(this.clientsBtn).scrollIntoView().click();
return this;
}
goToClientScopes() {
cy.get(this.clientScopesBtn).click();
cy.get(this.clientScopesBtn).scrollIntoView().click();
return this;
}

View file

@ -6,7 +6,7 @@ import {
Switch,
useParams,
} from "react-router-dom";
import { ErrorBoundary, useErrorHandler } from "react-error-boundary";
import { ErrorBoundary } from "react-error-boundary";
import { Header } from "./PageHeader";
import { PageNav } from "./PageNav";
@ -21,9 +21,7 @@ import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs";
import { ForbiddenSection } from "./ForbiddenSection";
import { SubGroups } from "./groups/SubGroupsContext";
import { useRealm } from "./context/realm-context/RealmContext";
import { useAdminClient, asyncStateFetch } from "./context/auth/AdminClient";
import { ErrorRenderer } from "./components/error/ErrorRenderer";
import { RecentUsed } from "./components/realm-selector/recent-used";
export const mainPageContentId = "kc-main-content-page-container";
@ -39,28 +37,11 @@ const AppContexts = ({ children }: { children: ReactNode }) => (
</AccessContextProvider>
);
// set the realm form the path if it's one of the know realms
// set the realm form the path
const RealmPathSelector = ({ children }: { children: ReactNode }) => {
const { setRealm } = useRealm();
const { realm } = useParams<{ realm: string }>();
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const recentUsed = new RecentUsed();
useEffect(
() =>
asyncStateFetch(
() => adminClient.realms.find(),
(realms) => {
recentUsed.clean(realms.map((r) => r.realm!));
if (realms.findIndex((r) => r.realm == realm) !== -1) {
setRealm(realm);
}
},
handleError
),
[]
);
useEffect(() => setRealm(realm), []);
return <>{children}</>;
};
@ -88,7 +69,7 @@ export const App = () => {
>
<ErrorBoundary
FallbackComponent={ErrorRenderer}
onReset={() => (location.href = "/")}
onReset={window.location.reload}
>
<Switch>
{routes(() => {}).map((route, i) => (

View file

@ -14,12 +14,12 @@ export const KeycloakAdminConsole = ({
adminClient,
}: KeycloakAdminConsoleProps) => {
return (
<RealmContextProvider>
<AdminClient.Provider value={adminClient}>
<WhoAmIContextProvider>
<RealmContextProvider>
<App />
</RealmContextProvider>
</WhoAmIContextProvider>
</AdminClient.Provider>
</RealmContextProvider>
);
};

View file

@ -18,7 +18,6 @@ import { WhoAmIContext } from "./context/whoami/WhoAmI";
import { HelpContext, HelpHeader } from "./components/help-enabler/HelpHeader";
import { Link, useHistory } from "react-router-dom";
import { useAdminClient } from "./context/auth/AdminClient";
import { useRealm } from "./context/realm-context/RealmContext";
export const Header = () => {
const adminClient = useAdminClient();
@ -59,13 +58,11 @@ export const Header = () => {
const ServerInfoDropdownItem = () => {
const { t } = useTranslation();
const history = useHistory();
const { setRealm } = useRealm();
return (
<DropdownItem
key="server info"
onClick={() => {
history.push("/master/");
setRealm("master");
}}
>
{t("realmInfo")}

View file

@ -1,7 +1,6 @@
import React from "react";
import { useHistory, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import _ from "lodash";
import {
Nav,
NavItem,
@ -12,8 +11,6 @@ import {
import { RealmSelector } from "./components/realm-selector/RealmSelector";
import { useRealm } from "./context/realm-context/RealmContext";
import { DataLoader } from "./components/data-loader/DataLoader";
import { useAdminClient } from "./context/auth/AdminClient";
import { useAccess } from "./context/access/Access";
import { routes } from "./route-config";
@ -21,10 +18,6 @@ export const PageNav: React.FunctionComponent = () => {
const { t } = useTranslation("common");
const { hasAccess, hasSomeAccess } = useAccess();
const { realm } = useRealm();
const adminClient = useAdminClient();
const realmLoader = async () => {
return _.sortBy(await adminClient.realms.find(), "realm");
};
const history = useHistory();
@ -83,13 +76,9 @@ export const PageNav: React.FunctionComponent = () => {
nav={
<Nav onSelect={onSelect}>
<NavList>
<DataLoader loader={realmLoader} deps={[realm]}>
{(realmList) => (
<NavItem className="keycloak__page_nav__nav_item__realm-selector">
<RealmSelector realmList={realmList || []} />
<RealmSelector />
</NavItem>
)}
</DataLoader>
</NavList>
{isOnAddRealm() && (
<NavGroup title="">

View file

@ -26,15 +26,15 @@ import {
AllClientScopes,
AllClientScopeType,
} from "../components/client-scope/ClientScopeTypes";
import KeycloakAdminClient from "keycloak-admin";
import { ChangeTypeDialog } from "./ChangeTypeDialog";
import "./client-scope.css";
type ClientScopeDefaultOptionalType = ClientScopeRepresentation & {
type: AllClientScopeType;
};
import "./client-scope.css";
import KeycloakAdminClient from "keycloak-admin";
import { ChangeTypeDialog } from "./ChangeTypeDialog";
const castAdminClient = (adminClient: KeycloakAdminClient) =>
(adminClient.clientScopes as unknown) as {
[index: string]: Function;

View file

@ -18,6 +18,8 @@ import {
} from "../../components/client-scope/ClientScopeTypes";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import "./client-scopes.css";
export type AddScopeDialogProps = {
clientScopes: ClientScopeRepresentation[];
open: boolean;
@ -27,8 +29,6 @@ export type AddScopeDialogProps = {
) => void;
};
import "./client-scopes.css";
export const AddScopeDialog = ({
clientScopes,
open,

View file

@ -32,7 +32,7 @@ exports[`Group BreadCrumbs tests couple of crumbs 1`] = `
href="//groups"
onClick={[Function]}
>
groups
Groups
</a>
</LinkAnchor>
</Link>

View file

@ -17,7 +17,14 @@ describe("<FormAccess />", () => {
<WhoAmIContext.Provider
value={{ refresh: () => {}, whoAmI: new WhoAmI("master", whoami) }}
>
<RealmContext.Provider value={{ realm, setRealm: () => {} }}>
<RealmContext.Provider
value={{
realm,
setRealm: () => {},
realms: [],
refresh: () => Promise.resolve(),
}}
>
<AccessContextProvider>
<FormAccess role="manage-clients">
<FormGroup label="test" fieldId="field">

View file

@ -16,24 +16,20 @@ import {
} from "@patternfly/react-core";
import { CheckIcon } from "@patternfly/react-icons";
import { toUpperCase } from "../../util";
import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { toUpperCase } from "../../util";
import { useRealm } from "../../context/realm-context/RealmContext";
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
import { RecentUsed } from "./recent-used";
import "./realm-selector.css";
type RealmSelectorProps = {
realmList: RealmRepresentation[];
};
export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
const { realm, setRealm } = useRealm();
export const RealmSelector = () => {
const { realm, setRealm, realms } = useRealm();
const { whoAmI } = useContext(WhoAmIContext);
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [filteredItems, setFilteredItems] = useState(realmList);
const [filteredItems, setFilteredItems] = useState<RealmRepresentation[]>();
const history = useHistory();
const { t } = useTranslation("common");
const recentUsed = new RecentUsed();
@ -47,6 +43,7 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
const AddRealm = () => (
<Button
data-testid="add-realm"
component="div"
isBlock
onClick={() => {
@ -59,30 +56,31 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
);
const onFilter = () => {
const filtered =
search === ""
? realmList
: realmList.filter(
if (search === "") {
setFilteredItems(undefined);
} else {
const filtered = realms.filter(
(r) => r.realm!.toLowerCase().indexOf(search.toLowerCase()) !== -1
);
setFilteredItems(filtered || []);
setFilteredItems(filtered);
}
};
const selectRealm = (realm: string) => {
setRealm(realm);
setOpen(!open);
history.push(`/${realm}/`);
};
useEffect(() => {
onFilter();
}, [search]);
const dropdownItems = realmList.map((r) => (
const dropdownItems = realms.map((r) => (
<DropdownItem
key={`realm-dropdown-item-${r.realm}`}
onClick={() => {
selectRealm(r.realm!);
history.push(`/${realm}/`);
}}
>
<RealmText value={r.realm!} />
@ -104,8 +102,9 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
return (
<>
{realmList.length > 5 && (
{realms.length > 5 && (
<ContextSelector
data-testid="realmSelector"
toggleText={toUpperCase(realm)}
isOpen={open}
screenReaderLabel={toUpperCase(realm)}
@ -117,8 +116,10 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
} else {
element = r as ReactElement;
}
const value = element.props.value || "master";
const value = element.props.value;
if (value) {
selectRealm(value);
}
}}
searchInputValue={search}
onSearchInputChange={(value) => setSearch(value)}
@ -130,7 +131,7 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
<RealmText value={realm} /> <Label>{t("recent")}</Label>
</ContextSelectorItem>
))}
{filteredItems
{(filteredItems || realms)
.filter((r) => !recentUsed.used.includes(r.realm!))
.map((item) => (
<ContextSelectorItem key={item.id}>
@ -142,14 +143,15 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
</ContextSelectorItem>
</ContextSelector>
)}
{realmList.length <= 5 && (
{realms.length <= 5 && (
<Dropdown
id="realm-select"
data-testid="realmSelector"
className="keycloak__realm_selector__dropdown"
isOpen={open}
toggle={
<DropdownToggle
id="realm-select-toggle"
data-testid="realmSelectorToggle"
onToggle={() => setOpen(!open)}
className="keycloak__realm_selector_dropdown__toggle"
>

View file

@ -1,26 +0,0 @@
import React from "react";
import { mount } from "enzyme";
import { act } from "@testing-library/react";
import { RealmSelector } from "../RealmSelector";
import { RealmContextProvider } from "../../../context/realm-context/RealmContext";
import { MemoryRouter } from "react-router-dom";
it("renders realm selector", async () => {
const wrapper = mount(
<MemoryRouter>
<RealmContextProvider>
<div id="realm">
<RealmSelector realmList={[{ id: "321", realm: "another" }]} />
</div>
</RealmContextProvider>
</MemoryRouter>
);
const expandButton = wrapper.find("button");
act(() => {
expandButton!.simulate("click");
});
expect(wrapper.find("#realm")).toMatchSnapshot();
});

View file

@ -1,305 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders realm selector 1`] = `
<div
id="realm"
>
<RealmSelector
realmList={
Array [
Object {
"id": "321",
"realm": "another",
},
]
}
>
<Dropdown
className="keycloak__realm_selector__dropdown"
dropdownItems={
Array [
<DropdownItem
onClick={[Function]}
>
<RealmText
value="another"
/>
</DropdownItem>,
<React.Fragment />,
]
}
id="realm-select"
isOpen={false}
toggle={
<DropdownToggle
className="keycloak__realm_selector_dropdown__toggle"
id="realm-select-toggle"
onToggle={[Function]}
>
</DropdownToggle>
}
>
<DropdownWithContext
autoFocus={true}
className="keycloak__realm_selector__dropdown"
direction="down"
dropdownItems={
Array [
<DropdownItem
onClick={[Function]}
>
<RealmText
value="another"
/>
</DropdownItem>,
<React.Fragment />,
]
}
id="realm-select"
isGrouped={false}
isOpen={false}
isPlain={false}
menuAppendTo="inline"
onSelect={[Function]}
position="left"
toggle={
<DropdownToggle
className="keycloak__realm_selector_dropdown__toggle"
id="realm-select-toggle"
onToggle={[Function]}
>
</DropdownToggle>
}
>
<div
className="pf-c-dropdown keycloak__realm_selector__dropdown"
data-ouia-component-id="OUIA-Generated-Dropdown-1"
data-ouia-component-type="PF4/Dropdown"
data-ouia-safe={true}
id="realm-select"
>
<DropdownToggle
aria-haspopup={true}
className="keycloak__realm_selector_dropdown__toggle"
getMenuRef={[Function]}
id="realm-select-toggle"
isOpen={false}
isPlain={false}
key=".0"
onEnter={[Function]}
onToggle={[Function]}
parentRef={
Object {
"current": <div
class="pf-c-dropdown pf-m-expanded keycloak__realm_selector__dropdown"
data-ouia-component-id="OUIA-Generated-Dropdown-1"
data-ouia-component-type="PF4/Dropdown"
data-ouia-safe="true"
id="realm-select"
>
<button
aria-expanded="true"
aria-haspopup="true"
class="pf-c-dropdown__toggle keycloak__realm_selector_dropdown__toggle"
data-ouia-component-id="OUIA-Generated-DropdownToggle-1"
data-ouia-component-type="PF4/DropdownToggle"
data-ouia-safe="true"
id="realm-select-toggle"
type="button"
>
<span
class="pf-c-dropdown__toggle-icon"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
role="img"
style="vertical-align: -0.125em;"
viewBox="0 0 320 512"
width="1em"
>
<path
d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"
/>
</svg>
</span>
</button>
<ul
aria-labelledby="realm-select-toggle"
class="pf-c-dropdown__menu"
role="menu"
>
<li
role="menuitem"
>
<a
aria-disabled="false"
class="pf-c-dropdown__menu-item"
data-ouia-component-id="OUIA-Generated-DropdownItem-1"
data-ouia-component-type="PF4/DropdownItem"
data-ouia-safe="true"
tabindex="-1"
>
<div
class="pf-l-split keycloak__realm_selector__list-item-split"
>
<div
class="pf-l-split__item pf-m-fill"
>
Another
</div>
<div
class="pf-l-split__item"
/>
</div>
</a>
</li>
</ul>
</div>,
}
}
>
<Toggle
aria-haspopup={true}
bubbleEvent={false}
className="keycloak__realm_selector_dropdown__toggle"
data-ouia-component-id="OUIA-Generated-DropdownToggle-1"
data-ouia-component-type="PF4/DropdownToggle"
data-ouia-safe={true}
getMenuRef={[Function]}
id="realm-select-toggle"
isActive={false}
isDisabled={false}
isOpen={false}
isPlain={false}
isPrimary={false}
isSplitButton={false}
onEnter={[Function]}
onToggle={[Function]}
parentRef={
Object {
"current": <div
class="pf-c-dropdown pf-m-expanded keycloak__realm_selector__dropdown"
data-ouia-component-id="OUIA-Generated-Dropdown-1"
data-ouia-component-type="PF4/Dropdown"
data-ouia-safe="true"
id="realm-select"
>
<button
aria-expanded="true"
aria-haspopup="true"
class="pf-c-dropdown__toggle keycloak__realm_selector_dropdown__toggle"
data-ouia-component-id="OUIA-Generated-DropdownToggle-1"
data-ouia-component-type="PF4/DropdownToggle"
data-ouia-safe="true"
id="realm-select-toggle"
type="button"
>
<span
class="pf-c-dropdown__toggle-icon"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
role="img"
style="vertical-align: -0.125em;"
viewBox="0 0 320 512"
width="1em"
>
<path
d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"
/>
</svg>
</span>
</button>
<ul
aria-labelledby="realm-select-toggle"
class="pf-c-dropdown__menu"
role="menu"
>
<li
role="menuitem"
>
<a
aria-disabled="false"
class="pf-c-dropdown__menu-item"
data-ouia-component-id="OUIA-Generated-DropdownItem-1"
data-ouia-component-type="PF4/DropdownItem"
data-ouia-safe="true"
tabindex="-1"
>
<div
class="pf-l-split keycloak__realm_selector__list-item-split"
>
<div
class="pf-l-split__item pf-m-fill"
>
Another
</div>
<div
class="pf-l-split__item"
/>
</div>
</a>
</li>
</ul>
</div>,
}
}
>
<button
aria-expanded={false}
aria-haspopup={true}
className="pf-c-dropdown__toggle keycloak__realm_selector_dropdown__toggle"
data-ouia-component-id="OUIA-Generated-DropdownToggle-1"
data-ouia-component-type="PF4/DropdownToggle"
data-ouia-safe={true}
disabled={false}
id="realm-select-toggle"
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<span
className="pf-c-dropdown__toggle-icon"
>
<CaretDownIcon
color="currentColor"
noVerticalAlign={false}
size="sm"
>
<svg
aria-hidden={true}
aria-labelledby={null}
fill="currentColor"
height="1em"
role="img"
style={
Object {
"verticalAlign": "-0.125em",
}
}
viewBox="0 0 320 512"
width="1em"
>
<path
d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"
/>
</svg>
</CaretDownIcon>
</span>
</button>
</Toggle>
</DropdownToggle>
</div>
</DropdownWithContext>
</Dropdown>
</RealmSelector>
</div>
`;

View file

@ -1,307 +0,0 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import _ from "lodash";
import {
Badge,
Button,
Chip,
ChipGroup,
Divider,
Modal,
ModalVariant,
Select,
SelectGroup,
SelectOption,
SelectVariant,
ToolbarItem,
} from "@patternfly/react-core";
import { KeycloakDataTable } from "../table-toolbar/KeycloakDataTable";
import {
asyncStateFetch,
useAdminClient,
} from "../../context/auth/AdminClient";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { FilterIcon } from "@patternfly/react-icons";
import { Row, ServiceRole } from "./RoleMapping";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
export type MappingType = "service-account" | "client-scope";
type AddRoleMappingModalProps = {
id: string;
type: MappingType;
name: string;
onAssign: (rows: Row[]) => void;
onClose: () => void;
};
type ClientRole = ClientRepresentation & {
numberOfRoles: number;
};
const realmRole = {
name: "realmRoles",
} as ClientRepresentation;
export const AddRoleMappingModal = ({
id,
name,
type,
onAssign,
onClose,
}: AddRoleMappingModalProps) => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const errorHandler = useErrorHandler();
const [clients, setClients] = useState<ClientRole[]>([]);
const [name, setName] = useState<string>();
const [searchToggle, setSearchToggle] = useState(false);
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const [selectedClients, setSelectedClients] = useState<ClientRole[]>([]);
const [selectedRows, setSelectedRows] = useState<Row[]>([]);
useEffect(
() =>
asyncStateFetch(
async () => {
const clients = await adminClient.clients.find();
setName(clients.find((client) => client.id === clientId)?.clientId);
return (
await Promise.all(
clients.map(async (client) => {
let roles: RoleRepresentation[] = [];
if (type === "service-account") {
roles = await adminClient.users.listAvailableClientRoleMappings(
{
id: id,
clientUniqueId: client.id!,
}
);
} else if (type === "client-scope") {
roles = await adminClient.clientScopes.listAvailableClientScopeMappings(
{
id,
client: client.id!,
}
);
}
return {
roles,
client,
};
})
)
)
.flat()
.filter((row) => row.roles.length !== 0)
.map((row) => {
return { ...row.client, numberOfRoles: row.roles.length };
});
},
(clients) => {
setClients(clients);
},
errorHandler
),
[]
);
useEffect(refresh, [searchToggle]);
const removeClient = (client: ClientRole) => {
setSelectedClients(selectedClients.filter((item) => item.id !== client.id));
};
const loader = async () => {
const realmRolesSelected = _.findIndex(
selectedClients,
(client) => client.name === "realmRoles"
);
let selected = selectedClients;
if (realmRolesSelected !== -1) {
selected = selectedClients.filter(
(client) => client.name !== "realmRoles"
);
}
let availableRoles: RoleRepresentation[] = [];
if (type === "service-account") {
availableRoles = await adminClient.users.listAvailableRealmRoleMappings({
id,
});
} else if (type === "client-scope") {
availableRoles = await adminClient.clientScopes.listAvailableRealmScopeMappings(
{ id }
);
}
const realmRoles = availableRoles.map((role) => {
return {
role,
client: undefined,
};
});
const allClients =
selectedClients.length !== 0
? selected
: await adminClient.clients.find();
const roles = (
await Promise.all(
allClients.map(async (client) => {
let clientAvailableRoles: RoleRepresentation[] = [];
if (type === "service-account") {
clientAvailableRoles = await adminClient.users.listAvailableClientRoleMappings(
{
id,
clientUniqueId: client.id!,
}
);
} else if (type === "client-scope") {
clientAvailableRoles = await adminClient.clientScopes.listAvailableClientScopeMappings(
{ id, client: client.id! }
);
}
return clientAvailableRoles.map((role) => {
return {
role,
client,
};
});
})
)
).flat();
return [
...(realmRolesSelected !== -1 || selected.length === 0 ? realmRoles : []),
...roles,
];
};
const createSelectGroup = (clients: ClientRepresentation[]) => [
<SelectGroup key="role" label={t("realmRoles")}>
<SelectOption key="realmRoles" value={realmRole}>
{t("realmRoles")}
</SelectOption>
</SelectGroup>,
<Divider key="divider" />,
<SelectGroup key="group" label={t("clients")}>
{clients.map((client) => (
<SelectOption key={client.id} value={client}>
{client.clientId}
</SelectOption>
))}
</SelectGroup>,
];
return (
<Modal
variant={ModalVariant.large}
<<<<<<< HEAD:src/clients/service-account/AddServiceAccountModal.tsx
title={t("assignRolesTo", {
client: name,
})}
=======
title={t("assignRolesTo", { client: name })}
>>>>>>> 0f6f6ab (fixed assign):src/components/role-mapping/AddRoleMappingModal.tsx
isOpen={true}
onClose={onClose}
actions={[
<Button
data-testid="assign"
key="confirm"
isDisabled={selectedRows?.length === 0}
variant="primary"
onClick={() => {
onAssign(selectedRows);
onClose();
}}
>
{t("assign")}
</Button>,
<Button
data-testid="cancel"
key="cancel"
variant="link"
onClick={onClose}
>
{t("common:cancel")}
</Button>,
]}
>
<Select
toggleId="role"
onToggle={() => setSearchToggle(!searchToggle)}
isOpen={searchToggle}
variant={SelectVariant.checkbox}
hasInlineFilter
menuAppendTo="parent"
placeholderText={
<>
<FilterIcon /> {t("filterByOrigin")}
</>
}
isGrouped
onFilter={(evt) => {
const value = evt?.target.value || "";
return createSelectGroup(
clients.filter((client) => client.clientId?.includes(value))
);
}}
selections={selectedClients}
onClear={() => setSelectedClients([])}
onSelect={(_, selection) => {
const client = selection as ClientRole;
if (selectedClients.includes(client)) {
removeClient(client);
} else {
setSelectedClients([...selectedClients, client]);
}
}}
>
{createSelectGroup(clients)}
</Select>
<ToolbarItem variant="chip-group">
<ChipGroup>
{selectedClients.map((client) => (
<Chip
key={`chip-${client.id}`}
onClick={() => {
removeClient(client);
refresh();
}}
>
{client.clientId || t("realmRoles")}
<Badge isRead={true}>{client.numberOfRoles}</Badge>
</Chip>
))}
</ChipGroup>
</ToolbarItem>
<KeycloakDataTable
key={key}
onSelect={(rows) => setSelectedRows([...rows])}
searchPlaceholderKey="clients:searchByRoleName"
canSelectAll={false}
loader={loader}
ariaLabelKey="clients:roles"
columns={[
{
name: "name",
cellRenderer: ServiceRole,
},
{
name: "role.description",
displayKey: t("description"),
},
]}
/>
</Modal>
);
};

View file

@ -290,7 +290,6 @@ export function KeycloakDataTable<T>({
)
)
);
setSearch;
};
const convertAction = () =>

View file

@ -1,20 +1,12 @@
import { createContext, useContext } from "react";
import KeycloakAdminClient from "keycloak-admin";
import { RealmContext } from "../realm-context/RealmContext";
export const AdminClient = createContext<KeycloakAdminClient | undefined>(
undefined
);
export const useAdminClient = () => {
const adminClient = useContext(AdminClient)!;
const { realm } = useContext(RealmContext);
adminClient.setConfig({
realmName: realm,
});
return adminClient;
return useContext(AdminClient)!;
};
/**

View file

@ -1,14 +1,24 @@
import React, { useContext, useState } from "react";
import React, { useContext, useEffect, useState } from "react";
import _ from "lodash";
import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { RecentUsed } from "../../components/realm-selector/recent-used";
import { useErrorHandler } from "react-error-boundary";
import { asyncStateFetch, useAdminClient } from "../auth/AdminClient";
import { WhoAmIContext } from "../whoami/WhoAmI";
type RealmContextType = {
realm: string;
setRealm: (realm: string) => void;
realms: RealmRepresentation[];
refresh: () => Promise<void>;
};
export const RealmContext = React.createContext<RealmContextType>({
realm: "",
setRealm: () => {},
realms: [],
refresh: () => Promise.resolve(),
});
type RealmContextProviderProps = { children: React.ReactNode };
@ -16,16 +26,52 @@ type RealmContextProviderProps = { children: React.ReactNode };
export const RealmContextProvider = ({
children,
}: RealmContextProviderProps) => {
const [realm, setRealm] = useState("");
const { whoAmI } = useContext(WhoAmIContext);
const [realm, setRealm] = useState(whoAmI.getHomeRealm());
const [realms, setRealms] = useState<RealmRepresentation[]>([]);
const adminClient = useAdminClient();
const errorHandler = useErrorHandler();
const recentUsed = new RecentUsed();
const set = (realm: string) => {
recentUsed.setRecentUsed(realm);
setRealm(realm);
const updateRealmsList = (realms: RealmRepresentation[]) => {
setRealms(_.sortBy(realms, "realm"));
recentUsed.clean(realms.map((r) => r.realm!));
};
useEffect(
() =>
asyncStateFetch(
() => adminClient.realms.find(),
(realms) => updateRealmsList(realms),
errorHandler
),
[]
);
const set = (realm: string) => {
if (
realms.length === 0 ||
realms.findIndex((r) => r.realm == realm) !== -1
) {
recentUsed.setRecentUsed(realm);
setRealm(realm);
adminClient.setConfig({
realmName: realm,
});
}
};
return (
<RealmContext.Provider value={{ realm, setRealm: set }}>
<RealmContext.Provider
value={{
realm,
setRealm: set,
realms,
refresh: async () => {
const list = await adminClient.realms.find();
updateRealmsList(list);
},
}}
>
{children}
</RealmContext.Provider>
);

View file

@ -1,9 +1,8 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useErrorHandler } from "react-error-boundary";
import i18n from "../../i18n";
import { AdminClient, asyncStateFetch } from "../auth/AdminClient";
import { RealmContext } from "../realm-context/RealmContext";
import { asyncStateFetch, useAdminClient } from "../auth/AdminClient";
import WhoAmIRepresentation, {
AccessType,
} from "keycloak-admin/lib/defs/whoAmIRepresentation";
@ -62,23 +61,16 @@ export const WhoAmIContext = React.createContext<WhoAmIProps>({
type WhoAmIProviderProps = { children: React.ReactNode };
export const WhoAmIContextProvider = ({ children }: WhoAmIProviderProps) => {
const adminClient = useContext(AdminClient)!;
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { realm, setRealm } = useContext(RealmContext);
const [whoAmI, setWhoAmI] = useState<WhoAmI>(new WhoAmI());
const [key, setKey] = useState(0);
useEffect(() => {
return asyncStateFetch(
() =>
adminClient.whoAmI.find({
realm: adminClient.keycloak?.realm,
}),
() => adminClient.whoAmI.find({ realm: "master" }),
(me) => {
const whoAmI = new WhoAmI(adminClient.keycloak?.realm, me);
if (!realm) {
setRealm(whoAmI.getHomeRealm());
}
setWhoAmI(whoAmI);
},
handleError

View file

@ -43,7 +43,7 @@ const RealmSettingsHeader = ({
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const history = useHistory();
const { setRealm } = useRealm();
const { refresh } = useRealm();
const [partialImportOpen, setPartialImportOpen] = useState(false);
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
@ -65,8 +65,8 @@ const RealmSettingsHeader = ({
try {
await adminClient.realms.del({ realm: realmName });
addAlert(t("deletedSuccess"), AlertVariant.success);
setRealm("master");
history.push("/master/");
refresh();
} catch (error) {
addAlert(t("deleteError", { error }), AlertVariant.danger);
}

View file

@ -19,11 +19,13 @@ import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { useAdminClient } from "../../context/auth/AdminClient";
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
import { FormAccess } from "../../components/form-access/FormAccess";
import { useRealm } from "../../context/realm-context/RealmContext";
export const NewRealmForm = () => {
const { t } = useTranslation("realm");
const history = useHistory();
const { refresh } = useContext(WhoAmIContext);
const { refresh: realmRefresh } = useRealm();
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
@ -56,10 +58,12 @@ export const NewRealmForm = () => {
try {
await adminClient.realms.create(realm);
addAlert(t("saveRealmSuccess"), AlertVariant.success);
refresh();
//force token update
refresh();
await adminClient.keycloak?.updateToken(Number.MAX_VALUE);
history.push(`/${realm.realm}/`);
await realmRefresh();
history.push(`/${realm.realm}`);
} catch (error) {
addAlert(
t("saveRealmError", {

View file

@ -43,7 +43,12 @@ export const MockAdminClient = (props: {
>
<WhoAmIContextProvider>
<RealmContext.Provider
value={{ realm: "master", setRealm: () => {} }}
value={{
realm: "master",
setRealm: () => {},
realms: [],
refresh: () => Promise.resolve(),
}}
>
<AccessContextProvider>{props.children}</AccessContextProvider>
</RealmContext.Provider>

View file

@ -9,7 +9,7 @@ import {
import { Meta } from "@storybook/react";
import { RealmSelector } from "../components/realm-selector/RealmSelector";
import { RealmContextProvider } from "../context/realm-context/RealmContext";
import { RealmContext } from "../context/realm-context/RealmContext";
import { HashRouter } from "react-router-dom";
export default {
@ -20,20 +20,24 @@ export default {
export const Header = () => {
return (
<HashRouter>
<RealmContextProvider>
<RealmContext.Provider
value={{
realm: "master",
setRealm: () => {},
realms: [
{ id: "master", realm: "Master" },
{ id: "photoz", realm: "Photoz" },
],
refresh: () => Promise.resolve(),
}}
>
<Page
sidebar={
<PageSidebar
nav={
<Nav>
<NavList>
<RealmSelector
realmList={[
{ id: "master", realm: "Master" },
{ id: "photoz", realm: "Photoz" },
]}
/>
<RealmSelector />
<NavItem id="default-link1" to="#default-link1" itemId={0}>
Link 1
</NavItem>
@ -57,7 +61,7 @@ export const Header = () => {
/>
}
/>
</RealmContextProvider>
</RealmContext.Provider>
</HashRouter>
);
};