mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Persist local-storage in external json-file (#2279)
This commit is contained in:
1 parent
da5a4bbdf4
commit
aedcc6d70e
32 files changed
+1025
-618
No files matched your search
@@ -4,7 +4,6 @@ import "@testing-library/jest-dom/extend-expect";
|
||||
|
||||
import { DockTabs } from "../dock-tabs";
|
||||
import { dockStore, IDockTab, TabKind } from "../dock.store";
|
||||
import { observable } from "mobx";
|
||||
|
||||
const onChangeTab = jest.fn();
|
||||
|
||||
@@ -134,9 +133,9 @@ describe("<DockTabs />", () => {
|
||||
});
|
||||
|
||||
it("disables 'Close All' & 'Close Other' items if only 1 tab available", () => {
|
||||
dockStore.tabs = observable.array<IDockTab>([{
|
||||
dockStore.tabs = [{
|
||||
id: "terminal", kind: TabKind.TERMINAL, title: "Terminal"
|
||||
}]);
|
||||
}];
|
||||
const { container, getByText } = renderTabs();
|
||||
const tab = container.querySelector(".Tab");
|
||||
|
||||
@@ -149,10 +148,10 @@ describe("<DockTabs />", () => {
|
||||
});
|
||||
|
||||
it("disables 'Close To The Right' item if last tab clicked", () => {
|
||||
dockStore.tabs = observable.array<IDockTab>([
|
||||
dockStore.tabs = [
|
||||
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal" },
|
||||
{ id: "logs", kind: TabKind.POD_LOGS, title: "Pod Logs" },
|
||||
]);
|
||||
];
|
||||
const { container, getByText } = renderTabs();
|
||||
const tab = container.querySelectorAll(".Tab")[1];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { dockStore, IDockTab, TabKind } from "./dock.store";
|
||||
export class CreateResourceStore extends DockTabStore<string> {
|
||||
constructor() {
|
||||
super({
|
||||
storageName: "create_resource"
|
||||
storageKey: "create_resource"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
import { autorun, observable, reaction } from "mobx";
|
||||
import { autobind, createStorage } from "../../utils";
|
||||
import { autorun, observable, reaction, toJS } from "mobx";
|
||||
import { autobind, createStorage, StorageHelper } from "../../utils";
|
||||
import { dockStore, TabId } from "./dock.store";
|
||||
|
||||
interface Options<T = any> {
|
||||
storageName?: string; // name to sync data with localStorage
|
||||
storageSerializer?: (data: T) => Partial<T>; // allow to customize data before saving to localStorage
|
||||
export interface DockTabStoreOptions {
|
||||
autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true
|
||||
storageKey?: string; // save data to persistent storage under the key
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class DockTabStore<T = any> {
|
||||
protected data = observable.map<TabId, T>([]);
|
||||
export type DockTabStorageState<T> = Record<TabId, T>;
|
||||
|
||||
constructor(protected options: Options<T> = {}) {
|
||||
const { storageName } = options;
|
||||
@autobind()
|
||||
export class DockTabStore<T> {
|
||||
protected storage?: StorageHelper<DockTabStorageState<T>>;
|
||||
protected data = observable.map<TabId, T>();
|
||||
|
||||
constructor(protected options: DockTabStoreOptions = {}) {
|
||||
this.options = {
|
||||
autoInit: true,
|
||||
...this.options,
|
||||
};
|
||||
|
||||
if (this.options.autoInit) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
protected init() {
|
||||
const { storageKey } = this.options;
|
||||
|
||||
// auto-save to local-storage
|
||||
if (storageName) {
|
||||
const storage = createStorage<[TabId, T][]>(storageName, []);
|
||||
|
||||
this.data.replace(storage.get());
|
||||
reaction(() => this.serializeData(), (data: T | any) => storage.set(data));
|
||||
if (storageKey) {
|
||||
this.storage = createStorage(storageKey, {});
|
||||
this.storage.whenReady.then(() => {
|
||||
this.data.replace(this.storage.get());
|
||||
reaction(() => this.getStorableData(), data => this.storage.set(data));
|
||||
});
|
||||
}
|
||||
|
||||
// clear data for closed tabs
|
||||
@@ -34,14 +49,22 @@ export class DockTabStore<T = any> {
|
||||
});
|
||||
}
|
||||
|
||||
protected serializeData() {
|
||||
const { storageSerializer } = this.options;
|
||||
protected finalizeDataForSave(data: T): T {
|
||||
return data;
|
||||
}
|
||||
|
||||
return Array.from(this.data).map(([tabId, tabData]) => {
|
||||
if (storageSerializer) return [tabId, storageSerializer(tabData)];
|
||||
protected getStorableData(): DockTabStorageState<T> {
|
||||
const allTabsData = toJS(this.data, { recurseEverything: true });
|
||||
|
||||
return [tabId, tabData];
|
||||
});
|
||||
return Object.fromEntries(
|
||||
Object.entries(allTabsData).map(([tabId, tabData]) => {
|
||||
return [tabId, this.finalizeDataForSave(tabData)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
isReady(tabId: TabId): boolean {
|
||||
return Boolean(this.getData(tabId) !== undefined);
|
||||
}
|
||||
|
||||
getData(tabId: TabId) {
|
||||
@@ -58,5 +81,6 @@ export class DockTabStore<T = any> {
|
||||
|
||||
reset() {
|
||||
this.data.clear();
|
||||
this.storage?.clear();
|
||||
}
|
||||
}
|
||||
@@ -21,28 +21,72 @@ export interface IDockTab {
|
||||
pinned?: boolean; // not closable
|
||||
}
|
||||
|
||||
export interface DockStorageState {
|
||||
height: number;
|
||||
tabs: IDockTab[];
|
||||
selectedTabId?: TabId;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class DockStore {
|
||||
protected initialTabs: IDockTab[] = [
|
||||
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal" },
|
||||
];
|
||||
|
||||
protected storage = createStorage("dock", {}); // keep settings in localStorage
|
||||
public readonly defaultTabId = this.initialTabs[0].id;
|
||||
public readonly minHeight = 100;
|
||||
|
||||
@observable isOpen = false;
|
||||
export class DockStore implements DockStorageState {
|
||||
readonly minHeight = 100;
|
||||
@observable fullSize = false;
|
||||
@observable height = this.defaultHeight;
|
||||
@observable tabs = observable.array<IDockTab>(this.initialTabs);
|
||||
@observable selectedTabId = this.defaultTabId;
|
||||
|
||||
private storage = createStorage<DockStorageState>("dock", {
|
||||
height: 300,
|
||||
tabs: [
|
||||
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal" },
|
||||
],
|
||||
});
|
||||
|
||||
get isOpen(): boolean {
|
||||
return this.storage.get().isOpen;
|
||||
}
|
||||
|
||||
set isOpen(isOpen: boolean) {
|
||||
this.storage.merge({ isOpen });
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return this.storage.get().height;
|
||||
}
|
||||
|
||||
set height(height: number) {
|
||||
this.storage.merge({
|
||||
height: Math.max(this.minHeight, Math.min(height || this.minHeight, this.maxHeight)),
|
||||
});
|
||||
}
|
||||
|
||||
get tabs(): IDockTab[] {
|
||||
return this.storage.get().tabs;
|
||||
}
|
||||
|
||||
set tabs(tabs: IDockTab[]) {
|
||||
this.storage.merge({ tabs });
|
||||
}
|
||||
|
||||
get selectedTabId(): TabId | undefined {
|
||||
return this.storage.get().selectedTabId || this.tabs[0]?.id;
|
||||
}
|
||||
|
||||
set selectedTabId(tabId: TabId) {
|
||||
if (tabId && !this.getTabById(tabId)) return; // skip invalid ids
|
||||
|
||||
this.storage.merge({ selectedTabId: tabId });
|
||||
}
|
||||
|
||||
@computed get selectedTab() {
|
||||
return this.tabs.find(tab => tab.id === this.selectedTabId);
|
||||
}
|
||||
|
||||
get defaultHeight() {
|
||||
return Math.round(window.innerHeight / 2.5);
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
// adjust terminal height if window size changes
|
||||
window.addEventListener("resize", throttle(this.adjustHeight, 250));
|
||||
}
|
||||
|
||||
get maxHeight() {
|
||||
@@ -50,35 +94,14 @@ export class DockStore {
|
||||
const mainLayoutTabs = 33;
|
||||
const mainLayoutMargin = 16;
|
||||
const dockTabs = 33;
|
||||
const preferedMax = window.innerHeight - mainLayoutHeader - mainLayoutTabs - mainLayoutMargin - dockTabs;
|
||||
const preferredMax = window.innerHeight - mainLayoutHeader - mainLayoutTabs - mainLayoutMargin - dockTabs;
|
||||
|
||||
return Math.max(preferedMax, this.minHeight); // don't let max < min
|
||||
return Math.max(preferredMax, this.minHeight); // don't let max < min
|
||||
}
|
||||
|
||||
constructor() {
|
||||
Object.assign(this, this.storage.get());
|
||||
|
||||
reaction(() => ({
|
||||
isOpen: this.isOpen,
|
||||
selectedTabId: this.selectedTabId,
|
||||
height: this.height,
|
||||
tabs: this.tabs.slice(),
|
||||
}), data => {
|
||||
this.storage.set(data);
|
||||
});
|
||||
|
||||
// adjust terminal height if window size changes
|
||||
window.addEventListener("resize", throttle(this.checkMaxHeight, 250));
|
||||
}
|
||||
|
||||
protected checkMaxHeight() {
|
||||
if (!this.height) {
|
||||
this.setHeight(this.defaultHeight || this.minHeight);
|
||||
}
|
||||
|
||||
if (this.height > this.maxHeight) {
|
||||
this.setHeight(this.maxHeight);
|
||||
}
|
||||
protected adjustHeight() {
|
||||
if (this.height < this.minHeight) this.height = this.minHeight;
|
||||
if (this.height > this.maxHeight) this.height = this.maxHeight;
|
||||
}
|
||||
|
||||
onResize(callback: () => void, options?: IReactionOptions) {
|
||||
@@ -165,7 +188,7 @@ export class DockStore {
|
||||
if (!tab || tab.pinned) {
|
||||
return;
|
||||
}
|
||||
this.tabs.remove(tab);
|
||||
this.tabs = this.tabs.filter(tab => tab.id !== tabId);
|
||||
|
||||
if (this.selectedTabId === tab.id) {
|
||||
if (this.tabs.length) {
|
||||
@@ -178,8 +201,7 @@ export class DockStore {
|
||||
if (!terminalStore.isConnected(newTab.id)) this.close();
|
||||
}
|
||||
this.selectTab(newTab.id);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.selectedTabId = null;
|
||||
this.close();
|
||||
}
|
||||
@@ -219,17 +241,9 @@ export class DockStore {
|
||||
this.selectedTabId = this.getTabById(tabId)?.id ?? null;
|
||||
}
|
||||
|
||||
@action
|
||||
setHeight(height?: number) {
|
||||
this.height = Math.max(this.minHeight, Math.min(height || this.minHeight, this.maxHeight));
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.selectedTabId = this.defaultTabId;
|
||||
this.tabs.replace(this.initialTabs);
|
||||
this.setHeight(this.defaultHeight);
|
||||
this.close();
|
||||
this.storage?.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class Dock extends React.Component<Props> {
|
||||
onStart={dockStore.open}
|
||||
onMinExtentSubceed={dockStore.close}
|
||||
onMinExtentExceed={dockStore.open}
|
||||
onDrag={dockStore.setHeight}
|
||||
onDrag={extent => dockStore.height = extent}
|
||||
/>
|
||||
<div className="tabs-container flex align-center" onDoubleClick={prevDefault(toggle)}>
|
||||
<DockTabs
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import { autobind, noop } from "../../utils";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { autorun, IReactionDisposer } from "mobx";
|
||||
import { dockStore, IDockTab, TabKind } from "./dock.store";
|
||||
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
|
||||
export interface KubeEditResource {
|
||||
export interface EditingResource {
|
||||
resource: string; // resource path, e.g. /api/v1/namespaces/default
|
||||
draft?: string; // edited draft in yaml
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class EditResourceStore extends DockTabStore<KubeEditResource> {
|
||||
private watchers = new Map<string /*tabId*/, IReactionDisposer>();
|
||||
export class EditResourceStore extends DockTabStore<EditingResource> {
|
||||
private watchers = new Map<TabId, IReactionDisposer>();
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
storageName: "edit_resource_store",
|
||||
storageSerializer: ({ draft, ...data }) => data, // skip saving draft in local-storage
|
||||
storageKey: "edit_resource_store",
|
||||
});
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
super.init();
|
||||
await this.storage.whenReady;
|
||||
|
||||
autorun(() => {
|
||||
Array.from(this.data).forEach(([tabId, { resource }]) => {
|
||||
@@ -42,12 +47,34 @@ export class EditResourceStore extends DockTabStore<KubeEditResource> {
|
||||
}
|
||||
}
|
||||
}, {
|
||||
delay: 100 // make sure all stores initialized
|
||||
delay: 100 // make sure all kube-object stores are initialized
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected finalizeDataForSave({ draft, ...data }: EditingResource): EditingResource {
|
||||
return data; // skip saving draft to local-storage
|
||||
}
|
||||
|
||||
isReady(tabId: TabId) {
|
||||
const tabDataReady = super.isReady(tabId);
|
||||
|
||||
return Boolean(tabDataReady && this.getResource(tabId)); // ready to edit resource
|
||||
}
|
||||
|
||||
getStore(tabId: TabId): KubeObjectStore | undefined {
|
||||
return apiManager.getStore(this.getResourcePath(tabId));
|
||||
}
|
||||
|
||||
getResource(tabId: TabId): KubeObject | undefined {
|
||||
return this.getStore(tabId)?.getByPath(this.getResourcePath(tabId));
|
||||
}
|
||||
|
||||
getResourcePath(tabId: TabId): string | undefined {
|
||||
return this.getData(tabId)?.resource;
|
||||
}
|
||||
|
||||
getTabByResource(object: KubeObject): IDockTab {
|
||||
const [tabId] = Array.from(this.data).find(([, { resource }]) => {
|
||||
return object.selfLink === resource;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import "./edit-resource.scss";
|
||||
|
||||
import React from "react";
|
||||
import { autorun, observable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observable, when } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import jsYaml from "js-yaml";
|
||||
import { IDockTab } from "./dock.store";
|
||||
import { cssNames } from "../../utils";
|
||||
@@ -11,8 +11,6 @@ import { InfoPanel } from "./info-panel";
|
||||
import { Badge } from "../badge";
|
||||
import { EditorPanel } from "./editor-panel";
|
||||
import { Spinner } from "../spinner";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -23,30 +21,28 @@ interface Props {
|
||||
export class EditResource extends React.Component<Props> {
|
||||
@observable error = "";
|
||||
|
||||
@disposeOnUnmount
|
||||
autoDumpResourceOnInit = autorun(() => {
|
||||
if (!this.tabData) return;
|
||||
async componentDidMount() {
|
||||
await when(() => this.isReady);
|
||||
|
||||
if (this.tabData.draft === undefined && this.resource) {
|
||||
this.saveDraft(this.resource);
|
||||
if (!this.tabData.draft) {
|
||||
this.saveDraft(this.resource); // make initial dump to editor
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return editResourceStore.isReady(this.tabId);
|
||||
}
|
||||
|
||||
get tabData() {
|
||||
return editResourceStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
get resource(): KubeObject {
|
||||
const { resource } = this.tabData;
|
||||
const store = apiManager.getStore(resource);
|
||||
|
||||
if (store) {
|
||||
return store.getByPath(resource);
|
||||
}
|
||||
get resource() {
|
||||
return editResourceStore.getResource(this.tabId);
|
||||
}
|
||||
|
||||
saveDraft(draft: string | object) {
|
||||
@@ -68,8 +64,8 @@ export class EditResource extends React.Component<Props> {
|
||||
if (this.error) {
|
||||
return;
|
||||
}
|
||||
const { resource, draft } = this.tabData;
|
||||
const store = apiManager.getStore(resource);
|
||||
const { draft } = this.tabData;
|
||||
const store = editResourceStore.getStore(this.tabId);
|
||||
const updatedResource = await store.update(this.resource, jsYaml.safeLoad(draft));
|
||||
|
||||
this.saveDraft(updatedResource); // update with new resourceVersion to avoid further errors on save
|
||||
@@ -84,13 +80,13 @@ export class EditResource extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tabId, resource, tabData, error, onChange, save } = this;
|
||||
const { draft } = tabData;
|
||||
|
||||
if (!resource || draft === undefined) {
|
||||
if (!this.isReady) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
const { kind, getNs, getName } = resource;
|
||||
|
||||
const { tabId, error, onChange, save } = this;
|
||||
const { kind, getNs, getName } = this.resource;
|
||||
const { draft } = this.tabData;
|
||||
|
||||
return (
|
||||
<div className={cssNames("EditResource flex column", this.props.className)}>
|
||||
|
||||
@@ -22,7 +22,7 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
storageName: "install_charts"
|
||||
storageKey: "install_charts"
|
||||
});
|
||||
autorun(() => {
|
||||
const { selectedTab, isOpen } = dockStore;
|
||||
|
||||
@@ -27,7 +27,7 @@ interface WorkloadLogsTabData {
|
||||
export class LogTabStore extends DockTabStore<LogTabData> {
|
||||
constructor() {
|
||||
super({
|
||||
storageName: "pod_logs"
|
||||
storageKey: "pod_logs"
|
||||
});
|
||||
|
||||
reaction(() => podsStore.items.length, () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
storageName: "chart_releases"
|
||||
storageKey: "chart_releases"
|
||||
});
|
||||
|
||||
autorun(() => {
|
||||
|
||||
Reference in new issue
Block a user