mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
epic: getting rid of vue -- part 2
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
1 parent
63de5fce3c
commit
1f26f0b3a7
22 files changed
+469
-297
No files matched your search
@@ -21,7 +21,7 @@ export class BaseStore<T = any> 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<T = any> 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<T = any> extends Singleton {
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore(data: Partial<T> = {}) {
|
||||
Object.assign(this.data, data);
|
||||
protected fromStore(data: T) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -120,6 +120,7 @@ export class BaseStore<T = any> extends Singleton {
|
||||
this.data = produce(this.data, updater);
|
||||
}
|
||||
|
||||
// todo: use "serializr" ?
|
||||
toJSON(): T {
|
||||
return toJS(this.data, {
|
||||
recurseEverything: true,
|
||||
|
||||
@@ -81,12 +81,11 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
|
||||
@action
|
||||
protected fromStore({ clusters = [] }: Partial<ClusterStoreModel> = {}) {
|
||||
// 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<ClusterId, Cluster>();
|
||||
clusters.forEach(clusterModel => {
|
||||
clustersMap.set(clusterModel.id, new Cluster(clusterModel));
|
||||
});
|
||||
this.clusters.replace(clustersMap);
|
||||
}
|
||||
|
||||
toJSON(): ClusterStoreModel {
|
||||
|
||||
@@ -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<string, Function>, options?: IpcOptions) {
|
||||
export function onMessages(messages: Record<string, IpcMessageHandler>, options?: IpcOptions) {
|
||||
Object.entries(messages).forEach(([channel, handler]) => {
|
||||
this.onMessage(channel, handler, options);
|
||||
onMessage(channel, handler, options);
|
||||
})
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
@@ -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<WorkspaceStoreModel> {
|
||||
static readonly defaultId: WorkspaceId = "default"
|
||||
|
||||
protected data: WorkspaceStoreModel = {
|
||||
currentWorkspace: WorkspaceStore.defaultId,
|
||||
workspaces: [{
|
||||
@observable currentWorkspace = WorkspaceStore.defaultId;
|
||||
|
||||
@observable workspaces = observable.map<WorkspaceId, Workspace>({
|
||||
[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<WorkspaceStoreModel> {
|
||||
});
|
||||
}
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+73
-57
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
<script>
|
||||
import marked from 'marked'
|
||||
import {readFileSync} from 'fs'
|
||||
import { getStaticPath } from "../../../common/register-static"
|
||||
import { userStore } from "../../../common/user-store"
|
||||
|
||||
export default {
|
||||
name: 'WhatsNewPage',
|
||||
data() {
|
||||
let releaseNotes = getStaticPath("RELEASE_NOTES.md");
|
||||
let content = marked(readFileSync(releaseNotes, 'utf8'));
|
||||
// todo: check if "fs" can work with custom protocols
|
||||
let fileContents = readFileSync("static://RELEASE_NOTES.md", 'utf8');
|
||||
let content = marked(fileContents);
|
||||
return {
|
||||
error: "",
|
||||
content: content,
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
@import "fonts";
|
||||
|
||||
*, *:before, *:after {
|
||||
@include set-draggable(false);
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@@ -167,14 +167,6 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.drag {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.nodrag {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
// app's common loading indicator, displaying on the route transitions
|
||||
#loading {
|
||||
position: absolute;
|
||||
|
||||
@@ -89,3 +89,13 @@
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@mixin set-draggable($isDraggable: true) {
|
||||
@if ($isDraggable) {
|
||||
-webkit-user-drag: auto;
|
||||
-webkit-app-region: drag;
|
||||
} @else {
|
||||
-webkit-user-drag: none;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import "../../common/system-ca"
|
||||
import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
// import { App } from "./components/app";
|
||||
|
||||
await Promise.all([
|
||||
userStore.whenLoaded,
|
||||
workspaceStore.whenLoaded,
|
||||
clusterStore.whenLoaded,
|
||||
]);
|
||||
|
||||
// App.init();
|
||||
document.getElementById("app").innerHTML = "<p>Hello from renderer!</p>"
|
||||
@@ -0,0 +1,19 @@
|
||||
import "../common/system-ca"
|
||||
import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
// import { App } from "./components/app";
|
||||
|
||||
async function render() {
|
||||
await Promise.all([
|
||||
userStore.whenLoaded,
|
||||
workspaceStore.whenLoaded,
|
||||
clusterStore.whenLoaded,
|
||||
]);
|
||||
|
||||
// App.init();
|
||||
document.getElementById("app").innerHTML = "<p>Hello from renderer!</p>"
|
||||
}
|
||||
|
||||
// run
|
||||
render();
|
||||
Reference in new issue
Block a user