epic: getting rid of vue -- part 2

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman committed 2020-07-08 12:12:11 +03:00
1 parent 63de5fce3c
commit 1f26f0b3a7
22 files changed
+469 -297

No files matched your search

+73 -57
View File
@@ -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<Cluster> {
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<string> {
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<string> {
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<number> {
return await this.getCluster(clusterId)?.getEventCount() || 0;
}
static ipcListen(clusterManager: ClusterManager) {
onMessages({
[ClusterIpcMessage.CLUSTER_ADD]: async (model: ClusterModel): Promise<boolean> => {
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<number> => {
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,
})
}
+58 -23
View File
@@ -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
})
}
}
+6 -16
View File
@@ -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<string, FeatureStatus>
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?
+26 -63
View File
@@ -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)
+5 -4
View File
@@ -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<MenuOptions> = {}) {
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)
});
+1 -5
View File
@@ -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()
}