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",
"win-ca": "^3.2.0",
"winston": "^3.2.1",
"winston-transport-browserconsole": "^1.0.5",
"ws": "^7.3.0"
},
"devDependencies": {

View File

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

View File

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

View File

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

View File

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

View File

@ -23,9 +23,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
private constructor() {
super({
configName: "lens-user-store",
confOptions: {
migrations: migrations
}
migrations: migrations,
});
// 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);
return [prefix, randId(), suffix].filter(s => s).join(sep);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ interface Props {
export class ClustersMenu extends React.Component<Props> {
showCluster = (cluster: Cluster) => {
clusterStore.activeClusterId = cluster.id;
console.log('load lens for cluster:', cluster.id);
console.log('load lens for cluster:', cluster.toJSON());
}
addCluster = () => {
@ -42,7 +42,7 @@ export class ClustersMenu extends React.Component<Props> {
if (cluster.initialized) {
menu.append(new MenuItem({
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"
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:
version "4.3.0"
resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.3.0.tgz#df68c0c202482c448d9b47313c07304c2d7c2c66"