mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
use common/base-store for config-file based stores (e.g. user-store)
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
5c8dd89a88
commit
a27ce9cf8b
124
src/common/base-store.ts
Normal file
124
src/common/base-store.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import path from "path"
|
||||||
|
import Config from "conf"
|
||||||
|
import { Options as ConfOptions } from "conf/dist/source/types"
|
||||||
|
import { app, remote } from "electron"
|
||||||
|
import { observable, reaction, toJS, when } from "mobx";
|
||||||
|
import Singleton from "./utils/singleton";
|
||||||
|
import isEqual from "lodash/isEqual"
|
||||||
|
import { getAppVersion } from "./utils/app-version";
|
||||||
|
|
||||||
|
export interface BaseStoreParams<T = any> {
|
||||||
|
configName: string;
|
||||||
|
autoLoad?: boolean;
|
||||||
|
syncEnabled?: boolean;
|
||||||
|
confOptions?: ConfOptions<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseStore<T = any> extends Singleton {
|
||||||
|
protected storeConfig: Config<T>;
|
||||||
|
protected syncDisposers: Function[] = [];
|
||||||
|
public whenLoaded = when(() => this.isLoaded);
|
||||||
|
|
||||||
|
@observable isLoaded = false;
|
||||||
|
@observable protected data: T;
|
||||||
|
|
||||||
|
protected constructor(protected params: BaseStoreParams) {
|
||||||
|
super();
|
||||||
|
this.params = {
|
||||||
|
autoLoad: true,
|
||||||
|
syncEnabled: true,
|
||||||
|
...params,
|
||||||
|
}
|
||||||
|
this.onConfigChange = this.onConfigChange.bind(this)
|
||||||
|
this.onModelChange = this.onModelChange.bind(this)
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return path.basename(this.storeConfig.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
get storeModel(): T {
|
||||||
|
const storeModel = { ...(this.storeConfig.store || {}) };
|
||||||
|
Reflect.deleteProperty(storeModel, "__internal__"); // fixme: avoid "external-internals"
|
||||||
|
return storeModel as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async init() {
|
||||||
|
if (this.params.autoLoad) {
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
if (this.params.syncEnabled) {
|
||||||
|
await this.whenLoaded;
|
||||||
|
this.enableSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async load() {
|
||||||
|
const { configName, syncEnabled, confOptions = {} } = this.params;
|
||||||
|
|
||||||
|
// use "await" to make pseudo-async "load" for more future-proof usages
|
||||||
|
this.storeConfig = await new Config({
|
||||||
|
projectName: "lens",
|
||||||
|
projectVersion: getAppVersion(),
|
||||||
|
configName: configName,
|
||||||
|
watch: syncEnabled, // watch for changes in multi-process app (e.g. main/renderer)
|
||||||
|
cwd: (app || remote.app).getPath("userData"), // todo: remove remote.app in favor ipc.invoke
|
||||||
|
...confOptions,
|
||||||
|
});
|
||||||
|
const data = this.storeConfig.store;
|
||||||
|
console.info(`[STORE]: [LOADED] ${this.storeConfig.path}`, data);
|
||||||
|
this.fromStore(data);
|
||||||
|
this.isLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
enableSync() {
|
||||||
|
const onConfigChangeStop = this.storeConfig.onDidAnyChange(this.onConfigChange);
|
||||||
|
const onModelChangeStop = reaction(() => this.toJSON(), this.onModelChange);
|
||||||
|
|
||||||
|
this.syncDisposers.push(
|
||||||
|
onConfigChangeStop, // watch for changes from file-system updates
|
||||||
|
onModelChangeStop, // refresh config file from runtime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disableSync() {
|
||||||
|
this.syncDisposers.forEach(dispose => dispose());
|
||||||
|
this.syncDisposers.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onConfigChange(data: T, oldValue: Partial<T>) {
|
||||||
|
if (!isEqual(this.toJSON(), data)) {
|
||||||
|
console.info(`[STORE]: [UPDATE] from ${this.name}`, { data, oldValue });
|
||||||
|
this.fromStore(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onModelChange(model: T) {
|
||||||
|
if (!isEqual(this.storeModel, model)) {
|
||||||
|
console.info(`[STORE]: [SAVE] ${this.name} from runtime update`, {
|
||||||
|
data: model,
|
||||||
|
oldValue: this.storeModel
|
||||||
|
});
|
||||||
|
// fixme: https://github.com/sindresorhus/conf/issues/114
|
||||||
|
Object.entries(model).forEach(([key, value]) => {
|
||||||
|
this.storeConfig.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: use "serializr" ?
|
||||||
|
protected fromStore(data: Partial<T> = {}) {
|
||||||
|
this.data = data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): T {
|
||||||
|
return toJS(this.data, {
|
||||||
|
recurseEverything: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
* [Symbol.iterator]() {
|
||||||
|
yield* Object.entries(this.toJSON());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,9 @@
|
|||||||
import path from "path"
|
|
||||||
import { app, remote } from "electron"
|
|
||||||
import { observable, reaction, toJS } from "mobx";
|
|
||||||
import Config from "conf"
|
|
||||||
import semver from "semver"
|
import semver from "semver"
|
||||||
|
import { observable, reaction, toJS } from "mobx";
|
||||||
|
import { BaseStore } from "./base-store";
|
||||||
import migrations from "../migrations/user-store"
|
import migrations from "../migrations/user-store"
|
||||||
import Singleton from "./utils/singleton";
|
|
||||||
import { getAppVersion } from "./utils/app-version";
|
import { getAppVersion } from "./utils/app-version";
|
||||||
import { tracker } from "./tracker";
|
import { tracker } from "./tracker";
|
||||||
import isEqual from "lodash/isEqual"
|
|
||||||
|
|
||||||
export interface UserStoreModel {
|
export interface UserStoreModel {
|
||||||
lastSeenAppVersion: string;
|
lastSeenAppVersion: string;
|
||||||
@ -23,10 +19,7 @@ export interface UserPreferences {
|
|||||||
downloadMirror?: string | "default";
|
downloadMirror?: string | "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserStore extends Singleton {
|
export class UserStore extends BaseStore<UserStoreModel> {
|
||||||
private storeConfig: Config<UserStoreModel>;
|
|
||||||
|
|
||||||
@observable isReady = false;
|
|
||||||
@observable lastSeenAppVersion = "0.0.0"
|
@observable lastSeenAppVersion = "0.0.0"
|
||||||
@observable seenContexts = observable.set();
|
@observable seenContexts = observable.set();
|
||||||
|
|
||||||
@ -36,67 +29,11 @@ export class UserStore extends Singleton {
|
|||||||
downloadMirror: "default",
|
downloadMirror: "default",
|
||||||
};
|
};
|
||||||
|
|
||||||
get name() {
|
protected constructor() {
|
||||||
return path.basename(this.storeConfig.path);
|
super({
|
||||||
}
|
|
||||||
|
|
||||||
get hasNewAppVersion() {
|
|
||||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
get storeModel() {
|
|
||||||
const storeModel = { ...this.storeConfig.store };
|
|
||||||
Reflect.deleteProperty(storeModel, "__internal__"); // fixme: avoid "external-internals"
|
|
||||||
return storeModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLastSeenAppVersion() {
|
|
||||||
this.lastSeenAppVersion = getAppVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
super();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async init() {
|
|
||||||
await this.load();
|
|
||||||
this.bindEvents();
|
|
||||||
this.isReady = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async load() {
|
|
||||||
this.storeConfig = new Config<UserStoreModel>({
|
|
||||||
configName: "lens-user-store",
|
configName: "lens-user-store",
|
||||||
migrations: migrations,
|
confOptions: {
|
||||||
cwd: (app || remote.app).getPath("userData"), // todo: move to main process + with ipc.invoke
|
migrations: migrations
|
||||||
watch: true, // enable onDidChange()-callback
|
|
||||||
});
|
|
||||||
const data = this.storeConfig.store;
|
|
||||||
console.info(`[STORE]: [LOADED] ${this.storeConfig.path}`, data);
|
|
||||||
this.fromStore(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected bindEvents() {
|
|
||||||
// refresh from file-system updates
|
|
||||||
this.storeConfig.onDidAnyChange((data, oldValue) => {
|
|
||||||
if (!isEqual(this.toJSON(), data)) {
|
|
||||||
console.info(`[STORE]: [UPDATE] from ${this.name}`, { data, oldValue });
|
|
||||||
this.fromStore(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// refresh config file from runtime
|
|
||||||
reaction(() => this.toJSON(), (model: UserStoreModel) => {
|
|
||||||
if (!isEqual(this.storeModel, model)) {
|
|
||||||
console.info(`[STORE]: [SAVE] ${this.name} from runtime update`, {
|
|
||||||
data: model,
|
|
||||||
oldValue: this.storeModel
|
|
||||||
});
|
|
||||||
// fixme: https://github.com/sindresorhus/conf/issues/114
|
|
||||||
Object.entries(model).forEach(([key, value]) => {
|
|
||||||
this.storeConfig.set(key, value);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -106,7 +43,14 @@ export class UserStore extends Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: maybe use "serializr"
|
get hasNewAppVersion() {
|
||||||
|
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLastSeenAppVersion() {
|
||||||
|
this.lastSeenAppVersion = getAppVersion();
|
||||||
|
}
|
||||||
|
|
||||||
protected fromStore(data: Partial<UserStoreModel> = {}) {
|
protected fromStore(data: Partial<UserStoreModel> = {}) {
|
||||||
const { lastSeenAppVersion, seenContexts, preferences } = data
|
const { lastSeenAppVersion, seenContexts, preferences } = data
|
||||||
if (lastSeenAppVersion) {
|
if (lastSeenAppVersion) {
|
||||||
@ -120,7 +64,7 @@ export class UserStore extends Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toJSON(): UserStoreModel {
|
toJSON() {
|
||||||
return toJS({
|
return toJS({
|
||||||
lastSeenAppVersion: this.lastSeenAppVersion,
|
lastSeenAppVersion: this.lastSeenAppVersion,
|
||||||
seenContexts: Array.from(this.seenContexts),
|
seenContexts: Array.from(this.seenContexts),
|
||||||
@ -131,4 +75,5 @@ export class UserStore extends Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userStore: UserStore = UserStore.getInstance();
|
const userStore: UserStore = UserStore.getInstance();
|
||||||
|
export { userStore }
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
* const usersStore: UsersStore = UsersStore.getInstance();
|
* const usersStore: UsersStore = UsersStore.getInstance();
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// todo: maybe convert to @decorator
|
|
||||||
class Singleton {
|
class Singleton {
|
||||||
private static instances = new WeakMap<object, Singleton>();
|
private static instances = new WeakMap<object, Singleton>();
|
||||||
|
|
||||||
|
|||||||
@ -48,10 +48,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toLanding: async function() {
|
toLanding: async function() {
|
||||||
if(userStore.hasNewAppVersion) {
|
userStore.saveLastSeenAppVersion();
|
||||||
userStore.saveLastSeenAppVersion();
|
tracker.event("app", "whats-new-seen")
|
||||||
tracker.event("app", "whats-new-seen")
|
|
||||||
}
|
|
||||||
// await this.$store.dispatch("updateLastSeenAppVersion")
|
// await this.$store.dispatch("updateLastSeenAppVersion")
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: "landing-page",
|
name: "landing-page",
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import App from './App'
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
import { userStore } from "../../common/user-store"
|
import { userStore } from "../../common/user-store"
|
||||||
import { when } from "mobx"
|
|
||||||
|
|
||||||
const promiseIpc = new PromiseIpc({maxTimeoutMs: 6000});
|
const promiseIpc = new PromiseIpc({maxTimeoutMs: 6000});
|
||||||
|
|
||||||
@ -30,10 +29,11 @@ Vue.mixin({
|
|||||||
|
|
||||||
// any initialization we want to do for app state
|
// any initialization we want to do for app state
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await when(() => userStore.isReady);
|
await userStore.whenLoaded;
|
||||||
await store.dispatch('init')
|
await store.dispatch('init')
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
components: { App },
|
components: {App},
|
||||||
store,
|
store,
|
||||||
router,
|
router,
|
||||||
template: '<App/>'
|
template: '<App/>'
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import semver from "semver"
|
|
||||||
import { userStore } from "../../../common/user-store"
|
import { userStore } from "../../../common/user-store"
|
||||||
import { getAppVersion } from "../../../common/utils/app-version"
|
import { getAppVersion } from "../../../common/utils/app-version"
|
||||||
import KubeContexts from './modules/kube-contexts'
|
import KubeContexts from './modules/kube-contexts'
|
||||||
@ -72,10 +71,11 @@ export default new Vuex.Store({
|
|||||||
seenContexts: state => state.seenContexts,
|
seenContexts: state => state.seenContexts,
|
||||||
hud: state => state.hud,
|
hud: state => state.hud,
|
||||||
isMenuVisible: function (state, getters) {
|
isMenuVisible: function (state, getters) {
|
||||||
return state.hud.isMenuVisible && !getters.showWhatsNew;
|
if (userStore.hasNewAppVersion) return false;
|
||||||
|
return state.hud.isMenuVisible;
|
||||||
},
|
},
|
||||||
showWhatsNew: function (state) {
|
showWhatsNew: function (state) {
|
||||||
return semver.gt(getAppVersion(), state.lastSeenAppVersion);
|
return userStore.hasNewAppVersion;
|
||||||
},
|
},
|
||||||
preferences: state => state.preferences,
|
preferences: state => state.preferences,
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user