added recently used realms to the top (#502)

* added recently used realms to the top

fixing: #396

* fixed add realm button

* moved setting recent to setRealm
cleanup to where realm list is updated
This commit is contained in:
Erik Jan de Wit 2021-04-08 21:20:35 +02:00 committed by GitHub
parent 5142d1b4bc
commit eb9092116d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 13 deletions

View file

@ -23,6 +23,7 @@ import { SubGroups } from "./groups/SubGroupsContext";
import { useRealm } from "./context/realm-context/RealmContext"; import { useRealm } from "./context/realm-context/RealmContext";
import { useAdminClient, asyncStateFetch } from "./context/auth/AdminClient"; import { useAdminClient, asyncStateFetch } from "./context/auth/AdminClient";
import { ErrorRenderer } from "./components/error/ErrorRenderer"; import { ErrorRenderer } from "./components/error/ErrorRenderer";
import { RecentUsed } from "./components/realm-selector/recent-used";
export const mainPageContentId = "kc-main-content-page-container"; export const mainPageContentId = "kc-main-content-page-container";
@ -44,11 +45,14 @@ const RealmPathSelector = ({ children }: { children: ReactNode }) => {
const { realm } = useParams<{ realm: string }>(); const { realm } = useParams<{ realm: string }>();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const handleError = useErrorHandler(); const handleError = useErrorHandler();
const recentUsed = new RecentUsed();
useEffect( useEffect(
() => () =>
asyncStateFetch( asyncStateFetch(
() => adminClient.realms.find(), () => adminClient.realms.find(),
(realms) => { (realms) => {
recentUsed.clean(realms.map((r) => r.realm!));
if (realms.findIndex((r) => r.realm == realm) !== -1) { if (realms.findIndex((r) => r.realm == realm) !== -1) {
setRealm(realm); setRealm(realm);
} }

View file

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import _ from "lodash";
import { import {
Nav, Nav,
NavItem, NavItem,
@ -8,6 +9,7 @@ import {
NavList, NavList,
PageSidebar, PageSidebar,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { RealmSelector } from "./components/realm-selector/RealmSelector"; import { RealmSelector } from "./components/realm-selector/RealmSelector";
import { useRealm } from "./context/realm-context/RealmContext"; import { useRealm } from "./context/realm-context/RealmContext";
import { DataLoader } from "./components/data-loader/DataLoader"; import { DataLoader } from "./components/data-loader/DataLoader";
@ -21,7 +23,7 @@ export const PageNav: React.FunctionComponent = () => {
const { realm } = useRealm(); const { realm } = useRealm();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const realmLoader = async () => { const realmLoader = async () => {
return await adminClient.realms.find(); return _.sortBy(await adminClient.realms.find(), "realm");
}; };
const history = useHistory(); const history = useHistory();

View file

@ -84,6 +84,7 @@
"maxLength": "Max length {{length}}", "maxLength": "Max length {{length}}",
"createRealm": "Create Realm", "createRealm": "Create Realm",
"recent": "Recent",
"jumpToSection": "Jump to section", "jumpToSection": "Jump to section",

View file

@ -12,6 +12,7 @@ import {
Split, Split,
ContextSelector, ContextSelector,
ContextSelectorItem, ContextSelectorItem,
Label,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { CheckIcon } from "@patternfly/react-icons"; import { CheckIcon } from "@patternfly/react-icons";
@ -19,6 +20,7 @@ import { toUpperCase } from "../../util";
import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation"; import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import { WhoAmIContext } from "../../context/whoami/WhoAmI"; import { WhoAmIContext } from "../../context/whoami/WhoAmI";
import { RecentUsed } from "./recent-used";
import "./realm-selector.css"; import "./realm-selector.css";
@ -34,6 +36,8 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
const [filteredItems, setFilteredItems] = useState(realmList); const [filteredItems, setFilteredItems] = useState(realmList);
const history = useHistory(); const history = useHistory();
const { t } = useTranslation("common"); const { t } = useTranslation("common");
const recentUsed = new RecentUsed();
const RealmText = ({ value }: { value: string }) => ( const RealmText = ({ value }: { value: string }) => (
<Split className="keycloak__realm_selector__list-item-split"> <Split className="keycloak__realm_selector__list-item-split">
<SplitItem isFilled>{toUpperCase(value)}</SplitItem> <SplitItem isFilled>{toUpperCase(value)}</SplitItem>
@ -41,7 +45,7 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
</Split> </Split>
); );
const AddRealm = ({ className }: { className?: string }) => ( const AddRealm = () => (
<Button <Button
component="div" component="div"
isBlock isBlock
@ -49,7 +53,6 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
history.push(`/${realm}/add-realm`); history.push(`/${realm}/add-realm`);
setOpen(!open); setOpen(!open);
}} }}
className={className}
> >
{t("createRealm")} {t("createRealm")}
</Button> </Button>
@ -65,6 +68,11 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
setFilteredItems(filtered || []); setFilteredItems(filtered || []);
}; };
const selectRealm = (realm: string) => {
setRealm(realm);
setOpen(!open);
};
useEffect(() => { useEffect(() => {
onFilter(); onFilter();
}, [search]); }, [search]);
@ -73,9 +81,8 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
<DropdownItem <DropdownItem
key={`realm-dropdown-item-${r.realm}`} key={`realm-dropdown-item-${r.realm}`}
onClick={() => { onClick={() => {
setRealm(r.realm!); selectRealm(r.realm!);
history.push(`/${r.realm}/`); history.push(`/${realm}/`);
setOpen(!open);
}} }}
> >
<RealmText value={r.realm!} /> <RealmText value={r.realm!} />
@ -104,20 +111,32 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
screenReaderLabel={toUpperCase(realm)} screenReaderLabel={toUpperCase(realm)}
onToggle={() => setOpen(!open)} onToggle={() => setOpen(!open)}
onSelect={(_, r) => { onSelect={(_, r) => {
const value = (r as ReactElement).props.value; let element: ReactElement;
setRealm(value || "master"); if (Array.isArray(r)) {
setOpen(!open); element = (r as ReactElement[])[0];
} else {
element = r as ReactElement;
}
const value = element.props.value || "master";
selectRealm(value);
}} }}
searchInputValue={search} searchInputValue={search}
onSearchInputChange={(value) => setSearch(value)} onSearchInputChange={(value) => setSearch(value)}
onSearchButtonClick={() => onFilter()} onSearchButtonClick={() => onFilter()}
className="keycloak__realm_selector__context_selector" className="keycloak__realm_selector__context_selector"
> >
{filteredItems.map((item) => ( {recentUsed.used.map((realm) => (
<ContextSelectorItem key={item.id}> <ContextSelectorItem key={realm}>
<RealmText value={item.realm!} /> <RealmText value={realm} /> <Label>{t("recent")}</Label>
</ContextSelectorItem> </ContextSelectorItem>
))} ))}
{filteredItems
.filter((r) => !recentUsed.used.includes(r.realm!))
.map((item) => (
<ContextSelectorItem key={item.id}>
<RealmText value={item.realm!} />
</ContextSelectorItem>
))}
<ContextSelectorItem key="add"> <ContextSelectorItem key="add">
<AddRealm /> <AddRealm />
</ContextSelectorItem> </ContextSelectorItem>

View file

@ -0,0 +1,32 @@
export class RecentUsed {
private readonly MAX_NUM = 3;
private readonly KEY = "recent-used-realms";
private recentUsedRealms: string[];
constructor() {
this.recentUsedRealms = JSON.parse(localStorage.getItem(this.KEY) || "[]");
}
private save() {
this.recentUsedRealms = this.recentUsedRealms.slice(0, this.MAX_NUM);
localStorage.setItem(this.KEY, JSON.stringify(this.recentUsedRealms));
}
clean(existingRealms: string[]) {
this.recentUsedRealms = this.recentUsedRealms.filter((realm) =>
existingRealms.includes(realm)
);
this.save();
}
get used(): string[] {
return this.recentUsedRealms;
}
setRecentUsed(realm: string) {
if (!this.recentUsedRealms.includes(realm)) {
this.recentUsedRealms.unshift(realm);
this.save();
}
}
}

View file

@ -1,4 +1,5 @@
import React, { useContext, useState } from "react"; import React, { useContext, useState } from "react";
import { RecentUsed } from "../../components/realm-selector/recent-used";
type RealmContextType = { type RealmContextType = {
realm: string; realm: string;
@ -16,9 +17,15 @@ export const RealmContextProvider = ({
children, children,
}: RealmContextProviderProps) => { }: RealmContextProviderProps) => {
const [realm, setRealm] = useState(""); const [realm, setRealm] = useState("");
const recentUsed = new RecentUsed();
const set = (realm: string) => {
recentUsed.setRecentUsed(realm);
setRealm(realm);
};
return ( return (
<RealmContext.Provider value={{ realm, setRealm }}> <RealmContext.Provider value={{ realm, setRealm: set }}>
{children} {children}
</RealmContext.Provider> </RealmContext.Provider>
); );