diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 3adca3a36f..9bfaf51ce6 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,14 +1,15 @@ +import url from "url" import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { FeatureStatusMap } from "./feature" -import { observable, toJS } from "mobx"; +import { computed, observable, toJS } from "mobx"; import { apiPrefix } from "../common/vars"; import { ContextHandler } from "./context-handler" 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 { getNodeWarningConditions, podHasIssues } from "./k8s" import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager"; -import request from "request-promise-native" +import request, { RequestPromiseOptions } from "request-promise-native" import logger from "./logger" enum ClusterStatus { @@ -43,7 +44,6 @@ export class Cluster implements ClusterModel { @observable contextName: string; @observable url: string; @observable port: number; - @observable apiUrl: string; @observable online: boolean; @observable accessible: boolean; @observable failureReason: string; @@ -63,21 +63,28 @@ export class Cluster implements ClusterModel { Object.assign(this, model) } + @computed get apiUrl() { + return url.parse(`http://${this.id}.localhost:${this.port}`) + } + + @computed get apiServerUrl() { + return url.parse(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}`) + } + async init(port: number) { - const { contextName } = this try { - const kubeConfig = loadConfig(this.kubeConfigPath) - kubeConfig.setCurrentContext(contextName); // fixme: is it required, when if so? this.port = port; - this.apiUrl = kubeConfig.getCurrentCluster().server - this.contextHandler = new ContextHandler(kubeConfig, this) + this.contextHandler = new ContextHandler(this); await this.contextHandler.init() // So we get the proxy port reserved this.kubeconfigManager = new KubeconfigManager(this) this.url = this.contextHandler.url this.initialized = true; - logger.debug(`[CLUSTER]: init done for "${this.id}", context ${contextName}`); + logger.debug(`[CLUSTER]: init done (id="${this.id}", context="${this.contextName}")`); } catch (err) { - logger.error(`[CLUSTER]: init "${this.id}" has failed`, { err, contextName }); + logger.error(`[CLUSTER]: init failed (id="${this.id}")`, { + contextName: this.contextName, + error: err + }); } } @@ -132,16 +139,15 @@ export class Cluster implements ClusterModel { return this.preferences.prometheus?.prefix || "" } - protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) { - const options = Object.assign({ + k8sRequest(path: string, options: RequestPromiseOptions = {}) { + return request(this.apiServerUrl + path, { json: true, - timeout: 10000 - }, (opts || {})) - if (!options.headers) { - options.headers = {} - } - options.headers.host = `${this.id}.localhost:${this.port}` - return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options) + timeout: 10000, + headers: { + ...(options.headers || {}), + host: this.apiUrl.host, + } + }) } protected async getConnectionStatus() { @@ -202,8 +208,8 @@ export class Cluster implements ClusterModel { if (kubernetesVersion.includes("gke")) return "gke" if (kubernetesVersion.includes("eks")) return "eks" if (kubernetesVersion.includes("IKS")) return "iks" - if (apiUrl.endsWith("azmk8s.io")) return "aks" - if (apiUrl.endsWith("k8s.ondigitalocean.com")) return "digitalocean" + if (apiUrl.href.endsWith("azmk8s.io")) return "aks" + if (apiUrl.href.endsWith("k8s.ondigitalocean.com")) return "digitalocean" if (contextName.startsWith("minikube")) return "minikube" if (kubernetesVersion.includes("+")) return "custom" return "vanilla" @@ -271,7 +277,7 @@ export class Cluster implements ClusterModel { return toJS({ ...storeModel, url: this.url, - apiUrl: this.apiUrl, + apiUrl: this.apiUrl.href, online: this.online, accessible: this.accessible, failureReason: this.failureReason, diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index 8feec50803..f4c14cf125 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -1,6 +1,5 @@ -import { CoreV1Api, KubeConfig } from "@kubernetes/client-node" +import { CoreV1Api } from "@kubernetes/client-node" import { ServerOptions } from "http-proxy" -import * as url from "url" import logger from "./logger" import { getFreePort } from "./port" import { KubeAuthProxy } from "./kube-auth-proxy" @@ -15,31 +14,21 @@ export class ContextHandler { public contextName: string protected id: string - protected clusterUrl: url.UrlWithStringQuery protected proxyServer: KubeAuthProxy + protected apiTarget: ServerOptions protected certData: string protected authCertData: string - protected cluster: Cluster - protected apiTarget: ServerOptions protected proxyTarget: ServerOptions protected clientCert: string protected clientKey: string - protected secureApiConnection = true - protected defaultNamespace: string - protected kubernetesApi: string protected prometheusProvider: string protected prometheusPath: string protected clusterName: string - constructor(kc: KubeConfig, cluster: Cluster) { + constructor(protected cluster: Cluster) { this.id = cluster.id - this.cluster = cluster - this.clusterUrl = url.parse(cluster.apiUrl) + this.url = cluster.apiUrl.href; this.contextName = cluster.contextName; - this.defaultNamespace = kc.getContextObject(cluster.contextName).namespace - this.url = `http://${this.id}.localhost:${cluster.port}/` - this.kubernetesApi = `http://127.0.0.1:${cluster.port}/${this.id}` - this.setClusterPreferences(cluster.preferences) } @@ -79,16 +68,12 @@ export class ContextHandler { return await provider.getPrometheusService(apiClient) }) const resolvedPrometheusServices = await Promise.all(prometheusPromises) - const service = resolvedPrometheusServices.filter(n => n)[0] - if (service) { - return service - } else { - return { - id: "lens", - namespace: "lens-metrics", - service: "prometheus", - port: 80 - } + const service = resolvedPrometheusServices.filter(n => n)[0]; + return service || { + id: "lens", + namespace: "lens-metrics", + service: "prometheus", + port: 80 } } @@ -112,17 +97,18 @@ export class ContextHandler { } protected async newApiTarget(timeout: number): Promise { + const clusterUrl = this.cluster.apiUrl; return { changeOrigin: true, timeout: timeout, headers: { - "Host": this.clusterUrl.hostname + "Host": clusterUrl.hostname }, target: { port: await this.resolveProxyPort(), protocol: "http://", host: "localhost", - path: this.clusterUrl.path + path: clusterUrl.path, }, } } diff --git a/src/main/index.ts b/src/main/index.ts index f96caf2709..187f9acd3a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,7 +5,6 @@ import "../common/prometheus-providers" import { app, dialog } from "electron" import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars"; import path from "path" -import { format as formatUrl } from "url" import initMenu from "./menu" import { LensProxy, listen } from "./proxy" import { WindowManager } from "./window-manager"; @@ -25,12 +24,6 @@ let windowManager: WindowManager; let clusterManager: ClusterManager; let proxyServer: LensProxy; -const vmURL = formatUrl({ - pathname: path.join(__dirname, `${appName}.html`), - protocol: "file", - slashes: true, -}) - mangleProxyEnv() if (app.commandLine.getSwitchValue("proxy-server") !== "") { process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server") @@ -81,9 +74,9 @@ async function main() { app.quit(); } - // manage lens windows - windowManager = new WindowManager({showSplash: true}); - windowManager.loadURL(vmURL) + // create window manager and open app + windowManager = new WindowManager(); + windowManager.showSplash(); } // Events @@ -95,19 +88,15 @@ app.on('window-all-closed', function () { if (!isMac) { app.quit(); } else { - windowManager = null - if (clusterManager) clusterManager.stop() + // windowManager.destroy(); + // clusterManager.stop() } }) -app.on("activate", () => { - if (!windowManager) { - windowManager = new WindowManager() - windowManager.loadURL(vmURL) - } +app.on("activate", (event, hasVisibleWindows) => { + // todo: something }) -// fixme: app can't quit normally (Cmd+W/Q not working) app.on("will-quit", async (event) => { event.preventDefault(); // To allow mixpanel sending to be executed if (clusterManager) clusterManager.stop() diff --git a/src/main/k8s.ts b/src/main/k8s.ts index 46fd27687d..590f3a728f 100644 --- a/src/main/k8s.ts +++ b/src/main/k8s.ts @@ -1,4 +1,4 @@ -import k8s from "@kubernetes/client-node" +import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node" import os from "os" import yaml from "js-yaml" import logger from "./logger"; @@ -10,8 +10,8 @@ function resolveTilde(filePath: string) { return filePath; } -export function loadConfig(kubeConfigPath?: string): k8s.KubeConfig { - const kc = new k8s.KubeConfig() +export function loadConfig(kubeConfigPath?: string): KubeConfig { + const kc = new KubeConfig() if (kubeConfigPath) { kc.loadFromFile(resolveTilde(kubeConfigPath)) } else { @@ -27,38 +27,35 @@ export function loadConfig(kubeConfigPath?: string): k8s.KubeConfig { * - Context * @param config KubeConfig to check */ -export function validateConfig(config: k8s.KubeConfig | string): k8s.KubeConfig { - if(typeof config == "string") { +export function validateConfig(config: KubeConfig | string): KubeConfig { + if (typeof config == "string") { config = loadConfig(config); } logger.debug(`validating kube config: ${JSON.stringify(config)}`) - if(!config.users || config.users.length == 0) { + if (!config.users || config.users.length == 0) { throw new Error("No users provided in config") } - if(!config.clusters || config.clusters.length == 0) { + if (!config.clusters || config.clusters.length == 0) { throw new Error("No clusters provided in config") } - if(!config.contexts || config.contexts.length == 0) { + if (!config.contexts || config.contexts.length == 0) { throw new Error("No contexts provided in config") } return config } - /** * Breaks kube config into several configs. Each context as it own KubeConfig object - * - * @param configString yaml string of kube config */ -export function splitConfig(kubeConfig: k8s.KubeConfig): k8s.KubeConfig[] { - const configs: k8s.KubeConfig[] = [] - if(!kubeConfig.contexts) { +export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] { + const configs: KubeConfig[] = [] + if (!kubeConfig.contexts) { return configs; } kubeConfig.contexts.forEach(ctx => { - const kc = new k8s.KubeConfig(); + const kc = new KubeConfig(); kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n); kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n) kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n) @@ -74,13 +71,13 @@ export function splitConfig(kubeConfig: k8s.KubeConfig): k8s.KubeConfig[] { * * @param configPath path to kube config yaml file */ -export function loadAndSplitConfig(configPath: string): k8s.KubeConfig[] { - const allConfigs = new k8s.KubeConfig(); +export function loadAndSplitConfig(configPath: string): KubeConfig[] { + const allConfigs = new KubeConfig(); allConfigs.loadFromFile(configPath); return splitConfig(allConfigs); } -export function dumpConfigYaml(kc: k8s.KubeConfig): string { +export function dumpConfigYaml(kc: KubeConfig): string { const config = { apiVersion: "v1", kind: "Config", @@ -128,10 +125,10 @@ export function dumpConfigYaml(kc: k8s.KubeConfig): string { console.log("dumping kc:", config); // skipInvalid: true makes dump ignore undefined values - return yaml.safeDump(config, {skipInvalid: true}); + return yaml.safeDump(config, { skipInvalid: true }); } -export function podHasIssues(pod: k8s.V1Pod) { +export function podHasIssues(pod: V1Pod) { // Logic adapted from dashboard const notReady = !!pod.status.conditions.find(condition => { return condition.type == "Ready" && condition.status !== "True" @@ -146,7 +143,7 @@ export function podHasIssues(pod: k8s.V1Pod) { // Logic adapted from dashboard // see: https://github.com/kontena/kontena-k8s-dashboard/blob/7d8f9cb678cc817a22dd1886c5e79415b212b9bf/client/api/endpoints/nodes.api.ts#L147 -export function getNodeWarningConditions(node: k8s.V1Node) { +export function getNodeWarningConditions(node: V1Node) { return node.status.conditions.filter(c => c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades" ) diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index 7123b0158a..dfb7645b1f 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -1,9 +1,9 @@ import { app } from "electron" import fs from "fs" -import { ensureDir, randomFileName} from "./file-helpers" +import { ensureDir, randomFileName } from "./file-helpers" import logger from "./logger" import { Cluster } from "./cluster" -import * as k8s from "./k8s" +import { dumpConfigYaml } from "./k8s" import { KubeConfig } from "@kubernetes/client-node" export class KubeconfigManager { @@ -21,37 +21,35 @@ export class KubeconfigManager { } /** - * Creates new "temporary" kubeconfig that point to the kubectl-proxy. + * Creates new "temporary" kubeconfig that point to the kubectl-proxy. * This way any user of the config does not need to know anything about the auth etc. details. */ protected createTemporaryKubeconfig(): string { - ensureDir(this.configDir) + ensureDir(this.configDir); const path = `${this.configDir}/${randomFileName("kubeconfig")}` - const originalKc = new KubeConfig() - originalKc.loadFromFile(this.cluster.kubeConfigPath) - const kc = { - clusters: [ - { - name: this.cluster.contextName, - server: `http://127.0.0.1:${this.cluster.contextHandler.proxyPort}` - } - ], - users: [ - { - name: "proxy" - } - ], - contexts: [ - { - name: this.cluster.contextName, - cluster: this.cluster.contextName, - namespace: originalKc.getContextObject(this.cluster.contextName).namespace, - user: "proxy" - } - ], - currentContext: this.cluster.contextName - } as KubeConfig - fs.writeFileSync(path, k8s.dumpConfigYaml(kc)) + const { contextName, contextHandler, kubeConfigPath } = this.cluster; + const kubeConfig = new KubeConfig() + kubeConfig.loadFromFile(kubeConfigPath) + kubeConfig.clusters = [ + { + name: contextName, + server: `http://127.0.0.1:${contextHandler.proxyPort}`, + skipTLSVerify: true, + } + ]; + kubeConfig.users = [ + { name: "proxy" }, + ]; + kubeConfig.currentContext = contextName; + kubeConfig.contexts = [ + { + name: contextName, + cluster: contextName, + namespace: kubeConfig.getContextObject(contextName).namespace, + user: "proxy" + } + ]; + fs.writeFileSync(path, dumpConfigYaml(kubeConfig)); return path } diff --git a/src/main/router.ts b/src/main/router.ts index 95d20c7040..ace4a3cead 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -4,14 +4,10 @@ import http from "http" import path from "path" import { readFile } from "fs-extra" import { Cluster } from "./cluster" -import { configRoute } from "./routes/config" import { helmApi } from "./helm-api" import { resourceApplierApi } from "./resource-applier-api" -import { kubeconfigRoute } from "./routes/kubeconfig" -import { metricsRoute } from "./routes/metrics" -import { watchRoute } from "./routes/watch" -import { portForwardRoute } from "./routes/port-forward" -import { apiPrefix, outDir, appName } from "../common/vars"; +import { apiPrefix, appName, outDir } from "../common/vars"; +import { configRoute, kubeconfigRoute, metricsRoute, portForwardRoute, watchRoute } from "./routes"; const mimeTypes: Record = { "html": "text/html", @@ -51,13 +47,13 @@ export class Router { } public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse) { - const url = new URL(req.url, "http://localhost") - const path = url.pathname + const reqUrl = new URL(req.url, "http://localhost") + const path = reqUrl.pathname const method = req.method.toLowerCase() const matchingRoute = this.router.route(method, path) if (matchingRoute.isBoom !== true) { // route() returns error if route not found -> object.isBoom === true - const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params }) + const request = await this.getRequest({ req, res, cluster, url: reqUrl, params: matchingRoute.params }) await matchingRoute.route(request) return true } else { diff --git a/src/main/routes/config.ts b/src/main/routes/config-route.ts similarity index 100% rename from src/main/routes/config.ts rename to src/main/routes/config-route.ts diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts new file mode 100644 index 0000000000..5e0a786974 --- /dev/null +++ b/src/main/routes/index.ts @@ -0,0 +1,5 @@ +export * from "./config-route" +export * from "./kubeconfig-route" +export * from "./metrics-route" +export * from "./port-forward-route" +export * from "./watch-route" \ No newline at end of file diff --git a/src/main/routes/kubeconfig.ts b/src/main/routes/kubeconfig-route.ts similarity index 97% rename from src/main/routes/kubeconfig.ts rename to src/main/routes/kubeconfig-route.ts index 2d77c4faa2..2705c6e319 100644 --- a/src/main/routes/kubeconfig.ts +++ b/src/main/routes/kubeconfig-route.ts @@ -12,7 +12,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster { 'name': cluster.contextName, 'cluster': { - 'server': cluster.apiUrl, + 'server': cluster.apiUrl.href, 'certificate-authority-data': secret.data["ca.crt"] } } diff --git a/src/main/routes/metrics.ts b/src/main/routes/metrics-route.ts similarity index 80% rename from src/main/routes/metrics.ts rename to src/main/routes/metrics-route.ts index ea5fb54c7c..abe36f3b23 100644 --- a/src/main/routes/metrics.ts +++ b/src/main/routes/metrics-route.ts @@ -1,8 +1,7 @@ import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" import requestPromise from "request-promise-native" -import { PrometheusProviderRegistry, PrometheusProvider, PrometheusNodeQuery, PrometheusClusterQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery, PrometheusQueryOpts} from "../prometheus/provider-registry" -import { apiPrefix } from "../../common/vars"; +import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry" export type IMetricsQuery = string | string[] | { [metricName: string]: string; @@ -11,11 +10,10 @@ export type IMetricsQuery = string | string[] | { class MetricsRoute extends LensApi { public async routeMetrics(request: LensApiRequest) { - const { response, cluster} = request + const { response, cluster } = request const query: IMetricsQuery = request.payload; - const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}` - const headers = { - "Host": `${cluster.id}.localhost:${cluster.port}`, + const headers: Record = { + "Host": cluster.apiUrl.host, "Content-type": "application/json", } const queryParams: IMetricsQuery = {} @@ -27,7 +25,7 @@ class MetricsRoute extends LensApi { let prometheusProvider: PrometheusProvider try { const prometheusPath = await cluster.contextHandler.getPrometheusPath() - metricsUrl = `${serverUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range` + metricsUrl = `${cluster.apiServerUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range` prometheusProvider = await cluster.contextHandler.getPrometheusProvider() } catch { this.respondJson(response, {}) @@ -65,11 +63,9 @@ class MetricsRoute extends LensApi { let data: any; if (typeof query === "string") { data = await loadMetrics(query) - } - else if (Array.isArray(query)) { + } else if (Array.isArray(query)) { data = await Promise.all(query.map(loadMetrics)); - } - else { + } else { data = {}; const result = await Promise.all( Object.entries(query).map((queryEntry: any) => { diff --git a/src/main/routes/port-forward.ts b/src/main/routes/port-forward-route.ts similarity index 100% rename from src/main/routes/port-forward.ts rename to src/main/routes/port-forward-route.ts diff --git a/src/main/routes/watch.ts b/src/main/routes/watch-route.ts similarity index 100% rename from src/main/routes/watch.ts rename to src/main/routes/watch-route.ts diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index ab16669d50..a22a3effe7 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,133 +1,101 @@ -import { BrowserView, BrowserWindow, shell } from "electron" import { reaction } from "mobx"; +import { BrowserWindow, shell } from "electron" import windowStateKeeper from "electron-window-state" import type { ClusterId } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store"; -import { tracker } from "../common/tracker"; - -export interface WindowManagerParams { - showSplash?: boolean; -} export class WindowManager { - protected mainWindow: BrowserWindow; - protected splashWindow?: BrowserWindow; - protected windowState: windowStateKeeper.State; - protected views = new Map(); - protected disposers: Function[] = []; + protected views = new Map(); + protected disposers = this.bindReactions(); - constructor(protected params: WindowManagerParams = {}) { - this.params = { showSplash: false, ...params }; + protected splashWindow = new BrowserWindow({ + width: 500, + height: 300, + backgroundColor: "#1e2124", + center: true, + frame: false, + resizable: false, + show: false, + }); - // Manage main window size and position with state persistence - this.windowState = windowStateKeeper({ - defaultHeight: 900, - defaultWidth: 1440, - }); + // Manage main window size and position with state persistence + protected windowState = windowStateKeeper({ + defaultHeight: 900, + defaultWidth: 1440, + }); - this.mainWindow = new BrowserWindow({ - show: false, - x: this.windowState.x, - y: this.windowState.y, - width: this.windowState.width, - height: this.windowState.height, - backgroundColor: "#1e2124", - titleBarStyle: "hidden", - webPreferences: { - nodeIntegration: true, - }, - }); - - // Splash-screen window with loading indicator - this.splashWindow = new BrowserWindow({ - width: 500, - height: 300, - backgroundColor: "#1e2124", - center: true, - frame: false, - resizable: false, - show: false, - }); - this.splashWindow.loadURL("static://splash.html") - - // Hook window state manager into window lifecycle - this.windowState.manage(this.mainWindow); - - // Disallow closing main window - this.mainWindow.on("close", (evt) => { - evt.preventDefault(); - }); - - // Open external links in default browser (target=_blank, window.open) - this.mainWindow.webContents.on("new-window", (event, url) => { - event.preventDefault(); - shell.openExternal(url); - }); - - // Track main window focus - this.mainWindow.on("focus", () => { - tracker.event("app", "focus") - }); - - // Clean up views for removed clusters - this.disposers.push( + protected bindReactions() { + return [ + // auto-destroy cluster-view when it's removed reaction(() => clusterStore.removedClusters.toJS(), removedClusters => { removedClusters.forEach(cluster => { - const lensView = this.getView(cluster.id); - if (lensView) { - lensView.destroy(); - this.views.delete(cluster.id); - } + this.destroyView(cluster.id); }); }) - ); + ] } - async loadURL(url: string) { - if (this.params.showSplash) { - this.splashWindow.show(); - } - await this.mainWindow.loadURL(url); - this.mainWindow.show(); + async showSplash() { + await this.splashWindow.loadURL("static://splash.html") + this.splashWindow.show(); + } + + hideSplash() { this.splashWindow.hide(); - - this.setView("cluster-id-blabla"); } - async setView(clusterId: ClusterId) { - const view = this.getView(clusterId) - this.mainWindow.addBrowserView(view); - // await view.webContents.loadURL("http://ya.ru"); - // view.setBounds({ - // x: 10, - // y: 10, - // width: this.windowState.width - 20, - // height: this.windowState.height - 20, - // }) - // view.setAutoResize({ horizontal: true, vertical: true }); + protected async showView(clusterId: ClusterId) { + const cluster = clusterStore.getById(clusterId); + if (!cluster) { + throw new Error(`Can't load view for non-existing cluster="${clusterId}"`); + } + const view = this.getView(clusterId); + const url = cluster.apiUrl.href; + if (view.webContents.getURL() !== url) { + await view.loadURL(url); + } + view.show(); } - getView(clusterId: ClusterId): BrowserView { + getView(clusterId: ClusterId) { let view = this.views.get(clusterId); if (!view) { - view = new BrowserView({ + view = new BrowserWindow({ + show: false, + x: this.windowState.x, + y: this.windowState.y, + width: this.windowState.width, + height: this.windowState.height, + backgroundColor: "#1e2124", + titleBarStyle: "hidden", webPreferences: { - nodeIntegration: true - } - }) + nodeIntegration: true, + }, + }); + // open external links in default browser (target=_blank, window.open) + view.webContents.on("new-window", (event, url) => { + event.preventDefault(); + shell.openExternal(url); + }); this.views.set(clusterId, view); } return view; } + destroyView(clusterId: ClusterId) { + const view = this.views.get(clusterId); + if (view) { + view.destroy(); + this.views.delete(clusterId); + } + } + destroy() { this.disposers.forEach(dispose => dispose()); this.disposers.length = 0; this.views.forEach(view => view.destroy()); this.views.clear(); - this.mainWindow.destroy(); this.splashWindow.destroy(); - this.mainWindow = null; this.splashWindow = null; } } diff --git a/src/renderer/api/endpoints/config.api.ts b/src/renderer/api/endpoints/config.api.ts index 9a24d3291c..bad571db6e 100644 --- a/src/renderer/api/endpoints/config.api.ts +++ b/src/renderer/api/endpoints/config.api.ts @@ -1,5 +1,5 @@ // App configuration api -import type { IConfigRoutePayload } from "../../../main/routes/config"; +import type { IConfigRoutePayload } from "../../../main/routes/config-route"; import { apiBase } from "../index"; export const configApi = { diff --git a/src/renderer/api/endpoints/metrics.api.ts b/src/renderer/api/endpoints/metrics.api.ts index edb840ba77..81d2f1d500 100644 --- a/src/renderer/api/endpoints/metrics.api.ts +++ b/src/renderer/api/endpoints/metrics.api.ts @@ -2,7 +2,7 @@ import moment from "moment"; import { apiBase } from "../index"; -import type { IMetricsQuery } from "../../../main/routes/metrics"; +import type { IMetricsQuery } from "../../../main/routes/metrics-route"; export interface IMetrics { status: string; diff --git a/src/renderer/config.store.ts b/src/renderer/config.store.ts index f1fd1874aa..7de80807ac 100755 --- a/src/renderer/config.store.ts +++ b/src/renderer/config.store.ts @@ -1,4 +1,4 @@ -import type { IConfigRoutePayload } from "../main/routes/config"; +import type { IConfigRoutePayload } from "../main/routes/config-route"; import { observable, when } from "mobx"; import { autobind, interval } from "./utils";