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

stores sync fixes, refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-14 19:33:47 +03:00
parent b8eaa657a2
commit a305b04292
14 changed files with 91 additions and 81 deletions

View File

@ -202,6 +202,7 @@
"uuid": "^8.1.0", "uuid": "^8.1.0",
"win-ca": "^3.2.0", "win-ca": "^3.2.0",
"winston": "^3.2.1", "winston": "^3.2.1",
"winston-transport-browserconsole": "^1.0.5",
"ws": "^7.3.0" "ws": "^7.3.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,24 +2,24 @@ import path from "path"
import Config from "conf" import Config from "conf"
import { Options as ConfOptions } from "conf/dist/source/types" import { Options as ConfOptions } from "conf/dist/source/types"
import produce from "immer"; import produce from "immer";
import { app, remote } from "electron" import { app, ipcMain, ipcRenderer, remote } from "electron"
import { action, observable, reaction, toJS, when } from "mobx"; import { action, observable, reaction, toJS, when } from "mobx";
import Singleton from "./utils/singleton"; import Singleton from "./utils/singleton";
import isEqual from "lodash/isEqual" import isEqual from "lodash/isEqual"
import { getAppVersion } from "./utils/app-version"; import { getAppVersion } from "./utils/app-version";
import logger from "../main/logger"; import logger from "../main/logger";
import { broadcastMessage } from "./ipc-helpers";
export interface BaseStoreParams<T = any> { export interface BaseStoreParams<T = any> extends ConfOptions<T> {
configName: string;
autoLoad?: boolean; autoLoad?: boolean;
syncEnabled?: boolean; syncEnabled?: boolean;
confOptions?: ConfOptions<T>;
} }
export class BaseStore<T = any> extends Singleton { export class BaseStore<T = any> extends Singleton {
protected storeConfig: Config<T>; protected storeConfig: Config<T>;
protected syncDisposers: Function[] = []; protected syncDisposers: Function[] = [];
whenLoaded = when(() => this.isLoaded);
@observable isLoaded = false; @observable isLoaded = false;
@observable protected data: T; @observable protected data: T;
@ -30,8 +30,6 @@ export class BaseStore<T = any> extends Singleton {
syncEnabled: true, syncEnabled: true,
...params, ...params,
} }
this.onConfigChange = this.onConfigChange.bind(this)
this.onModelChange = this.onModelChange.bind(this)
this.init(); this.init();
} }
@ -39,10 +37,8 @@ export class BaseStore<T = any> extends Singleton {
return path.basename(this.storeConfig.path); return path.basename(this.storeConfig.path);
} }
get storeModel(): T { get syncEvent() {
const storeModel = { ...(this.storeConfig.store || {}) }; return `[STORE]:[SYNC]:${this.name}`
Reflect.deleteProperty(storeModel, "__internal__"); // fixme: avoid "external-internals"
return storeModel as T;
} }
protected async init() { protected async init() {
@ -50,39 +46,46 @@ export class BaseStore<T = any> extends Singleton {
await this.load(); await this.load();
} }
if (this.params.syncEnabled) { if (this.params.syncEnabled) {
await when(() => this.isLoaded); await this.whenLoaded;
this.enableSync(); this.enableSync();
} }
} }
async load() { async load() {
const { configName, syncEnabled, confOptions = {} } = this.params; const { autoLoad, syncEnabled, ...confOptions } = this.params;
this.storeConfig = new Config({
// use "await" to make pseudo-async "load" for more future-proof use-cases ...confOptions,
this.storeConfig = await new Config({
projectName: "lens", projectName: "lens",
projectVersion: getAppVersion(), projectVersion: getAppVersion(),
configName: configName,
watch: syncEnabled, // watch for changes in multi-process app (e.g. main/renderer)
get cwd() { get cwd() {
return (app || remote.app).getPath("userData"); return (app || remote.app).getPath("userData");
}, },
...confOptions,
}); });
const jsonModel = this.storeConfig.store; const storeModel = Object.assign({}, this.storeConfig.store);
logger.info(`💿 Store loaded from ${this.storeConfig.path}`); Reflect.deleteProperty(storeModel, "__internal__"); // fixme: avoid "external-internals"
this.fromStore(jsonModel); logger.info(`[STORE]: loaded ${this.storeConfig.path}`);
this.fromStore(storeModel);
this.isLoaded = true; this.isLoaded = true;
} }
enableSync() { enableSync() {
const onConfigChangeStop = this.storeConfig.onDidAnyChange(this.onConfigChange);
const onModelChangeStop = reaction(() => this.toJSON(), this.onModelChange);
this.syncDisposers.push( this.syncDisposers.push(
onConfigChangeStop, // watch for changes from file-system updates reaction(() => this.toJSON(), this.onModelChange.bind(this)),
onModelChangeStop, // refresh config file from runtime
); );
if (ipcMain) {
ipcMain.on(this.syncEvent, (event, model: T) => {
logger.info(`[STORE]: ${this.name} sync update from renderer`, model);
this.onSync(model);
});
this.syncDisposers.push(() => ipcMain.removeAllListeners(this.syncEvent));
}
if (ipcRenderer) {
ipcRenderer.on(this.syncEvent, (event, model: T) => {
logger.info(`[STORE]: ${this.name} sync update from main`, model);
this.onSync(model);
});
this.syncDisposers.push(() => ipcRenderer.removeAllListeners(this.syncEvent));
}
} }
disableSync() { disableSync() {
@ -90,24 +93,26 @@ export class BaseStore<T = any> extends Singleton {
this.syncDisposers.length = 0; this.syncDisposers.length = 0;
} }
protected onConfigChange(data: T, oldValue: Partial<T>) { protected onSync(model: T) {
if (!isEqual(this.toJSON(), data)) { if (!isEqual(this.toJSON(), model)) {
logger.info(`💿 Store received update from ${this.name}`, { data, oldValue }); logger.info(`[STORE]: ${this.name} received update from main`, model);
this.fromStore(data); this.fromStore(model);
} }
} }
protected onModelChange(model: T) { protected async onModelChange(model: T) {
if (!isEqual(this.storeModel, model)) { // update views and save to config file
logger.info(`💿 Store ${this.name} is saving updates from app runtime`, { if (ipcMain) {
data: model, broadcastMessage(this.syncEvent, model);
oldValue: this.storeModel
});
// fixme: https://github.com/sindresorhus/conf/issues/114 // fixme: https://github.com/sindresorhus/conf/issues/114
Object.entries(model).forEach(([key, value]) => { Object.entries(model).forEach(([key, value]) => {
this.storeConfig.set(key, value); this.storeConfig.set(key, value);
}); });
} }
// sends "update-request" event to main-process
if (ipcRenderer) {
ipcRenderer.send(this.syncEvent, model);
}
} }
@action @action

View File

@ -42,10 +42,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
private constructor() { private constructor() {
super({ super({
configName: "lens-cluster-store", configName: "lens-cluster-store",
confOptions: { accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
migrations: migrations, migrations: migrations,
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
}
}); });
} }

View File

@ -11,26 +11,26 @@ export interface IpcMessageOptions {
timeout?: number; timeout?: number;
} }
export interface IpcMessageHandler { export interface IpcMessageHandler<T extends any[] = any> {
(...args: any[]): any; (...args: T): any;
} }
export function sendMessage(channel: IpcChannel, ...args: any[]) { export function broadcastMessage(channel: IpcChannel, ...args: any[]) {
const webContent = webContents.getFocusedWebContents(); webContents.getAllWebContents().forEach(webContent => {
if (webContent) {
webContent.send(channel, ...args); webContent.send(channel, ...args);
} })
} }
export async function invokeMessage(channel: IpcChannel, ...args: any[]) { export async function invokeMessage<T = any>(channel: IpcChannel, ...args: any[]): Promise<T> {
logger.debug(`[IPC]: invoke channel "${channel}"`, { args }); logger.info(`[IPC]: invoke channel "${channel}"`, { args });
return ipcRenderer.invoke(channel, ...args); return ipcRenderer.invoke(channel, ...args);
} }
export function handleMessage(channel: IpcChannel, handler: IpcMessageHandler, options: IpcMessageOptions = {}) { // todo: make isomorphic api
export function handleMessage<T extends any[]>(channel: IpcChannel, handler: IpcMessageHandler<T>, options: IpcMessageOptions = {}) {
const { timeout = 0 } = options; const { timeout = 0 } = options;
ipcMain.handle(channel, async (event, ...args: any[]) => { ipcMain.handle(channel, async (event, ...args: T) => {
logger.debug(`[IPC]: handle "${channel}"`, { event, args }); logger.info(`[IPC]: handle "${channel}"`, { event, args });
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let timerId; let timerId;
if (timeout) { if (timeout) {

View File

@ -1,11 +1,12 @@
// IPC messages (all channels) // IPC messages (all channels)
// All values must be unique
export enum ClusterIpcMessage { export enum ClusterIpcMessage {
CLUSTER_ADD = "cluster-add", ADD = "cluster-add",
CLUSTER_STOP = "cluster-stop", STOP = "cluster-stop",
CLUSTER_REMOVE = "cluster-remove", REFRESH = "cluster-refresh",
CLUSTER_REMOVE_WORKSPACE = "cluster-remove-all-from-workspace", REMOVE = "cluster-remove",
CLUSTER_REFRESH = "cluster-refresh", REMOVE_WORKSPACE = "cluster-remove-all-from-workspace",
FEATURE_INSTALL = "cluster-feature-install", FEATURE_INSTALL = "cluster-feature-install",
FEATURE_UPGRADE = "cluster-feature-upgrade", FEATURE_UPGRADE = "cluster-feature-upgrade",
FEATURE_REMOVE = "cluster-feature-remove", FEATURE_REMOVE = "cluster-feature-remove",

View File

@ -23,9 +23,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
private constructor() { private constructor() {
super({ super({
configName: "lens-user-store", configName: "lens-user-store",
confOptions: { migrations: migrations,
migrations: migrations
}
}); });
// track telemetry availability // track telemetry availability

View File

@ -1,6 +1,6 @@
// Create random filename // Create random system name
export function randomFileName({ prefix = "", suffix = "", sep = "__" } = {}) { export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) {
const randId = () => Math.random().toString(16).substr(2); const randId = () => Math.random().toString(16).substr(2);
return [prefix, randId(), suffix].filter(s => s).join(sep); return [prefix, randId(), suffix].filter(s => s).join(sep);
} }

View File

@ -3,5 +3,5 @@
export * from "./base64" export * from "./base64"
export * from "./camelCase" export * from "./camelCase"
export * from "./splitArray" export * from "./splitArray"
export * from "./randomFileName" export * from "./getRandId"
export * from "./cloneJson" export * from "./cloneJson"

View File

@ -160,11 +160,11 @@ export class ClusterManager {
static ipcListen(clusterManager: ClusterManager) { static ipcListen(clusterManager: ClusterManager) {
const handlers = { const handlers = {
[ClusterIpcMessage.CLUSTER_ADD]: clusterManager.addCluster, [ClusterIpcMessage.ADD]: clusterManager.addCluster,
[ClusterIpcMessage.CLUSTER_STOP]: clusterManager.stopCluster, [ClusterIpcMessage.STOP]: clusterManager.stopCluster,
[ClusterIpcMessage.CLUSTER_REMOVE]: clusterManager.removeCluster, [ClusterIpcMessage.REMOVE]: clusterManager.removeCluster,
[ClusterIpcMessage.CLUSTER_REMOVE_WORKSPACE]: clusterManager.removeAllByWorkspace, [ClusterIpcMessage.REMOVE_WORKSPACE]: clusterManager.removeAllByWorkspace,
[ClusterIpcMessage.CLUSTER_REFRESH]: clusterManager.refreshCluster, [ClusterIpcMessage.REFRESH]: clusterManager.refreshCluster,
[ClusterIpcMessage.FEATURE_INSTALL]: clusterManager.installFeature, [ClusterIpcMessage.FEATURE_INSTALL]: clusterManager.installFeature,
[ClusterIpcMessage.FEATURE_UPGRADE]: clusterManager.upgradeFeature, [ClusterIpcMessage.FEATURE_UPGRADE]: clusterManager.upgradeFeature,
[ClusterIpcMessage.FEATURE_REMOVE]: clusterManager.uninstallFeature, [ClusterIpcMessage.FEATURE_REMOVE]: clusterManager.uninstallFeature,

View File

@ -62,14 +62,14 @@ export class Cluster implements ClusterModel {
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
this.webContentUrl = `http://${this.id}.localhost:${port}`; this.webContentUrl = `http://${this.id}.localhost:${port}`;
this.initialized = true; this.initialized = true;
logger.info(`Cluster init success`, { logger.info(`[CLUSTER]: init success`, {
id: this.id, id: this.id,
serverUrl: this.apiUrl, serverUrl: this.apiUrl,
webContentUrl: this.webContentUrl, webContentUrl: this.webContentUrl,
kubeProxyUrl: this.kubeProxyUrl, kubeProxyUrl: this.kubeProxyUrl,
}); });
} catch (err) { } catch (err) {
logger.error(`💣 Cluster init failed: ${err}`, { logger.error(`[CLUSTER]: init failed: ${err}`, {
id: this.id, id: this.id,
error: err, error: err,
}); });

View File

@ -1,6 +1,6 @@
import { ChildProcess, spawn } from "child_process" import { ChildProcess, spawn } from "child_process"
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import { sendMessage } from "../common/ipc-helpers"; import { broadcastMessage } from "../common/ipc-helpers";
import type { Cluster } from "./cluster" import type { Cluster } from "./cluster"
import { bundledKubectl, Kubectl } from "./kubectl" import { bundledKubectl, Kubectl } from "./kubectl"
import logger from "./logger" import logger from "./logger"
@ -84,7 +84,7 @@ export class KubeAuthProxy {
const channel = `kube-auth:${this.cluster.id}` const channel = `kube-auth:${this.cluster.id}`
const message = { data, stream }; const message = { data, stream };
logger.debug(channel, message); logger.debug(channel, message);
sendMessage(channel, message); broadcastMessage(channel, message);
} }
public exit() { public exit() {

View File

@ -42,13 +42,8 @@ export class WindowManager {
}); });
}), }),
// auto-show active cluster view // auto-show active cluster view
reaction(() => clusterStore.activeCluster, activeCluster => { reaction(() => clusterStore.activeClusterId, clusterId => this.activateView(clusterId), {
if (activeCluster) {
this.activateView(activeCluster.id);
}
}, {
fireImmediately: true, fireImmediately: true,
delay: 250,
}) })
) )
} }
@ -71,10 +66,14 @@ export class WindowManager {
if (!cluster) return; if (!cluster) return;
try { try {
const activeView = this.activeView; const activeView = this.activeView;
const isFresh = !this.getView(clusterId); const isLoadedBefore = !!this.getView(clusterId);
const view = this.initView(clusterId); const view = this.initView(clusterId);
logger.info(`Activating cluster(${cluster.id}) view`, {
contextName: cluster.contextName,
isLoadedBefore: isLoadedBefore,
});
if (view !== activeView) { if (view !== activeView) {
if (isFresh) { if (!isLoadedBefore) {
await cluster.whenReady; await cluster.whenReady;
await view.loadURL(cluster.webContentUrl); await view.loadURL(cluster.webContentUrl);
} }

View File

@ -24,7 +24,7 @@ interface Props {
export class ClustersMenu extends React.Component<Props> { export class ClustersMenu extends React.Component<Props> {
showCluster = (cluster: Cluster) => { showCluster = (cluster: Cluster) => {
clusterStore.activeClusterId = cluster.id; clusterStore.activeClusterId = cluster.id;
console.log('load lens for cluster:', cluster.id); console.log('load lens for cluster:', cluster.toJSON());
} }
addCluster = () => { addCluster = () => {
@ -42,7 +42,7 @@ export class ClustersMenu extends React.Component<Props> {
if (cluster.initialized) { if (cluster.initialized) {
menu.append(new MenuItem({ menu.append(new MenuItem({
label: _i18n._(t`Disconnect`), label: _i18n._(t`Disconnect`),
click: () => console.log(`disconnect cluster and navigate to workspaces`, cluster) click: () => console.log(`disconnect cluster and navigate to workspaces`, cluster.contextName)
})) }))
} }

View File

@ -11785,6 +11785,14 @@ win-ca@^3.2.0:
node-forge "^0.8.2" node-forge "^0.8.2"
split "^1.0.1" split "^1.0.1"
winston-transport-browserconsole@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/winston-transport-browserconsole/-/winston-transport-browserconsole-1.0.5.tgz#8ef1bc32da5fb0a66604f2b8b6f127ed725108c9"
integrity sha512-BFZDvknATAmsqaRY3WatB2S0eEb0ziwqBYIv+H0Fnu9oe7GnPUVQzEJC1frmLdNsfEkfnyR1PCNDBAFtZE3SuQ==
dependencies:
winston "^3.2.1"
winston-transport "^4.3.0"
winston-transport@^4.3.0: winston-transport@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.3.0.tgz#df68c0c202482c448d9b47313c07304c2d7c2c66" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.3.0.tgz#df68c0c202482c448d9b47313c07304c2d7c2c66"