1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

fix namespace filter to not jump after clicking

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-09-17 10:10:31 -04:00
parent 13b99afa21
commit 39cdb9e4ff
18 changed files with 191 additions and 118 deletions

View File

@ -10,7 +10,7 @@ import { namespaceStore } from "../+namespaces/namespace.store";
@observer @observer
export class Apps extends React.Component { export class Apps extends React.Component {
static get tabRoutes(): TabRoute[] { static get tabRoutes(): TabRoute[] {
const query = namespaceStore.getContextParams(); const query = namespaceStore.contextParams;
return [ return [
{ {
title: <Trans>Charts</Trans>, title: <Trans>Charts</Trans>,
@ -32,8 +32,8 @@ export class Apps extends React.Component {
return ( return (
<TabLayout className="Apps" tabs={tabRoutes}> <TabLayout className="Apps" tabs={tabRoutes}>
<Switch> <Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)} {tabRoutes.map((route, index) => <Route key={index} {...route} />)}
<Redirect to={tabRoutes[0].url}/> <Redirect to={tabRoutes[0].url} />
</Switch> </Switch>
</TabLayout> </TabLayout>
) )

View File

@ -20,7 +20,7 @@ export const clusterIssuersURL = buildURL("/clusterissuers");
@observer @observer
export class Config extends React.Component { export class Config extends React.Component {
static get tabRoutes(): TabRoute[] { static get tabRoutes(): TabRoute[] {
const query = namespaceStore.getContextParams() const query = namespaceStore.contextParams
const routes: TabRoute[] = [] const routes: TabRoute[] = []
if (isAllowedResource("configmaps")) { if (isAllowedResource("configmaps")) {
routes.push({ routes.push({
@ -70,8 +70,8 @@ export class Config extends React.Component {
return ( return (
<TabLayout className="Config" tabs={tabRoutes}> <TabLayout className="Config" tabs={tabRoutes}>
<Switch> <Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)} {tabRoutes.map((route, index) => <Route key={index} {...route} />)}
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/> <Redirect to={configURL({ query: namespaceStore.contextParams })} />
</Switch> </Switch>
</TabLayout> </TabLayout>
) )

View File

@ -14,6 +14,19 @@
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
&__option {
&--is-selected {
.Icon {
visibility: visible!important;
}
}
.Icon {
visibility: hidden;
}
}
} }
} }

View File

@ -5,18 +5,16 @@ import { computed } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { Select, SelectOption, SelectProps } from "../select"; import { Select, SelectOption, SelectProps } from "../select";
import { cssNames, noop } from "../../utils"; import ReactSelect, { ActionMeta, components, OptionTypeBase, ValueType } from "react-select"
import { autobind, cssNames, noop } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { namespaceStore } from "./namespace.store"; import { namespaceStore } from "./namespace.store";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { FilterIcon } from "../item-object-list/filter-icon";
import { FilterType } from "../item-object-list/page-filters.store";
interface Props extends SelectProps { interface Props extends SelectProps {
showIcons?: boolean; showIcons?: boolean;
showClusterOption?: boolean; // show cluster option on the top (default: false) showClusterOption?: boolean; // show cluster option on the top (default: false)
clusterOptionLabel?: React.ReactNode; // label for cluster option (default: "Cluster") clusterOptionLabel?: React.ReactNode; // label for cluster option (default: "Cluster")
customizeOptions?(nsOptions: SelectOption[]): SelectOption[];
} }
const defaultProps: Partial<Props> = { const defaultProps: Partial<Props> = {
@ -44,12 +42,15 @@ export class NamespaceSelect extends React.Component<Props> {
} }
@computed get options(): SelectOption[] { @computed get options(): SelectOption[] {
const { customizeOptions, showClusterOption, clusterOptionLabel } = this.props; const options: SelectOption[] = namespaceStore.items
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() })); .map(ns => ({ value: ns.getName() }))
options = customizeOptions ? customizeOptions(options) : options; .map(opt => ({ ...opt, label: this.formatOptionLabel(opt) }))
const { showClusterOption, clusterOptionLabel } = this.props;
if (showClusterOption) { if (showClusterOption) {
options.unshift({ value: null, label: clusterOptionLabel }); options.unshift({ value: null, label: clusterOptionLabel });
} }
return options; return options;
} }
@ -58,7 +59,7 @@ export class NamespaceSelect extends React.Component<Props> {
const { value, label } = option; const { value, label } = option;
return label || ( return label || (
<> <>
{showIcons && <Icon small material="layers"/>} {showIcons && <Icon small material="layers" />}
{value} {value}
</> </>
); );
@ -70,7 +71,7 @@ export class NamespaceSelect extends React.Component<Props> {
<Select <Select
className={cssNames("NamespaceSelect", className)} className={cssNames("NamespaceSelect", className)}
menuClass="NamespaceSelectMenu" menuClass="NamespaceSelectMenu"
formatOptionLabel={this.formatOptionLabel} autoConvertOptions={false}
options={this.options} options={this.options}
{...selectProps} {...selectProps}
/> />
@ -78,30 +79,53 @@ export class NamespaceSelect extends React.Component<Props> {
} }
} }
interface BasicNS {
label: React.ReactElement;
value: string;
}
@observer @observer
export class NamespaceSelectFilter extends React.Component { export class NamespaceSelectFilter extends React.Component {
async componentDidMount() {
if (!namespaceStore.isLoaded) {
await namespaceStore.loadAll();
namespaceStore.contextNs.clear();
}
}
@autobind()
onChange(value: BasicNS, actionMeta: ActionMeta<BasicNS>) {
switch (actionMeta.action) {
case "select-option":
namespaceStore.contextNs.add(actionMeta.option.value)
break
case "clear":
namespaceStore.contextNs.clear()
break
case "deselect-option":
namespaceStore.contextNs.delete(actionMeta.option.value)
break
}
}
render() { render() {
const { contextNs, hasContext, toggleContext } = namespaceStore;
let placeholder = <Trans>All namespaces</Trans>;
if (contextNs.length == 1) placeholder = <Trans>Namespace: {contextNs[0]}</Trans>
if (contextNs.length >= 2) placeholder = <Trans>Namespaces: {contextNs.join(", ")}</Trans>
return ( return (
<NamespaceSelect <ReactSelect
placeholder={placeholder} placeholder={<Trans>Filter by namespace...</Trans>}
isMulti
closeMenuOnSelect={false} closeMenuOnSelect={false}
isOptionSelected={() => false} hideSelectedOptions={false}
controlShouldRenderValue={false} className={cssNames("Select", "NamespaceSelect", "theme-dark")}
onChange={({ value: namespace }: SelectOption) => toggleContext(namespace)} classNamePrefix="Select"
formatOptionLabel={({ value: namespace }: SelectOption) => { components={{
const isSelected = hasContext(namespace); Menu(props) {
return ( return <components.Menu {...props} className="NamespaceSelectMenu" />
<div className="flex gaps align-center"> }
<FilterIcon type={FilterType.NAMESPACE}/>
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right"/>}
</div>
)
}} }}
onChange={this.onChange}
options={namespaceStore.Options}
value={namespaceStore.SelectedValues}
controlShouldRenderValue={false}
/> />
) )
} }

View File

@ -1,45 +1,50 @@
import { action, observable, reaction } from "mobx"; import React from "react";
import { action, computed, observable, ObservableSet, reaction } from "mobx";
import { autobind, createStorage } from "../../utils"; import { autobind, createStorage } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectStore } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints"; import { Namespace, namespacesApi } from "../../api/endpoints";
import { IQueryParams, navigation, setQueryParams } from "../../navigation"; import { IQueryParams, navigation, setQueryParams } from "../../navigation";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
import { isAllowedResource } from "../../../common/rbac"; import { isAllowedResource } from "../../../common/rbac";
import { Icon } from "../icon";
@autobind() @autobind()
export class NamespaceStore extends KubeObjectStore<Namespace> { export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi; api = namespacesApi;
contextNs = observable.array<string>(); contextNs = observable.set<string>();
protected storage = createStorage<string[]>("context_ns", this.contextNs); protected storage = createStorage<Set<string>>("context_ns", this.contextNs, {
parse(from: string) {
get initNamespaces() { return new Set(JSON.parse(from))
const fromUrl = navigation.searchParams.getAsArray("namespaces"); },
return fromUrl.length ? fromUrl : this.storage.get(); stringify(from: Set<string>) {
} return JSON.stringify([...from.values()])
}
});
constructor() { constructor() {
super(); super();
// restore context namespaces // restore context namespaces
const { initNamespaces: namespaces } = this; const fromUrl = navigation.searchParams.getAsArray("namespaces")
this.setContext(namespaces); const namespaces = fromUrl.length ? fromUrl : this.storage.get();
this.updateUrl(namespaces); this.context = [...namespaces];
this.updateUrl(...namespaces);
// sync with local-storage & url-search-params // sync with local-storage & url-search-params
reaction(() => this.contextNs.toJS(), namespaces => { reaction(() => this.contextNs.toJS(), namespaces => {
this.storage.set(namespaces); this.storage.set(namespaces);
this.updateUrl(namespaces); this.updateUrl(...namespaces.values());
}); });
} }
getContextParams(): Partial<IQueryParams> { @computed
return { get contextParams(): IQueryParams {
namespaces: this.contextNs const namespaces = [...this.contextNs.values()]
} return { namespaces }
} }
protected updateUrl(namespaces: string[]) { protected updateUrl(...namespaces: string[]) {
setQueryParams({ namespaces }, { replace: true }) setQueryParams({ namespaces }, { replace: true })
} }
@ -68,18 +73,42 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}) })
} }
setContext(namespaces: string[]) { @action
set context(namespaces: string[]) {
this.contextNs.replace(namespaces); this.contextNs.replace(namespaces);
} }
@action
hasContext(namespace: string | string[]) { hasContext(namespace: string | string[]) {
const context = Array.isArray(namespace) ? namespace : [namespace]; const context = Array.isArray(namespace) ? namespace : [namespace];
return context.every(namespace => this.contextNs.includes(namespace)); return context.every(namespace => this.contextNs.has(namespace));
} }
@action
toggleContext(namespace: string) { toggleContext(namespace: string) {
if (this.hasContext(namespace)) this.contextNs.remove(namespace); if (this.contextNs.has(namespace)) {
else this.contextNs.push(namespace); this.contextNs.delete(namespace)
} else {
this.contextNs.add(namespace)
}
}
@computed
get Options() {
return this.items.map(namespace => ({
value: namespace.getName(),
label: (
<div className="flex gaps align-center">
<span>{namespace.getName()}</span>
<Icon small material="check" className="box right" />
</div>
),
}))
}
@computed
get SelectedValues() {
return this.Options.filter(({ value }) => this.contextNs.has(value))
} }
@action @action

View File

@ -20,7 +20,7 @@ interface Props extends RouteComponentProps<{}> {
@observer @observer
export class Network extends React.Component<Props> { export class Network extends React.Component<Props> {
static get tabRoutes(): TabRoute[] { static get tabRoutes(): TabRoute[] {
const query = namespaceStore.getContextParams() const query = namespaceStore.contextParams
const routes: TabRoute[] = []; const routes: TabRoute[] = [];
if (isAllowedResource("services")) { if (isAllowedResource("services")) {
routes.push({ routes.push({
@ -62,8 +62,8 @@ export class Network extends React.Component<Props> {
return ( return (
<TabLayout className="Network" tabs={tabRoutes}> <TabLayout className="Network" tabs={tabRoutes}>
<Switch> <Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)} {tabRoutes.map((route, index) => <Route key={index} {...route} />)}
<Redirect to={networkURL({ query: namespaceStore.getContextParams() })}/> <Redirect to={networkURL({ query: namespaceStore.contextParams })} />
</Switch> </Switch>
</TabLayout> </TabLayout>
) )

View File

@ -20,7 +20,7 @@ interface Props extends RouteComponentProps<{}> {
export class Storage extends React.Component<Props> { export class Storage extends React.Component<Props> {
static get tabRoutes() { static get tabRoutes() {
const tabRoutes: TabRoute[] = []; const tabRoutes: TabRoute[] = [];
const query = namespaceStore.getContextParams() const query = namespaceStore.contextParams
tabRoutes.push({ tabRoutes.push({
title: <Trans>Persistent Volume Claims</Trans>, title: <Trans>Persistent Volume Claims</Trans>,
@ -54,8 +54,8 @@ export class Storage extends React.Component<Props> {
return ( return (
<TabLayout className="Storage" tabs={tabRoutes}> <TabLayout className="Storage" tabs={tabRoutes}>
<Switch> <Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)} {tabRoutes.map((route, index) => <Route key={index} {...route} />)}
<Redirect to={storageURL({ query: namespaceStore.getContextParams() })}/> <Redirect to={storageURL({ query: namespaceStore.contextParams })} />
</Switch> </Switch>
</TabLayout> </TabLayout>
) )

View File

@ -20,7 +20,7 @@ interface Props extends RouteComponentProps<{}> {
export class UserManagement extends React.Component<Props> { export class UserManagement extends React.Component<Props> {
static get tabRoutes() { static get tabRoutes() {
const tabRoutes: TabRoute[] = []; const tabRoutes: TabRoute[] = [];
const query = namespaceStore.getContextParams() const query = namespaceStore.contextParams
tabRoutes.push( tabRoutes.push(
{ {
title: <Trans>Service Accounts</Trans>, title: <Trans>Service Accounts</Trans>,
@ -57,8 +57,8 @@ export class UserManagement extends React.Component<Props> {
return ( return (
<TabLayout className="UserManagement" tabs={tabRoutes}> <TabLayout className="UserManagement" tabs={tabRoutes}>
<Switch> <Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)} {tabRoutes.map((route, index) => <Route key={index} {...route} />)}
<Redirect to={usersManagementURL({ query: namespaceStore.getContextParams() })}/> <Redirect to={usersManagementURL({ query: namespaceStore.contextParams })} />
</Switch> </Switch>
</TabLayout> </TabLayout>
) )

View File

@ -21,55 +21,55 @@ import { isAllowedResource } from "../../../common/rbac";
export class OverviewStatuses extends React.Component { export class OverviewStatuses extends React.Component {
render() { render() {
const { contextNs } = namespaceStore; const { contextNs } = namespaceStore;
const pods = isAllowedResource("pods") ? podsStore.getAllByNs(contextNs) : []; const pods = isAllowedResource("pods") ? podsStore.getAllByNs([...contextNs]) : [];
const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs(contextNs) : []; const deployments = isAllowedResource("deployments") ? deploymentStore.getAllByNs([...contextNs]) : [];
const statefulSets = isAllowedResource("statefulsets") ? statefulSetStore.getAllByNs(contextNs) : []; const statefulSets = isAllowedResource("statefulsets") ? statefulSetStore.getAllByNs([...contextNs]) : [];
const daemonSets = isAllowedResource("daemonsets") ? daemonSetStore.getAllByNs(contextNs) : []; const daemonSets = isAllowedResource("daemonsets") ? daemonSetStore.getAllByNs([...contextNs]) : [];
const jobs = isAllowedResource("jobs") ? jobStore.getAllByNs(contextNs) : []; const jobs = isAllowedResource("jobs") ? jobStore.getAllByNs([...contextNs]) : [];
const cronJobs = isAllowedResource("cronjobs") ? cronJobStore.getAllByNs(contextNs) : []; const cronJobs = isAllowedResource("cronjobs") ? cronJobStore.getAllByNs([...contextNs]) : [];
return ( return (
<div className="OverviewStatuses"> <div className="OverviewStatuses">
<div className="header flex gaps align-center"> <div className="header flex gaps align-center">
<h5 className="box grow"><Trans>Overview</Trans></h5> <h5 className="box grow"><Trans>Overview</Trans></h5>
<NamespaceSelectFilter/> <NamespaceSelectFilter />
</div> </div>
<PageFiltersList/> <PageFiltersList />
<div className="workloads"> <div className="workloads">
{isAllowedResource("pods") && {isAllowedResource("pods") &&
<div className="workload"> <div className="workload">
<div className="title"><Link to={podsURL()}><Trans>Pods</Trans> ({pods.length})</Link></div> <div className="title"><Link to={podsURL()}><Trans>Pods</Trans> ({pods.length})</Link></div>
<OverviewWorkloadStatus status={podsStore.getStatuses(pods)}/> <OverviewWorkloadStatus status={podsStore.getStatuses(pods)} />
</div> </div>
} }
{isAllowedResource("deployments") && {isAllowedResource("deployments") &&
<div className="workload"> <div className="workload">
<div className="title"><Link to={deploymentsURL()}><Trans>Deployments</Trans> ({deployments.length})</Link></div> <div className="title"><Link to={deploymentsURL()}><Trans>Deployments</Trans> ({deployments.length})</Link></div>
<OverviewWorkloadStatus status={deploymentStore.getStatuses(deployments)}/> <OverviewWorkloadStatus status={deploymentStore.getStatuses(deployments)} />
</div> </div>
} }
{isAllowedResource("statefulsets") && {isAllowedResource("statefulsets") &&
<div className="workload"> <div className="workload">
<div className="title"><Link to={statefulSetsURL()}><Trans>StatefulSets</Trans> ({statefulSets.length})</Link></div> <div className="title"><Link to={statefulSetsURL()}><Trans>StatefulSets</Trans> ({statefulSets.length})</Link></div>
<OverviewWorkloadStatus status={statefulSetStore.getStatuses(statefulSets)}/> <OverviewWorkloadStatus status={statefulSetStore.getStatuses(statefulSets)} />
</div> </div>
} }
{isAllowedResource("daemonsets") && {isAllowedResource("daemonsets") &&
<div className="workload"> <div className="workload">
<div className="title"><Link to={daemonSetsURL()}><Trans>DaemonSets</Trans> ({daemonSets.length})</Link></div> <div className="title"><Link to={daemonSetsURL()}><Trans>DaemonSets</Trans> ({daemonSets.length})</Link></div>
<OverviewWorkloadStatus status={daemonSetStore.getStatuses(daemonSets)}/> <OverviewWorkloadStatus status={daemonSetStore.getStatuses(daemonSets)} />
</div> </div>
} }
{isAllowedResource("jobs") && {isAllowedResource("jobs") &&
<div className="workload"> <div className="workload">
<div className="title"><Link to={jobsURL()}><Trans>Jobs</Trans> ({jobs.length})</Link></div> <div className="title"><Link to={jobsURL()}><Trans>Jobs</Trans> ({jobs.length})</Link></div>
<OverviewWorkloadStatus status={jobStore.getStatuses(jobs)}/> <OverviewWorkloadStatus status={jobStore.getStatuses(jobs)} />
</div> </div>
} }
{isAllowedResource("cronjobs") && {isAllowedResource("cronjobs") &&
<div className="workload"> <div className="workload">
<div className="title"><Link to={cronJobsURL()}><Trans>CronJobs</Trans> ({cronJobs.length})</Link></div> <div className="title"><Link to={cronJobsURL()}><Trans>CronJobs</Trans> ({cronJobs.length})</Link></div>
<OverviewWorkloadStatus status={cronJobStore.getStatuses(cronJobs)}/> <OverviewWorkloadStatus status={cronJobStore.getStatuses(cronJobs)} />
</div> </div>
} }
</div> </div>
</div> </div>

View File

@ -23,7 +23,7 @@ interface Props extends RouteComponentProps {
@observer @observer
export class Workloads extends React.Component<Props> { export class Workloads extends React.Component<Props> {
static get tabRoutes(): TabRoute[] { static get tabRoutes(): TabRoute[] {
const query = namespaceStore.getContextParams(); const query = namespaceStore.contextParams;
const routes: TabRoute[] = [ const routes: TabRoute[] = [
{ {
title: <Trans>Overview</Trans>, title: <Trans>Overview</Trans>,
@ -88,8 +88,8 @@ export class Workloads extends React.Component<Props> {
return ( return (
<TabLayout className="Workloads" tabs={tabRoutes}> <TabLayout className="Workloads" tabs={tabRoutes}>
<Switch> <Switch>
{tabRoutes.map((route, index) => <Route key={index} {...route}/>)} {tabRoutes.map((route, index) => <Route key={index} {...route} />)}
<Redirect to={workloadsURL({ query: namespaceStore.getContextParams() })}/> <Redirect to={workloadsURL({ query: namespaceStore.contextParams })} />
</Switch> </Switch>
</TabLayout> </TabLayout>
) )

View File

@ -30,17 +30,17 @@ export class PageFiltersStore {
protected syncWithContextNamespace() { protected syncWithContextNamespace() {
const disposers = [ const disposers = [
reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => { reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
if (filteredNs.length !== namespaceStore.contextNs.length) { if (filteredNs.length !== namespaceStore.contextNs.size) {
namespaceStore.setContext(filteredNs); namespaceStore.context = filteredNs;
} }
}), }),
reaction(() => namespaceStore.contextNs.toJS(), contextNs => { reaction(() => namespaceStore.contextNs.toJS(), contextNs => {
const filteredNs = this.getValues(FilterType.NAMESPACE); const filteredNs = this.getValues(FilterType.NAMESPACE);
const isChanged = contextNs.length !== filteredNs.length; const isChanged = contextNs.size !== filteredNs.length;
if (isChanged) { if (isChanged) {
this.filters.replace([ this.filters.replace([
...this.filters.filter(({ type }) => type !== FilterType.NAMESPACE), ...this.filters.filter(({ type }) => type !== FilterType.NAMESPACE),
...contextNs.map(ns => ({ type: FilterType.NAMESPACE, value: ns })), ...[...contextNs.values()].map(ns => ({ type: FilterType.NAMESPACE, value: ns })),
]); ]);
} }
}, { }, {

View File

@ -73,7 +73,7 @@ export class Sidebar extends React.Component<Props> {
render() { render() {
const { toggle, isPinned, className } = this.props; const { toggle, isPinned, className } = this.props;
const query = namespaceStore.getContextParams(); const query = namespaceStore.contextParams;
return ( return (
<SidebarContext.Provider value={{ pinned: isPinned }}> <SidebarContext.Provider value={{ pinned: isPinned }}>
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}> <div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>

View File

@ -3,7 +3,7 @@
html { html {
$menuBackgroundColor: $contentColor; $menuBackgroundColor: $contentColor;
$menuSelectedOptionBgc: $layoutBackground; $menuSelectedOptionBgc: $selectedBackground;
--select-menu-bgc: #{$menuBackgroundColor}; --select-menu-bgc: #{$menuBackgroundColor};
--select-menu-border-color: #{$halfGray}; --select-menu-border-color: #{$halfGray};

View File

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { createStorage, IStorageHelperOptions } from "../utils"; import { createStorage, StorageHelperOptions } from "../utils";
export function useStorage<T>(key: string, initialValue?: T, options?: IStorageHelperOptions) { export function useStorage<T>(key: string, initialValue?: T, options?: StorageHelperOptions<T>) {
const storage = createStorage(key, initialValue, options); const storage = createStorage(key, initialValue, options);
const [storageValue, setStorageValue] = useState(storage.get()); const [storageValue, setStorageValue] = useState(storage.get());
const setValue = (value: T) => { const setValue = (value: T) => {

View File

@ -16,6 +16,7 @@
"borderFaintColor": "#373a3e", "borderFaintColor": "#373a3e",
"mainBackground": "#1e2124", "mainBackground": "#1e2124",
"contentColor": "#262b2f", "contentColor": "#262b2f",
"selectedBackground": "#444444",
"layoutBackground": "#2e3136", "layoutBackground": "#2e3136",
"layoutTabsBackground": "#252729", "layoutTabsBackground": "#252729",
"layoutTabsActiveColor": "#ffffff", "layoutTabsActiveColor": "#ffffff",

View File

@ -16,6 +16,7 @@
"borderFaintColor": "#dfdfdf", "borderFaintColor": "#dfdfdf",
"mainBackground": "#f1f1f1", "mainBackground": "#f1f1f1",
"contentColor": "#ffffff", "contentColor": "#ffffff",
"selectedBackground": "#cccccc",
"layoutBackground": "#e8e8e8", "layoutBackground": "#e8e8e8",
"layoutTabsBackground": "#f8f8f8", "layoutTabsBackground": "#f8f8f8",
"layoutTabsActiveColor": "#333333", "layoutTabsActiveColor": "#333333",
@ -52,7 +53,7 @@
"helmDescriptionPreColor": "#555555", "helmDescriptionPreColor": "#555555",
"colorSuccess": "#206923", "colorSuccess": "#206923",
"colorOk": "#399c3d", "colorOk": "#399c3d",
"colorInfo": "#2d71a4", "colorInfo": "#deebff",
"colorError": "#ce3933", "colorError": "#ce3933",
"colorSoftError": "#e85555", "colorSoftError": "#e85555",
"colorWarning": "#ff9800", "colorWarning": "#ff9800",
@ -91,7 +92,7 @@
"drawerSubtitleBackground": "#f1f1f1", "drawerSubtitleBackground": "#f1f1f1",
"drawerItemNameColor": "#727272", "drawerItemNameColor": "#727272",
"drawerItemValueColor": "#555555", "drawerItemValueColor": "#555555",
"clusterMenuBackground": "#e8e8e8", "clusterMenuBackground": "#cccccc",
"clusterMenuBorderColor": "#c9cfd3", "clusterMenuBorderColor": "#c9cfd3",
"clusterSettingsBackground": "#ffffff", "clusterSettingsBackground": "#ffffff",
"addClusterIconColor": "#8d8d8d", "addClusterIconColor": "#8d8d8d",

View File

@ -123,6 +123,7 @@ $iconActiveBackground: var(--iconActiveBackground);
$filterAreaBackground: var(--filterAreaBackground); $filterAreaBackground: var(--filterAreaBackground);
$selectOptionHoveredColor: var(--selectOptionHoveredColor); $selectOptionHoveredColor: var(--selectOptionHoveredColor);
$selectedBackground: var(--selectedBackground);
$lineProgressBackground: var(--lineProgressBackground); $lineProgressBackground: var(--lineProgressBackground);
$radioActiveBackground: var(--radioActiveBackground); $radioActiveBackground: var(--radioActiveBackground);
$menuActiveBackground: var(--menuActiveBackground); $menuActiveBackground: var(--menuActiveBackground);

View File

@ -1,23 +1,27 @@
// Helper to work with browser's local/session storage api // Helper to work with browser's local/session storage api
export interface IStorageHelperOptions { export interface StorageHelperOptions<T> {
addKeyPrefix?: boolean; addKeyPrefix?: boolean;
useSession?: boolean; // use `sessionStorage` instead of `localStorage` useSession?: boolean; // use `sessionStorage` instead of `localStorage`
parse?(from: string): T;
stringify?(from: T): string;
} }
export function createStorage<T>(key: string, defaultValue?: T, options?: IStorageHelperOptions) { export function createStorage<T>(key: string, defaultValue?: T, options?: StorageHelperOptions<T>) {
return new StorageHelper(key, defaultValue, options); return new StorageHelper(key, defaultValue, options);
} }
export class StorageHelper<T> { export class StorageHelper<T> {
static keyPrefix = "lens_"; static keyPrefix = "lens_";
static defaultOptions: IStorageHelperOptions = { static defaultOptions: StorageHelperOptions<any> = {
addKeyPrefix: true, addKeyPrefix: true,
useSession: false, useSession: false,
parse: JSON.parse,
stringify: JSON.stringify,
} }
constructor(protected key: string, protected defaultValue?: T, protected options?: IStorageHelperOptions) { constructor(protected key: string, protected defaultValue?: T, protected options?: StorageHelperOptions<T>) {
this.options = Object.assign({}, StorageHelper.defaultOptions, options); this.options = Object.assign({}, StorageHelper.defaultOptions, options);
if (this.options.addKeyPrefix) { if (this.options.addKeyPrefix) {
@ -34,16 +38,16 @@ export class StorageHelper<T> {
const strValue = this.storage.getItem(this.key); const strValue = this.storage.getItem(this.key);
if (strValue != null) { if (strValue != null) {
try { try {
return JSON.parse(strValue); return this.options.parse(strValue)
} catch (e) { } catch (e) {
console.error(`Parsing json failed for pair: ${this.key}=${strValue}`) console.error(`Parsing failed for pair: ${this.key}=${strValue}`)
} }
} }
return this.defaultValue; return this.defaultValue;
} }
set(value: T) { set(value: T) {
this.storage.setItem(this.key, JSON.stringify(value)); this.storage.setItem(this.key, this.options.stringify(value));
return this; return this;
} }