From eb9092116d9a97a9e3427285c656fc2e78f3465e Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Thu, 8 Apr 2021 21:20:35 +0200 Subject: [PATCH] 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 --- src/App.tsx | 4 ++ src/PageNav.tsx | 4 +- src/common-messages.json | 1 + .../realm-selector/RealmSelector.tsx | 41 ++++++++++++++----- src/components/realm-selector/recent-used.ts | 32 +++++++++++++++ src/context/realm-context/RealmContext.tsx | 9 +++- 6 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 src/components/realm-selector/recent-used.ts diff --git a/src/App.tsx b/src/App.tsx index b315dc56f4..2d85b88813 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,6 +23,7 @@ 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"; @@ -44,11 +45,14 @@ const RealmPathSelector = ({ children }: { children: ReactNode }) => { 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); } diff --git a/src/PageNav.tsx b/src/PageNav.tsx index 91120f990d..09a13f6a40 100644 --- a/src/PageNav.tsx +++ b/src/PageNav.tsx @@ -1,6 +1,7 @@ import React from "react"; import { useHistory, useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; +import _ from "lodash"; import { Nav, NavItem, @@ -8,6 +9,7 @@ import { NavList, PageSidebar, } from "@patternfly/react-core"; + import { RealmSelector } from "./components/realm-selector/RealmSelector"; import { useRealm } from "./context/realm-context/RealmContext"; import { DataLoader } from "./components/data-loader/DataLoader"; @@ -21,7 +23,7 @@ export const PageNav: React.FunctionComponent = () => { const { realm } = useRealm(); const adminClient = useAdminClient(); const realmLoader = async () => { - return await adminClient.realms.find(); + return _.sortBy(await adminClient.realms.find(), "realm"); }; const history = useHistory(); diff --git a/src/common-messages.json b/src/common-messages.json index 0db0b31e01..e288163ca8 100644 --- a/src/common-messages.json +++ b/src/common-messages.json @@ -84,6 +84,7 @@ "maxLength": "Max length {{length}}", "createRealm": "Create Realm", + "recent": "Recent", "jumpToSection": "Jump to section", diff --git a/src/components/realm-selector/RealmSelector.tsx b/src/components/realm-selector/RealmSelector.tsx index e8669c5d6d..1228b4f09a 100644 --- a/src/components/realm-selector/RealmSelector.tsx +++ b/src/components/realm-selector/RealmSelector.tsx @@ -12,6 +12,7 @@ import { Split, ContextSelector, ContextSelectorItem, + Label, } from "@patternfly/react-core"; import { CheckIcon } from "@patternfly/react-icons"; @@ -19,6 +20,7 @@ import { toUpperCase } from "../../util"; import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation"; import { useRealm } from "../../context/realm-context/RealmContext"; import { WhoAmIContext } from "../../context/whoami/WhoAmI"; +import { RecentUsed } from "./recent-used"; import "./realm-selector.css"; @@ -34,6 +36,8 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => { const [filteredItems, setFilteredItems] = useState(realmList); const history = useHistory(); const { t } = useTranslation("common"); + const recentUsed = new RecentUsed(); + const RealmText = ({ value }: { value: string }) => ( {toUpperCase(value)} @@ -41,7 +45,7 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => { ); - const AddRealm = ({ className }: { className?: string }) => ( + const AddRealm = () => ( @@ -65,6 +68,11 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => { setFilteredItems(filtered || []); }; + const selectRealm = (realm: string) => { + setRealm(realm); + setOpen(!open); + }; + useEffect(() => { onFilter(); }, [search]); @@ -73,9 +81,8 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => { { - setRealm(r.realm!); - history.push(`/${r.realm}/`); - setOpen(!open); + selectRealm(r.realm!); + history.push(`/${realm}/`); }} > @@ -104,20 +111,32 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => { screenReaderLabel={toUpperCase(realm)} onToggle={() => setOpen(!open)} onSelect={(_, r) => { - const value = (r as ReactElement).props.value; - setRealm(value || "master"); - setOpen(!open); + let element: ReactElement; + if (Array.isArray(r)) { + element = (r as ReactElement[])[0]; + } else { + element = r as ReactElement; + } + const value = element.props.value || "master"; + selectRealm(value); }} searchInputValue={search} onSearchInputChange={(value) => setSearch(value)} onSearchButtonClick={() => onFilter()} className="keycloak__realm_selector__context_selector" > - {filteredItems.map((item) => ( - - + {recentUsed.used.map((realm) => ( + + ))} + {filteredItems + .filter((r) => !recentUsed.used.includes(r.realm!)) + .map((item) => ( + + + + ))} diff --git a/src/components/realm-selector/recent-used.ts b/src/components/realm-selector/recent-used.ts new file mode 100644 index 0000000000..be49955821 --- /dev/null +++ b/src/components/realm-selector/recent-used.ts @@ -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(); + } + } +} diff --git a/src/context/realm-context/RealmContext.tsx b/src/context/realm-context/RealmContext.tsx index f98a1c86e7..c55c9b2a25 100644 --- a/src/context/realm-context/RealmContext.tsx +++ b/src/context/realm-context/RealmContext.tsx @@ -1,4 +1,5 @@ import React, { useContext, useState } from "react"; +import { RecentUsed } from "../../components/realm-selector/recent-used"; type RealmContextType = { realm: string; @@ -16,9 +17,15 @@ export const RealmContextProvider = ({ children, }: RealmContextProviderProps) => { const [realm, setRealm] = useState(""); + const recentUsed = new RecentUsed(); + + const set = (realm: string) => { + recentUsed.setRecentUsed(realm); + setRealm(realm); + }; return ( - + {children} );