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:
parent
ce1cccc965
commit
cac4896517
@ -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,
|
||||
|
||||
@ -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<ServerOptions> {
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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<string, string> = {
|
||||
"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 {
|
||||
|
||||
5
src/main/routes/index.ts
Normal file
5
src/main/routes/index.ts
Normal 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"
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
@ -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<string, string> = {
|
||||
"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) => {
|
||||
@ -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<ClusterId, BrowserView>();
|
||||
protected disposers: Function[] = [];
|
||||
protected views = new Map<ClusterId, BrowserWindow>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user