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:
parent
5142d1b4bc
commit
eb9092116d
6 changed files with 78 additions and 13 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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",
|
||||||
|
|
||||||
|
|
|
@ -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,16 +111,28 @@ 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={realm}>
|
||||||
|
<RealmText value={realm} /> <Label>{t("recent")}</Label>
|
||||||
|
</ContextSelectorItem>
|
||||||
|
))}
|
||||||
|
{filteredItems
|
||||||
|
.filter((r) => !recentUsed.used.includes(r.realm!))
|
||||||
|
.map((item) => (
|
||||||
<ContextSelectorItem key={item.id}>
|
<ContextSelectorItem key={item.id}>
|
||||||
<RealmText value={item.realm!} />
|
<RealmText value={item.realm!} />
|
||||||
</ContextSelectorItem>
|
</ContextSelectorItem>
|
||||||
|
|
32
src/components/realm-selector/recent-used.ts
Normal file
32
src/components/realm-selector/recent-used.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue