1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

window-manager refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-09 20:15:15 +03:00
parent ce1cccc965
commit cac4896517
16 changed files with 179 additions and 238 deletions

View File

@ -1,14 +1,15 @@
import url from "url"
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature" import type { FeatureStatusMap } from "./feature"
import { observable, toJS } from "mobx"; import { computed, observable, toJS } from "mobx";
import { apiPrefix } from "../common/vars"; import { apiPrefix } from "../common/vars";
import { ContextHandler } from "./context-handler" import { ContextHandler } from "./context-handler"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
import { Kubectl } from "./kubectl"; import { Kubectl } from "./kubectl";
import { KubeconfigManager } from "./kubeconfig-manager" 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 { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager";
import request from "request-promise-native" import request, { RequestPromiseOptions } from "request-promise-native"
import logger from "./logger" import logger from "./logger"
enum ClusterStatus { enum ClusterStatus {
@ -43,7 +44,6 @@ export class Cluster implements ClusterModel {
@observable contextName: string; @observable contextName: string;
@observable url: string; @observable url: string;
@observable port: number; @observable port: number;
@observable apiUrl: string;
@observable online: boolean; @observable online: boolean;
@observable accessible: boolean; @observable accessible: boolean;
@observable failureReason: string; @observable failureReason: string;
@ -63,21 +63,28 @@ export class Cluster implements ClusterModel {
Object.assign(this, model) 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) { async init(port: number) {
const { contextName } = this
try { try {
const kubeConfig = loadConfig(this.kubeConfigPath)
kubeConfig.setCurrentContext(contextName); // fixme: is it required, when if so?
this.port = port; this.port = port;
this.apiUrl = kubeConfig.getCurrentCluster().server this.contextHandler = new ContextHandler(this);
this.contextHandler = new ContextHandler(kubeConfig, this)
await this.contextHandler.init() // So we get the proxy port reserved await this.contextHandler.init() // So we get the proxy port reserved
this.kubeconfigManager = new KubeconfigManager(this) this.kubeconfigManager = new KubeconfigManager(this)
this.url = this.contextHandler.url this.url = this.contextHandler.url
this.initialized = true; 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) { } 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 || "" return this.preferences.prometheus?.prefix || ""
} }
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) { k8sRequest(path: string, options: RequestPromiseOptions = {}) {
const options = Object.assign({ return request(this.apiServerUrl + path, {
json: true, json: true,
timeout: 10000 timeout: 10000,
}, (opts || {})) headers: {
if (!options.headers) { ...(options.headers || {}),
options.headers = {} host: this.apiUrl.host,
} }
options.headers.host = `${this.id}.localhost:${this.port}` })
return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options)
} }
protected async getConnectionStatus() { protected async getConnectionStatus() {
@ -202,8 +208,8 @@ export class Cluster implements ClusterModel {
if (kubernetesVersion.includes("gke")) return "gke" if (kubernetesVersion.includes("gke")) return "gke"
if (kubernetesVersion.includes("eks")) return "eks" if (kubernetesVersion.includes("eks")) return "eks"
if (kubernetesVersion.includes("IKS")) return "iks" if (kubernetesVersion.includes("IKS")) return "iks"
if (apiUrl.endsWith("azmk8s.io")) return "aks" if (apiUrl.href.endsWith("azmk8s.io")) return "aks"
if (apiUrl.endsWith("k8s.ondigitalocean.com")) return "digitalocean" if (apiUrl.href.endsWith("k8s.ondigitalocean.com")) return "digitalocean"
if (contextName.startsWith("minikube")) return "minikube" if (contextName.startsWith("minikube")) return "minikube"
if (kubernetesVersion.includes("+")) return "custom" if (kubernetesVersion.includes("+")) return "custom"
return "vanilla" return "vanilla"
@ -271,7 +277,7 @@ export class Cluster implements ClusterModel {
return toJS({ return toJS({
...storeModel, ...storeModel,
url: this.url, url: this.url,
apiUrl: this.apiUrl, apiUrl: this.apiUrl.href,
online: this.online, online: this.online,
accessible: this.accessible, accessible: this.accessible,
failureReason: this.failureReason, failureReason: this.failureReason,

View File

@ -1,6 +1,5 @@
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node" import { CoreV1Api } from "@kubernetes/client-node"
import { ServerOptions } from "http-proxy" import { ServerOptions } from "http-proxy"
import * as url from "url"
import logger from "./logger" import logger from "./logger"
import { getFreePort } from "./port" import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy" import { KubeAuthProxy } from "./kube-auth-proxy"
@ -15,31 +14,21 @@ export class ContextHandler {
public contextName: string public contextName: string
protected id: string protected id: string
protected clusterUrl: url.UrlWithStringQuery
protected proxyServer: KubeAuthProxy protected proxyServer: KubeAuthProxy
protected apiTarget: ServerOptions
protected certData: string protected certData: string
protected authCertData: string protected authCertData: string
protected cluster: Cluster
protected apiTarget: ServerOptions
protected proxyTarget: ServerOptions protected proxyTarget: ServerOptions
protected clientCert: string protected clientCert: string
protected clientKey: string protected clientKey: string
protected secureApiConnection = true
protected defaultNamespace: string
protected kubernetesApi: string
protected prometheusProvider: string protected prometheusProvider: string
protected prometheusPath: string protected prometheusPath: string
protected clusterName: string protected clusterName: string
constructor(kc: KubeConfig, cluster: Cluster) { constructor(protected cluster: Cluster) {
this.id = cluster.id this.id = cluster.id
this.cluster = cluster this.url = cluster.apiUrl.href;
this.clusterUrl = url.parse(cluster.apiUrl)
this.contextName = cluster.contextName; 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) this.setClusterPreferences(cluster.preferences)
} }
@ -79,16 +68,12 @@ export class ContextHandler {
return await provider.getPrometheusService(apiClient) return await provider.getPrometheusService(apiClient)
}) })
const resolvedPrometheusServices = await Promise.all(prometheusPromises) const resolvedPrometheusServices = await Promise.all(prometheusPromises)
const service = resolvedPrometheusServices.filter(n => n)[0] const service = resolvedPrometheusServices.filter(n => n)[0];
if (service) { return service || {
return service id: "lens",
} else { namespace: "lens-metrics",
return { service: "prometheus",
id: "lens", port: 80
namespace: "lens-metrics",
service: "prometheus",
port: 80
}
} }
} }
@ -112,17 +97,18 @@ export class ContextHandler {
} }
protected async newApiTarget(timeout: number): Promise<ServerOptions> { protected async newApiTarget(timeout: number): Promise<ServerOptions> {
const clusterUrl = this.cluster.apiUrl;
return { return {
changeOrigin: true, changeOrigin: true,
timeout: timeout, timeout: timeout,
headers: { headers: {
"Host": this.clusterUrl.hostname "Host": clusterUrl.hostname
}, },
target: { target: {
port: await this.resolveProxyPort(), port: await this.resolveProxyPort(),
protocol: "http://", protocol: "http://",
host: "localhost", host: "localhost",
path: this.clusterUrl.path path: clusterUrl.path,
}, },
} }
} }

View File

@ -5,7 +5,6 @@ import "../common/prometheus-providers"
import { app, dialog } from "electron" import { app, dialog } from "electron"
import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars"; import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars";
import path from "path" import path from "path"
import { format as formatUrl } from "url"
import initMenu from "./menu" import initMenu from "./menu"
import { LensProxy, listen } from "./proxy" import { LensProxy, listen } from "./proxy"
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
@ -25,12 +24,6 @@ let windowManager: WindowManager;
let clusterManager: ClusterManager; let clusterManager: ClusterManager;
let proxyServer: LensProxy; let proxyServer: LensProxy;
const vmURL = formatUrl({
pathname: path.join(__dirname, `${appName}.html`),
protocol: "file",
slashes: true,
})
mangleProxyEnv() mangleProxyEnv()
if (app.commandLine.getSwitchValue("proxy-server") !== "") { if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server") process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
@ -81,9 +74,9 @@ async function main() {
app.quit(); app.quit();
} }
// manage lens windows // create window manager and open app
windowManager = new WindowManager({showSplash: true}); windowManager = new WindowManager();
windowManager.loadURL(vmURL) windowManager.showSplash();
} }
// Events // Events
@ -95,19 +88,15 @@ app.on('window-all-closed', function () {
if (!isMac) { if (!isMac) {
app.quit(); app.quit();
} else { } else {
windowManager = null // windowManager.destroy();
if (clusterManager) clusterManager.stop() // clusterManager.stop()
} }
}) })
app.on("activate", () => { app.on("activate", (event, hasVisibleWindows) => {
if (!windowManager) { // todo: something
windowManager = new WindowManager()
windowManager.loadURL(vmURL)
}
}) })
// fixme: app can't quit normally (Cmd+W/Q not working)
app.on("will-quit", async (event) => { app.on("will-quit", async (event) => {
event.preventDefault(); // To allow mixpanel sending to be executed event.preventDefault(); // To allow mixpanel sending to be executed
if (clusterManager) clusterManager.stop() if (clusterManager) clusterManager.stop()

View File

@ -1,4 +1,4 @@
import k8s from "@kubernetes/client-node" import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"
import os from "os" import os from "os"
import yaml from "js-yaml" import yaml from "js-yaml"
import logger from "./logger"; import logger from "./logger";
@ -10,8 +10,8 @@ function resolveTilde(filePath: string) {
return filePath; return filePath;
} }
export function loadConfig(kubeConfigPath?: string): k8s.KubeConfig { export function loadConfig(kubeConfigPath?: string): KubeConfig {
const kc = new k8s.KubeConfig() const kc = new KubeConfig()
if (kubeConfigPath) { if (kubeConfigPath) {
kc.loadFromFile(resolveTilde(kubeConfigPath)) kc.loadFromFile(resolveTilde(kubeConfigPath))
} else { } else {
@ -27,38 +27,35 @@ export function loadConfig(kubeConfigPath?: string): k8s.KubeConfig {
* - Context * - Context
* @param config KubeConfig to check * @param config KubeConfig to check
*/ */
export function validateConfig(config: k8s.KubeConfig | string): k8s.KubeConfig { export function validateConfig(config: KubeConfig | string): KubeConfig {
if(typeof config == "string") { if (typeof config == "string") {
config = loadConfig(config); config = loadConfig(config);
} }
logger.debug(`validating kube config: ${JSON.stringify(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") 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") 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") throw new Error("No contexts provided in config")
} }
return config return config
} }
/** /**
* Breaks kube config into several configs. Each context as it own KubeConfig object * 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[] { export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
const configs: k8s.KubeConfig[] = [] const configs: KubeConfig[] = []
if(!kubeConfig.contexts) { if (!kubeConfig.contexts) {
return configs; return configs;
} }
kubeConfig.contexts.forEach(ctx => { kubeConfig.contexts.forEach(ctx => {
const kc = new k8s.KubeConfig(); const kc = new KubeConfig();
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n); kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n) kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n)
kc.contexts = [kubeConfig.getContextObject(ctx.name)].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 * @param configPath path to kube config yaml file
*/ */
export function loadAndSplitConfig(configPath: string): k8s.KubeConfig[] { export function loadAndSplitConfig(configPath: string): KubeConfig[] {
const allConfigs = new k8s.KubeConfig(); const allConfigs = new KubeConfig();
allConfigs.loadFromFile(configPath); allConfigs.loadFromFile(configPath);
return splitConfig(allConfigs); return splitConfig(allConfigs);
} }
export function dumpConfigYaml(kc: k8s.KubeConfig): string { export function dumpConfigYaml(kc: KubeConfig): string {
const config = { const config = {
apiVersion: "v1", apiVersion: "v1",
kind: "Config", kind: "Config",
@ -128,10 +125,10 @@ export function dumpConfigYaml(kc: k8s.KubeConfig): string {
console.log("dumping kc:", config); console.log("dumping kc:", config);
// skipInvalid: true makes dump ignore undefined values // 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 // Logic adapted from dashboard
const notReady = !!pod.status.conditions.find(condition => { const notReady = !!pod.status.conditions.find(condition => {
return condition.type == "Ready" && condition.status !== "True" return condition.type == "Ready" && condition.status !== "True"
@ -146,7 +143,7 @@ export function podHasIssues(pod: k8s.V1Pod) {
// Logic adapted from dashboard // Logic adapted from dashboard
// see: https://github.com/kontena/kontena-k8s-dashboard/blob/7d8f9cb678cc817a22dd1886c5e79415b212b9bf/client/api/endpoints/nodes.api.ts#L147 // 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 => return node.status.conditions.filter(c =>
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades" c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
) )

View File

@ -1,9 +1,9 @@
import { app } from "electron" import { app } from "electron"
import fs from "fs" import fs from "fs"
import { ensureDir, randomFileName} from "./file-helpers" import { ensureDir, randomFileName } from "./file-helpers"
import logger from "./logger" import logger from "./logger"
import { Cluster } from "./cluster" import { Cluster } from "./cluster"
import * as k8s from "./k8s" import { dumpConfigYaml } from "./k8s"
import { KubeConfig } from "@kubernetes/client-node" import { KubeConfig } from "@kubernetes/client-node"
export class KubeconfigManager { 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. * This way any user of the config does not need to know anything about the auth etc. details.
*/ */
protected createTemporaryKubeconfig(): string { protected createTemporaryKubeconfig(): string {
ensureDir(this.configDir) ensureDir(this.configDir);
const path = `${this.configDir}/${randomFileName("kubeconfig")}` const path = `${this.configDir}/${randomFileName("kubeconfig")}`
const originalKc = new KubeConfig() const { contextName, contextHandler, kubeConfigPath } = this.cluster;
originalKc.loadFromFile(this.cluster.kubeConfigPath) const kubeConfig = new KubeConfig()
const kc = { kubeConfig.loadFromFile(kubeConfigPath)
clusters: [ kubeConfig.clusters = [
{ {
name: this.cluster.contextName, name: contextName,
server: `http://127.0.0.1:${this.cluster.contextHandler.proxyPort}` server: `http://127.0.0.1:${contextHandler.proxyPort}`,
} skipTLSVerify: true,
], }
users: [ ];
{ kubeConfig.users = [
name: "proxy" { name: "proxy" },
} ];
], kubeConfig.currentContext = contextName;
contexts: [ kubeConfig.contexts = [
{ {
name: this.cluster.contextName, name: contextName,
cluster: this.cluster.contextName, cluster: contextName,
namespace: originalKc.getContextObject(this.cluster.contextName).namespace, namespace: kubeConfig.getContextObject(contextName).namespace,
user: "proxy" user: "proxy"
} }
], ];
currentContext: this.cluster.contextName fs.writeFileSync(path, dumpConfigYaml(kubeConfig));
} as KubeConfig
fs.writeFileSync(path, k8s.dumpConfigYaml(kc))
return path return path
} }

View File

@ -4,14 +4,10 @@ import http from "http"
import path from "path" import path from "path"
import { readFile } from "fs-extra" import { readFile } from "fs-extra"
import { Cluster } from "./cluster" import { Cluster } from "./cluster"
import { configRoute } from "./routes/config"
import { helmApi } from "./helm-api" import { helmApi } from "./helm-api"
import { resourceApplierApi } from "./resource-applier-api" import { resourceApplierApi } from "./resource-applier-api"
import { kubeconfigRoute } from "./routes/kubeconfig" import { apiPrefix, appName, outDir } from "../common/vars";
import { metricsRoute } from "./routes/metrics" import { configRoute, kubeconfigRoute, metricsRoute, portForwardRoute, watchRoute } from "./routes";
import { watchRoute } from "./routes/watch"
import { portForwardRoute } from "./routes/port-forward"
import { apiPrefix, outDir, appName } from "../common/vars";
const mimeTypes: Record<string, string> = { const mimeTypes: Record<string, string> = {
"html": "text/html", "html": "text/html",
@ -51,13 +47,13 @@ export class Router {
} }
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse) { public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse) {
const url = new URL(req.url, "http://localhost") const reqUrl = new URL(req.url, "http://localhost")
const path = url.pathname const path = reqUrl.pathname
const method = req.method.toLowerCase() const method = req.method.toLowerCase()
const matchingRoute = this.router.route(method, path) const matchingRoute = this.router.route(method, path)
if (matchingRoute.isBoom !== true) { // route() returns error if route not found -> object.isBoom === true 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) await matchingRoute.route(request)
return true return true
} else { } else {

5
src/main/routes/index.ts Normal file
View File

@ -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"

View File

@ -12,7 +12,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
{ {
'name': cluster.contextName, 'name': cluster.contextName,
'cluster': { 'cluster': {
'server': cluster.apiUrl, 'server': cluster.apiUrl.href,
'certificate-authority-data': secret.data["ca.crt"] 'certificate-authority-data': secret.data["ca.crt"]
} }
} }

View File

@ -1,8 +1,7 @@
import { LensApiRequest } from "../router" import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" import { LensApi } from "../lens-api"
import requestPromise from "request-promise-native" import requestPromise from "request-promise-native"
import { PrometheusProviderRegistry, PrometheusProvider, PrometheusNodeQuery, PrometheusClusterQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery, PrometheusQueryOpts} from "../prometheus/provider-registry" import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry"
import { apiPrefix } from "../../common/vars";
export type IMetricsQuery = string | string[] | { export type IMetricsQuery = string | string[] | {
[metricName: string]: string; [metricName: string]: string;
@ -11,11 +10,10 @@ export type IMetricsQuery = string | string[] | {
class MetricsRoute extends LensApi { class MetricsRoute extends LensApi {
public async routeMetrics(request: LensApiRequest) { public async routeMetrics(request: LensApiRequest) {
const { response, cluster} = request const { response, cluster } = request
const query: IMetricsQuery = request.payload; const query: IMetricsQuery = request.payload;
const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}` const headers: Record<string, string> = {
const headers = { "Host": cluster.apiUrl.host,
"Host": `${cluster.id}.localhost:${cluster.port}`,
"Content-type": "application/json", "Content-type": "application/json",
} }
const queryParams: IMetricsQuery = {} const queryParams: IMetricsQuery = {}
@ -27,7 +25,7 @@ class MetricsRoute extends LensApi {
let prometheusProvider: PrometheusProvider let prometheusProvider: PrometheusProvider
try { try {
const prometheusPath = await cluster.contextHandler.getPrometheusPath() 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() prometheusProvider = await cluster.contextHandler.getPrometheusProvider()
} catch { } catch {
this.respondJson(response, {}) this.respondJson(response, {})
@ -65,11 +63,9 @@ class MetricsRoute extends LensApi {
let data: any; let data: any;
if (typeof query === "string") { if (typeof query === "string") {
data = await loadMetrics(query) data = await loadMetrics(query)
} } else if (Array.isArray(query)) {
else if (Array.isArray(query)) {
data = await Promise.all(query.map(loadMetrics)); data = await Promise.all(query.map(loadMetrics));
} } else {
else {
data = {}; data = {};
const result = await Promise.all( const result = await Promise.all(
Object.entries(query).map((queryEntry: any) => { Object.entries(query).map((queryEntry: any) => {

View File

@ -1,133 +1,101 @@
import { BrowserView, BrowserWindow, shell } from "electron"
import { reaction } from "mobx"; import { reaction } from "mobx";
import { BrowserWindow, shell } from "electron"
import windowStateKeeper from "electron-window-state" import windowStateKeeper from "electron-window-state"
import type { ClusterId } from "../common/cluster-store"; import type { ClusterId } from "../common/cluster-store";
import { clusterStore } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store";
import { tracker } from "../common/tracker";
export interface WindowManagerParams {
showSplash?: boolean;
}
export class WindowManager { export class WindowManager {
protected mainWindow: BrowserWindow; protected views = new Map<ClusterId, BrowserWindow>();
protected splashWindow?: BrowserWindow; protected disposers = this.bindReactions();
protected windowState: windowStateKeeper.State;
protected views = new Map<ClusterId, BrowserView>();
protected disposers: Function[] = [];
constructor(protected params: WindowManagerParams = {}) { protected splashWindow = new BrowserWindow({
this.params = { showSplash: false, ...params }; width: 500,
height: 300,
backgroundColor: "#1e2124",
center: true,
frame: false,
resizable: false,
show: false,
});
// Manage main window size and position with state persistence // Manage main window size and position with state persistence
this.windowState = windowStateKeeper({ protected windowState = windowStateKeeper({
defaultHeight: 900, defaultHeight: 900,
defaultWidth: 1440, defaultWidth: 1440,
}); });
this.mainWindow = new BrowserWindow({ protected bindReactions() {
show: false, return [
x: this.windowState.x, // auto-destroy cluster-view when it's removed
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(
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => { reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
removedClusters.forEach(cluster => { removedClusters.forEach(cluster => {
const lensView = this.getView(cluster.id); this.destroyView(cluster.id);
if (lensView) {
lensView.destroy();
this.views.delete(cluster.id);
}
}); });
}) })
); ]
} }
async loadURL(url: string) { async showSplash() {
if (this.params.showSplash) { await this.splashWindow.loadURL("static://splash.html")
this.splashWindow.show(); this.splashWindow.show();
} }
await this.mainWindow.loadURL(url);
this.mainWindow.show(); hideSplash() {
this.splashWindow.hide(); this.splashWindow.hide();
this.setView("cluster-id-blabla");
} }
async setView(clusterId: ClusterId) { protected async showView(clusterId: ClusterId) {
const view = this.getView(clusterId) const cluster = clusterStore.getById(clusterId);
this.mainWindow.addBrowserView(view); if (!cluster) {
// await view.webContents.loadURL("http://ya.ru"); throw new Error(`Can't load view for non-existing cluster="${clusterId}"`);
// view.setBounds({ }
// x: 10, const view = this.getView(clusterId);
// y: 10, const url = cluster.apiUrl.href;
// width: this.windowState.width - 20, if (view.webContents.getURL() !== url) {
// height: this.windowState.height - 20, await view.loadURL(url);
// }) }
// view.setAutoResize({ horizontal: true, vertical: true }); view.show();
} }
getView(clusterId: ClusterId): BrowserView { getView(clusterId: ClusterId) {
let view = this.views.get(clusterId); let view = this.views.get(clusterId);
if (!view) { 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: { 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); this.views.set(clusterId, view);
} }
return view; return view;
} }
destroyView(clusterId: ClusterId) {
const view = this.views.get(clusterId);
if (view) {
view.destroy();
this.views.delete(clusterId);
}
}
destroy() { destroy() {
this.disposers.forEach(dispose => dispose()); this.disposers.forEach(dispose => dispose());
this.disposers.length = 0; this.disposers.length = 0;
this.views.forEach(view => view.destroy()); this.views.forEach(view => view.destroy());
this.views.clear(); this.views.clear();
this.mainWindow.destroy();
this.splashWindow.destroy(); this.splashWindow.destroy();
this.mainWindow = null;
this.splashWindow = null; this.splashWindow = null;
} }
} }

View File

@ -1,5 +1,5 @@
// App configuration api // App configuration api
import type { IConfigRoutePayload } from "../../../main/routes/config"; import type { IConfigRoutePayload } from "../../../main/routes/config-route";
import { apiBase } from "../index"; import { apiBase } from "../index";
export const configApi = { export const configApi = {

View File

@ -2,7 +2,7 @@
import moment from "moment"; import moment from "moment";
import { apiBase } from "../index"; import { apiBase } from "../index";
import type { IMetricsQuery } from "../../../main/routes/metrics"; import type { IMetricsQuery } from "../../../main/routes/metrics-route";
export interface IMetrics { export interface IMetrics {
status: string; status: string;

View File

@ -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 { observable, when } from "mobx";
import { autobind, interval } from "./utils"; import { autobind, interval } from "./utils";