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:
parent
b8eaa657a2
commit
a305b04292
@ -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": {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user