Persist local-storage in external json-file (#2279)

This commit is contained in:
Roman authored and GitHub committed 2021-03-18 08:36:34 -04:00
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"
});
}
}
+45 -21
View File
@@ -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();
}
}
+67 -53
View File
@@ -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();
}
}
+1 -1
View File
@@ -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;
+20 -24
View File
@@ -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(() => {