From 1f26f0b3a74735a1a988cee64adfaf19d3e62164 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 8 Jul 2020 09:09:11 +0300 Subject: [PATCH] epic: getting rid of vue -- part 2 Signed-off-by: Roman --- package.json | 9 +- src/common/base-store.ts | 13 +- src/common/cluster-store.ts | 11 +- src/common/ipc-helpers.ts | 10 +- src/common/register-protocol.ts | 17 ++ src/common/register-static.ts | 25 --- src/common/utils/kubeconfig.ts | 4 +- src/common/vars.ts | 10 +- src/common/workspace-store.ts | 76 ++++--- src/main/cluster-manager.ts | 130 ++++++------ src/main/cluster.ts | 81 +++++--- src/main/feature.ts | 22 +-- src/main/index.ts | 89 +++------ src/main/menu.ts | 9 +- src/main/window-manager.ts | 6 +- src/renderer/_vue/components/WhatsNewPage.vue | 6 +- src/renderer/components/app.scss | 12 +- src/renderer/components/mixins.scss | 10 + src/renderer/index.ts | 14 -- src/renderer/index.tsx | 19 ++ webpack.renderer.ts | 6 +- yarn.lock | 187 ++++++++++++++++-- 22 files changed, 469 insertions(+), 297 deletions(-) create mode 100644 src/common/register-protocol.ts delete mode 100644 src/common/register-static.ts delete mode 100644 src/renderer/index.ts create mode 100644 src/renderer/index.tsx diff --git a/package.json b/package.json index 4426aeaa05..b0537a40e0 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,7 @@ }, "scripts": { "dev": "concurrently -k \"yarn dev-run -C\" \"yarn dev:main\" \"yarn dev:renderer\"", - "dev-run": "nodemon --watch out/main.* --exec \"electron --inspect .\" $@", - "dev-test": "yarn test --watch", + "dev-run": "env DEBUG=true nodemon --watch out/main.* --exec \"electron --inspect .\" $@", "dev:main": "env DEBUG=true yarn compile:main --watch $@", "dev:renderer": "env DEBUG=true yarn compile:renderer --watch $@", "compile": "concurrently \"yarn i18n:compile\" \"yarn compile:main -p\" \"yarn compile:renderer -p\"", @@ -34,7 +33,8 @@ "download-bins": "concurrently yarn:download:*", "download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:helm": "yarn run ts-node build/download_helm.ts", - "lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/" + "lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/", + "rebuild": "electron-rebuild -f -w node-pty" }, "config": { "bundledKubectlVersion": "1.17.4", @@ -260,9 +260,10 @@ "css-element-queries": "^1.2.3", "css-loader": "^3.5.3", "dompurify": "^2.0.11", - "electron": "^6.1.12", + "electron": "^7.3.2", "electron-builder": "^22.7.0", "electron-notarize": "^0.3.0", + "electron-rebuild": "^1.11.0", "eslint": "^7.3.1", "file-loader": "^6.0.0", "flex.box": "^3.4.4", diff --git a/src/common/base-store.ts b/src/common/base-store.ts index aa71e0d576..71d8991e23 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -21,7 +21,7 @@ export class BaseStore extends Singleton { public whenLoaded = when(() => this.isLoaded); @observable isLoaded = false; - @observable protected data = {} as T; + @observable protected data: T; protected constructor(protected params: BaseStoreParams) { super(); @@ -69,9 +69,9 @@ export class BaseStore extends Singleton { }, ...confOptions, }); - const data = this.storeConfig.store; - console.info(`[STORE]: [LOADED] ${this.storeConfig.path}`, data); - this.fromStore(data); + const jsonModel = this.storeConfig.store; + console.info(`[STORE]: [LOADED] ${this.storeConfig.path}`, jsonModel); + this.fromStore(jsonModel); this.isLoaded = true; } @@ -111,8 +111,8 @@ export class BaseStore extends Singleton { } @action - protected fromStore(data: Partial = {}) { - Object.assign(this.data, data); + protected fromStore(data: T) { + this.data = data; } @action @@ -120,6 +120,7 @@ export class BaseStore extends Singleton { this.data = produce(this.data, updater); } + // todo: use "serializr" ? toJSON(): T { return toJS(this.data, { recurseEverything: true, diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 33c32b2e9f..ee3323e6cd 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -81,12 +81,11 @@ export class ClusterStore extends BaseStore { @action protected fromStore({ clusters = [] }: Partial = {}) { - // fixme: handle clusters update + delete - clusters.forEach(model => { - if (!this.clusters.has(model.id)) { - this.clusters.set(model.id, new Cluster(model)); - } - }) + const clustersMap = new Map(); + clusters.forEach(clusterModel => { + clustersMap.set(clusterModel.id, new Cluster(clusterModel)); + }); + this.clusters.replace(clustersMap); } toJSON(): ClusterStoreModel { diff --git a/src/common/ipc-helpers.ts b/src/common/ipc-helpers.ts index 3d473c8492..cd157e9bc0 100644 --- a/src/common/ipc-helpers.ts +++ b/src/common/ipc-helpers.ts @@ -9,12 +9,16 @@ export interface IpcOptions { timeout?: number; } +export interface IpcMessageHandler { + (...args: any[]): any; +} + export async function invokeMessage(channel: string, ...args: any[]) { logger.debug(`[IPC]: invoke channel "${channel}"`, { args }); return ipcRenderer.invoke(channel, ...args); } -export function onMessage(channel: string, handler: (...args: any[]) => any, options: IpcOptions = {}) { +export function onMessage(channel: string, handler: IpcMessageHandler, options: IpcOptions = {}) { const { timeout = 0 } = options; ipcMain.handle(channel, async (event, ...args: any[]) => { logger.debug(`[IPC]: handle "${channel}"`, { event, args }); @@ -37,8 +41,8 @@ export function onMessage(channel: string, handler: (...args: any[]) => any, opt }) } -export function onMessages(messages: Record, options?: IpcOptions) { +export function onMessages(messages: Record, options?: IpcOptions) { Object.entries(messages).forEach(([channel, handler]) => { - this.onMessage(channel, handler, options); + onMessage(channel, handler, options); }) } diff --git a/src/common/register-protocol.ts b/src/common/register-protocol.ts new file mode 100644 index 0000000000..1ddb2ef7ae --- /dev/null +++ b/src/common/register-protocol.ts @@ -0,0 +1,17 @@ +// Register custom protocols + +import path from "path"; +import { protocol } from "electron" +import logger from "../main/logger"; + +export function registerFileProtocol(name: string, basePath: string) { + protocol.registerFileProtocol(name, (request, callback) => { + const filePath = request.url.replace(name + "://", ""); + const absPath = path.resolve(basePath, filePath); + callback(absPath); + }, (error) => { + if (error) { + logger.error(`Failed to register protocol "${name}"`, { basePath, error }); + } + }) +} diff --git a/src/common/register-static.ts b/src/common/register-static.ts deleted file mode 100644 index 9b68a611b3..0000000000 --- a/src/common/register-static.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Setup static folder for common assets - -import path from "path"; -import { protocol } from "electron" -import logger from "../main/logger"; -import { staticDir, staticProto, outDir } from "./vars"; - -export function registerStaticProtocol(rootFolder = staticDir) { - const scheme = staticProto.replace("://", ""); - protocol.registerFileProtocol(scheme, (request, callback) => { - const relativePath = request.url.replace(staticProto, ""); - const absPath = path.resolve(rootFolder, relativePath); - callback(absPath); - }, (error) => { - logger.debug(`Failed to register protocol "${scheme}"`, error); - }) -} - -export function getStaticUrl(filePath: string) { - return staticProto + filePath; -} - -export function getStaticPath(filePath: string) { - return path.resolve(staticDir, filePath); -} diff --git a/src/common/utils/kubeconfig.ts b/src/common/utils/kubeconfig.ts index 4e5e314524..a6f7015515 100644 --- a/src/common/utils/kubeconfig.ts +++ b/src/common/utils/kubeconfig.ts @@ -7,8 +7,8 @@ import * as path from "path" // Writes kubeconfigs to "embedded" store, i.e. .../Lens/kubeconfigs/ export function writeEmbeddedKubeConfig(clusterId: string, kubeConfig: string): string { // This can be called from main & renderer - const a = (app || remote.app) - const kubeConfigBase = path.join(a.getPath("userData"), "kubeconfigs") + const userData = (app || remote.app).getPath("userData"); + const kubeConfigBase = path.join(userData, "kubeconfigs") ensureDirSync(kubeConfigBase) const kubeConfigFile = path.join(kubeConfigBase, clusterId) diff --git a/src/common/vars.ts b/src/common/vars.ts index ca051bc24e..327b3be436 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -1,9 +1,7 @@ // App's common configuration for any process (main, renderer, build pipeline, etc.) +import packageInfo from "../../package.json" import path from "path"; -export const appName = "lens" - -// Flags export const isMac = process.platform === "darwin" export const isWindows = process.platform === "win32" export const isDebugging = process.env.DEBUG === "true"; @@ -12,6 +10,10 @@ export const isDevelopment = isDebugging || !isProduction; export const buildVersion = process.env.BUILD_VERSION; export const isTestEnv = !!process.env.JEST_WORKER_ID; +export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}` +export const appProto = "lens" // app's "userData" folder (e.g. "lens://icons/logo.svg") +export const staticProto = "static" // static content folder (e.g. "static://RELEASE_NOTES.md") + // Paths export const contextDir = process.cwd(); export const staticDir = path.join(contextDir, "static"); @@ -22,8 +24,6 @@ export const htmlTemplate = path.resolve(rendererDir, "template.html"); export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss"); // Apis -export const staticProto = "static://" - export const apiPrefix = { BASE: '/api', KUBE_BASE: '/api-kube', // kubernetes cluster api diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index dd84e17487..bc396df394 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -1,11 +1,11 @@ -import { action, computed, toJS } from "mobx"; +import { action, observable } from "mobx"; import { BaseStore } from "./base-store"; import { clusterStore } from "./cluster-store" export type WorkspaceId = string; export interface WorkspaceStoreModel { - currentWorkspace?: WorkspaceId; // last visited/activated + currentWorkspace?: WorkspaceId; workspaces: Workspace[] } @@ -18,17 +18,14 @@ export interface Workspace { export class WorkspaceStore extends BaseStore { static readonly defaultId: WorkspaceId = "default" - protected data: WorkspaceStoreModel = { - currentWorkspace: WorkspaceStore.defaultId, - workspaces: [{ + @observable currentWorkspace = WorkspaceStore.defaultId; + + @observable workspaces = observable.map({ + [WorkspaceStore.defaultId]: { id: WorkspaceStore.defaultId, name: "default" - }] - } - - @computed get workspaces() { - return toJS(this.data.workspaces); - } + } + }); private constructor() { super({ @@ -36,40 +33,59 @@ export class WorkspaceStore extends BaseStore { }); } - public getById(id: WorkspaceId): Workspace { - return this.workspaces.find(workspace => workspace.id === id); - } - - public getIndexById(id: WorkspaceId): number { - return this.workspaces.findIndex(workspace => workspace.id === id); + getById(id: WorkspaceId): Workspace { + return this.workspaces.get(id); } @action setCurrent(id: WorkspaceId) { - this.data.currentWorkspace = id; + if (!this.getById(id)) return; + this.currentWorkspace = id; } @action - public saveWorkspace(newWorkspace: Workspace) { - const workspace = this.getById(newWorkspace.id); - if (workspace) { - Object.assign(workspace, newWorkspace); + public saveWorkspace(workspace: Workspace) { + const id = workspace.id; + const existingWorkspace = this.getById(id); + if (existingWorkspace) { + Object.assign(existingWorkspace, workspace); } else { - this.data.workspaces.push(newWorkspace); + this.workspaces.set(id, workspace); } } @action - public removeWorkspace(workspaceOrId: Workspace | WorkspaceId) { - const workspace = this.getById(typeof workspaceOrId == "string" ? workspaceOrId : workspaceOrId.id); + public removeWorkspace(id: WorkspaceId) { + const workspace = this.getById(id); if (!workspace) return; - if (workspace.id === WorkspaceStore.defaultId) { + if (id === WorkspaceStore.defaultId) { throw new Error("Cannot remove default workspace"); } - const index = this.getIndexById(workspace.id); - if (index > -1) { - this.data.workspaces.splice(index, 1) - clusterStore.removeByWorkspaceId(workspace.id) + if (id === this.currentWorkspace) { + this.currentWorkspace = WorkspaceStore.defaultId; + } + this.workspaces.delete(id); + clusterStore.removeByWorkspaceId(id) + } + + @action + protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) { + if (currentWorkspace) { + this.currentWorkspace = currentWorkspace + } + if (workspaces.length) { + this.workspaces.clear(); + workspaces.forEach(workspace => { + this.workspaces.set(workspace.id, workspace) + }) + } + } + + toJSON(): WorkspaceStoreModel { + const { currentWorkspace, workspaces } = this; + return { + currentWorkspace: currentWorkspace, + workspaces: Array.from(workspaces.values()), } } } diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 7f7034a2d5..93bb74db19 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,5 +1,5 @@ import { autorun } from "mobx"; -import { apiPrefix } from "../common/vars"; +import { apiPrefix, appProto } from "../common/vars"; import { app } from "electron" import path from "path" import http from "http" @@ -26,8 +26,8 @@ export class ClusterManager { } constructor(protected port: number) { - ClusterManager.ipcListen(this); autorun(() => { + // fixme: detect and stop removed clusters from config file ? clusterStore.clusters.forEach((cluster: Cluster) => { if (!cluster.initialized) { cluster.init(this.port); @@ -35,6 +35,8 @@ export class ClusterManager { } }) }); + + ClusterManager.ipcListen(this); } stop() { @@ -48,6 +50,7 @@ export class ClusterManager { } protected async addCluster(clusterModel: ClusterModel): Promise { + tracker.event("cluster", "add"); try { await validateConfig(clusterModel.kubeConfigPath); return clusterStore.addCluster({ @@ -60,7 +63,13 @@ export class ClusterManager { } } + protected stopCluster(clusterId: ClusterId) { + tracker.event("cluster", "stop"); + this.getCluster(clusterId)?.stopServer(); + } + protected removeAllByWorkspace(workspaceId: string) { + tracker.event("cluster", "remove-workspace"); const clusters = clusterStore.getByWorkspaceId(workspaceId); clusters.forEach(cluster => { this.removeCluster(cluster.id); @@ -68,6 +77,7 @@ export class ClusterManager { } protected removeCluster(clusterId: string): Cluster { + tracker.event("cluster", "remove"); const cluster = this.getCluster(clusterId); if (cluster) { cluster.stopServer() @@ -97,64 +107,70 @@ export class ClusterManager { return cluster; } - protected async uploadClusterIcon(cluster: Cluster, fileName: string, src: string): Promise { - await ensureDir(ClusterManager.clusterIconDir) - fileName = filenamify(cluster.contextName + "-" + fileName) - const dest = path.join(ClusterManager.clusterIconDir, fileName) - await copyFile(src, dest) - return "store:///icons/" + fileName + protected async uploadClusterIcon({ clusterId, name: fileName, path: src }: ClusterIconUpload): Promise { + const cluster = this.getCluster(clusterId); + if (cluster) { + tracker.event("cluster", "upload-icon"); + await ensureDir(ClusterManager.clusterIconDir) + fileName = filenamify(cluster.contextName + "-" + fileName) + const dest = path.join(ClusterManager.clusterIconDir, fileName) + await copyFile(src, dest) + cluster.preferences.icon = `${appProto}:///icons/${fileName}` + return cluster.preferences.icon; + } + } + + // todo: remove current icon file ? + protected resetClusterIcon(clusterId: ClusterId) { + const cluster = this.getCluster(clusterId); + if (cluster) { + tracker.event("cluster", "reset-icon") + cluster.preferences.icon = null; + } + } + + // todo: check feature failures + protected async installFeature({ clusterId, name, config }: FeatureInstallRequest) { + tracker.event("cluster", "install-feature") + return this.getCluster(clusterId)?.installFeature(name, config) + } + + protected async upgradeFeature({ clusterId, name, config }: FeatureInstallRequest) { + tracker.event("cluster", "upgrade-feature") + return this.getCluster(clusterId)?.upgradeFeature(name, config) + } + + protected async uninstallFeature({ clusterId, name }: FeatureInstallRequest) { + tracker.event("cluster", "uninstall-feature") + return this.getCluster(clusterId)?.uninstallFeature(name); + } + + protected async refreshCluster(clusterId: ClusterId) { + await this.getCluster(clusterId)?.refreshCluster(); + } + + protected async getEventsCount(clusterId: ClusterId): Promise { + return await this.getCluster(clusterId)?.getEventCount() || 0; } static ipcListen(clusterManager: ClusterManager) { - onMessages({ - [ClusterIpcMessage.CLUSTER_ADD]: async (model: ClusterModel): Promise => { - tracker.event("cluster", "add"); - await clusterManager.addCluster(model); - return true; - }, - [ClusterIpcMessage.CLUSTER_STOP]: (clusterId: ClusterId) => { - tracker.event("cluster", "stop"); - clusterManager.getCluster(clusterId)?.stopServer(); - }, - [ClusterIpcMessage.CLUSTER_REMOVE]: (clusterId: ClusterId) => { - tracker.event("cluster", "remove"); - clusterManager.removeCluster(clusterId); - }, - [ClusterIpcMessage.CLUSTER_REMOVE_WORKSPACE]: (workspaceId: ClusterId) => { - clusterManager.removeAllByWorkspace(workspaceId); - }, - [ClusterIpcMessage.CLUSTER_REFRESH]: (clusterId: ClusterId) => { - clusterManager.getCluster(clusterId)?.refreshCluster(); - }, - [ClusterIpcMessage.CLUSTER_EVENTS]: async (clusterId: ClusterId): Promise => { - return await clusterManager.getCluster(clusterId)?.getEventCount() || 0; - }, - // todo: check feature failures - [ClusterIpcMessage.FEATURE_INSTALL]: ({ clusterId, name, config }: FeatureInstallRequest) => { - tracker.event("cluster", "install-feature") - return clusterManager.getCluster(clusterId)?.installFeature(name, config) - }, - [ClusterIpcMessage.FEATURE_UPGRADE]: ({ clusterId, name, config }: FeatureInstallRequest) => { - tracker.event("cluster", "upgrade-feature") - return clusterManager.getCluster(clusterId)?.upgradeFeature(name, config) - }, - [ClusterIpcMessage.FEATURE_REMOVE]: ({ clusterId, name }: FeatureInstallRequest) => { - tracker.event("cluster", "uninstall-feature") - return clusterManager.getCluster(clusterId)?.uninstallFeature(name); - }, - [ClusterIpcMessage.ICON_SAVE]: async ({ clusterId, name, path }: ClusterIconUpload) => { - tracker.event("cluster", "upload-icon"); - const cluster = clusterManager.getCluster(clusterId); - if (!cluster) return false; - cluster.preferences.icon = await clusterManager.uploadClusterIcon(cluster, name, path); - }, - [ClusterIpcMessage.ICON_RESET]: async (clusterId: ClusterId) => { - tracker.event("cluster", "reset-icon") - const cluster = clusterManager.getCluster(clusterId); - if (!cluster) return false; - cluster.preferences.icon = null; // todo: remove current file-icon ? - }, - }, { + 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.CLUSTER_EVENTS]: clusterManager.getEventsCount, + [ClusterIpcMessage.FEATURE_INSTALL]: clusterManager.installFeature, + [ClusterIpcMessage.FEATURE_UPGRADE]: clusterManager.upgradeFeature, + [ClusterIpcMessage.FEATURE_REMOVE]: clusterManager.uninstallFeature, + [ClusterIpcMessage.ICON_SAVE]: clusterManager.uploadClusterIcon, + [ClusterIpcMessage.ICON_RESET]: clusterManager.removeCluster, + }; + Object.entries(handlers).forEach(([key, handler]) => { + handlers[key as keyof typeof handlers] = handler.bind(clusterManager); + }) + onMessages(handlers, { timeout: 2000, }) } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 883d97de34..4937714ce4 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,13 +1,13 @@ -import { observable } from "mobx"; +import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" +import type { FeatureStatusMap } from "./feature" +import { observable, toJS } from "mobx"; import { apiPrefix } from "../common/vars"; import { ContextHandler } from "./context-handler" -import { FeatureStatusMap } from "./feature" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import { Kubectl } from "./kubectl"; import { KubeconfigManager } from "./kubeconfig-manager" import { getNodeWarningConditions, loadConfig, podHasIssues } from "./k8s" import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager"; -import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import request from "request-promise-native" import logger from "./logger" @@ -17,30 +17,43 @@ enum ClusterStatus { Offline = 0 } +export interface ClusterState extends ClusterModel { + url: string; + apiUrl: string; + online?: boolean; + accessible?: boolean; + failureReason?: string; + nodes?: number; + eventCount?: number; + version?: string; + distribution?: string; + isAdmin?: boolean; + features?: FeatureStatusMap; +} + export class Cluster implements ClusterModel { + public contextHandler: ContextHandler; + public kubeCtl: Kubectl + protected kubeconfigManager: KubeconfigManager; + @observable initialized = false; @observable id: ClusterId; @observable workspace: string; @observable kubeConfigPath: string; @observable contextName: string; + @observable url: string; + @observable port: number; + @observable apiUrl: string; + @observable online: boolean; + @observable accessible: boolean; + @observable failureReason: string; + @observable nodes: number; + @observable version: string; + @observable distribution: string; + @observable isAdmin: boolean; + @observable eventCount: number; @observable preferences: ClusterPreferences = {}; - - public contextHandler: ContextHandler; - public url: string; - public port: number; - public apiUrl: string; - public online: boolean; - public accessible: boolean; - public failureReason: string; - public nodes: number; - public version: string; - public distribution: string; - public isAdmin: boolean; - public eventCount: number; - public kubeCtl: Kubectl - public features: FeatureStatusMap = {}; - - protected kubeconfigManager: KubeconfigManager; + @observable features: FeatureStatusMap = {}; constructor(model: ClusterModel) { Object.assign(this, model) @@ -50,7 +63,7 @@ export class Cluster implements ClusterModel { const { contextName } = this try { const kubeConfig = loadConfig(this.kubeConfigPath) - kubeConfig.setCurrentContext(contextName); // fixme: is it needed at all? + kubeConfig.setCurrentContext(contextName); // fixme: is it required, when if so? this.port = port; this.apiUrl = kubeConfig.getCurrentCluster().server this.contextHandler = new ContextHandler(kubeConfig, this) @@ -238,12 +251,34 @@ export class Cluster implements ClusterModel { } toJSON(): ClusterModel { - return { + return toJS({ id: this.id, contextName: this.contextName, kubeConfigPath: this.kubeConfigPath, workspace: this.workspace, preferences: this.preferences, - } + }, { + recurseEverything: true + }) + } + + getState(): ClusterState { + const storeModel = this.toJSON(); + return toJS({ + ...storeModel, + url: this.url, + apiUrl: this.apiUrl, + online: this.online, + accessible: this.accessible, + failureReason: this.failureReason, + nodes: this.nodes, + version: this.version, + distribution: this.distribution, + isAdmin: this.isAdmin, + features: this.features, + eventCount: this.eventCount, + }, { + recurseEverything: true + }) } } diff --git a/src/main/feature.ts b/src/main/feature.ts index 6eb3062cc3..b323cb65a6 100644 --- a/src/main/feature.ts +++ b/src/main/feature.ts @@ -3,30 +3,22 @@ import path from "path" import hb from "handlebars" import { ResourceApplier } from "./resource-applier" import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node" -import logger from "./logger"; import { Cluster } from "./cluster"; +import logger from "./logger"; -export type FeatureInstallRequest = { +export type FeatureStatusMap = Record + +export interface FeatureInstallRequest { clusterId: string; name: string; config?: any; } -export type FeatureInstallResponse = { - success: boolean; - message: string; -} - -export type FeatureStatus = { +export interface FeatureStatus { currentVersion: string; installed: boolean; latestVersion: string; canUpgrade: boolean; - // TODO We need bunch of other stuff too: upgradeable, latestVersion, ... -}; - -export type FeatureStatusMap = { - [name: string]: FeatureStatus; } export abstract class Feature { @@ -35,9 +27,7 @@ export abstract class Feature { latestVersion: string; constructor(config: any) { - if(config) { - this.config = config; - } + if(config) this.config = config; } // TODO Return types for these? diff --git a/src/main/index.ts b/src/main/index.ts index d9489b577d..a71f08fa00 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,12 +2,10 @@ import "../common/system-ca" import "../common/prometheus-providers" -import { app, dialog, protocol } from "electron" -import { appName, isDevelopment, isMac } from "../common/vars"; -import { PromiseIpc } from "electron-promise-ipc" +import { app, dialog } from "electron" +import { appName, appProto, isDevelopment, isMac, staticDir, staticProto } from "../common/vars"; import path from "path" import { format as formatUrl } from "url" -import logger from "./logger" import initMenu from "./menu" import * as proxy from "./proxy" import { WindowManager } from "./window-manager"; @@ -16,25 +14,13 @@ import AppUpdater from "./app-updater" import { shellSync } from "./shell-sync" import { getFreePort } from "./port" import { mangleProxyEnv } from "./proxy-env" -import { findMainWebContents } from "./webcontents" -import { registerStaticProtocol } from "../common/register-static"; +import { registerFileProtocol } from "../common/register-protocol"; import { clusterStore } from "../common/cluster-store" import { userStore } from "../common/user-store"; +import { workspaceStore } from "../common/workspace-store"; import { tracker } from "../common/tracker"; +import logger from "./logger" -if (isDevelopment) { - const appName = "LensDev"; - const appData = app.getPath("appData"); - app.setName(appName); - app.setPath("userData", path.join(appData, appName)); -} - -mangleProxyEnv() -if (app.commandLine.getSwitchValue("proxy-server") !== "") { - process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server") -} - -const promiseIpc = new PromiseIpc({ timeout: 2000 }) let windowManager: WindowManager = null; let clusterManager: ClusterManager = null; let proxyServer: proxy.LensProxy = null; @@ -45,24 +31,30 @@ const vmURL = formatUrl({ slashes: true, }) -async function main() { - shellSync(app.getLocale()) +mangleProxyEnv() +if (app.commandLine.getSwitchValue("proxy-server") !== "") { + process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server") +} +async function main() { + shellSync(app.getLocale()); + + // todo: check other usages .getPath("userData") and enable "lazy-evaluation" + const workingDir = path.join(app.getPath("appData"), appName); + app.setName(appName); + app.setPath("userData", workingDir); + logger.info(`Start app from "${workingDir}"`) + + tracker.event("app", "start"); const updater = new AppUpdater() updater.start(); - tracker.event("app", "start"); + initMenu(); + registerFileProtocol(appProto, app.getPath("userData")); + registerFileProtocol(staticProto, staticDir); - registerStaticProtocol(); - protocol.registerFileProtocol('store', (request, callback) => { - const url = request.url.substr(8) - callback(path.normalize(`${app.getPath("userData")}/${url}`)) - }, (error) => { - if (error) console.error('Failed to register protocol') - }) - - let port: number = null // find free port + let port: number try { port = await getFreePort() } catch (error) { @@ -71,10 +63,11 @@ async function main() { app.quit(); } - // preload required stores + // preload configuration from stores await Promise.all([ userStore.load(), clusterStore.load(), + workspaceStore.load(), ]); // create cluster manager @@ -89,39 +82,9 @@ async function main() { app.quit(); } - // boot windowmanager + // manage lens windows windowManager = new WindowManager(); windowManager.showMain(vmURL) - - initMenu({ - logoutHook: async () => { - // IPC send needs webContents as we're sending it to renderer - promiseIpc.send('logout', findMainWebContents(), {}).then((data: any) => { - logger.debug("logout IPC sent"); - }) - }, - showPreferencesHook: async () => { - // IPC send needs webContents as we're sending it to renderer - promiseIpc.send('navigate', findMainWebContents(), { name: 'preferences-page' }).then((data: any) => { - logger.debug("navigate: preferences IPC sent"); - }) - }, - addClusterHook: async () => { - promiseIpc.send('navigate', findMainWebContents(), { name: "add-cluster-page" }).then((data: any) => { - logger.debug("navigate: add-cluster-page IPC sent"); - }) - }, - showWhatsNewHook: async () => { - promiseIpc.send('navigate', findMainWebContents(), { name: "whats-new-page" }).then((data: any) => { - logger.debug("navigate: whats-new-page IPC sent"); - }) - }, - clusterSettingsHook: async () => { - promiseIpc.send('navigate', findMainWebContents(), { name: "cluster-settings-page" }).then((data: any) => { - logger.debug("navigate: cluster-settings-page IPC sent"); - }) - }, - }, promiseIpc) } app.on("ready", main) diff --git a/src/main/menu.ts b/src/main/menu.ts index 82aef3d5fe..ebbcfa42bf 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -1,5 +1,6 @@ +import { PromiseIpc } from "electron-promise-ipc"; import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron" -import { isDevelopment, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars"; +import { isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars"; // todo: refactor + split menu sections to separated files, e.g. menus/file.menu.ts @@ -38,10 +39,8 @@ function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) { /** * Constructs the menu based on the example at: https://electronjs.org/docs/api/menu#main-process * Menu items are constructed piece-by-piece to have slightly better control on individual sub-menus - * - * @param ipc the main promiceIpc handle. Needed to be able to hook IPC sending into logout click handler. */ -export default function initMenu(opts: MenuOptions, promiseIpc: any) { +export default function initMenu(opts: Partial = {}) { const mt: MenuItemConstructorOptions[] = []; const macAppMenu: MenuItemConstructorOptions = { label: app.getName(), @@ -199,6 +198,8 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) { const menu = Menu.buildFromTemplate(mt); Menu.setApplicationMenu(menu); + const promiseIpc = new PromiseIpc({ timeout: 2000 }) + promiseIpc.on("enableClusterSettingsMenuItem", (clusterId: string) => { setClusterSettingsEnabled(true) }); diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index fc2805f9cb..996b3fbb8c 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,17 +1,13 @@ import { BrowserWindow, shell } from "electron" -import { PromiseIpc } from "electron-promise-ipc" import windowStateKeeper from "electron-window-state" -import { getStaticUrl } from "../common/register-static"; import { tracker } from "../common/tracker"; export class WindowManager { public mainWindow: BrowserWindow = null; public splashWindow: BrowserWindow = null; - protected promiseIpc: any protected windowState: windowStateKeeper.State; constructor({ showSplash = true } = {}) { - this.promiseIpc = new PromiseIpc({ timeout: 2000 }) // Manage main window size&position with persistence this.windowState = windowStateKeeper({ defaultHeight: 900, @@ -31,7 +27,7 @@ export class WindowManager { } }) if (showSplash) { - this.splashWindow.loadURL(getStaticUrl("splash.html")) + this.splashWindow.loadURL("static://splash.html") this.splashWindow.show() } diff --git a/src/renderer/_vue/components/WhatsNewPage.vue b/src/renderer/_vue/components/WhatsNewPage.vue index 781b66253f..15ea82a101 100644 --- a/src/renderer/_vue/components/WhatsNewPage.vue +++ b/src/renderer/_vue/components/WhatsNewPage.vue @@ -29,14 +29,14 @@