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",
|
||||
"win-ca": "^3.2.0",
|
||||
"winston": "^3.2.1",
|
||||
"winston-transport-browserconsole": "^1.0.5",
|
||||
"ws": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -3,5 +3,5 @@
|
||||
export * from "./base64"
|
||||
export * from "./camelCase"
|
||||
export * from "./splitArray"
|
||||
export * from "./randomFileName"
|
||||
export * from "./getRandId"
|
||||
export * from "./cloneJson"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user