Lens restructure (#540)

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman authored and GitHub committed 2020-06-30 14:35:16 +03:00
1 parent e810eab39f
commit b7974827d2
786 files changed
+14567 -24066

No files matched your search

+14 -20
View File
@@ -1,19 +1,18 @@
import { KubeConfig } from "@kubernetes/client-node"
import { PromiseIpc } from "electron-promise-ipc"
import * as http from "http"
import http from "http"
import { Cluster, ClusterBaseInfo } from "./cluster"
import { clusterStore } from "../common/cluster-store"
import * as k8s from "./k8s"
import logger from "./logger"
import { LensProxy } from "./proxy"
import { app } from "electron"
import * as path from "path"
import path from "path"
import { promises } from "fs"
import { ensureDir } from "fs-extra"
import * as filenamify from "filenamify"
import filenamify from "filenamify"
import { v4 as uuid } from "uuid"
declare const __static: string;
import { apiPrefix } from "../common/vars";
export type FeatureInstallRequest = {
name: string;
@@ -92,7 +91,7 @@ export class ClusterManager {
reject("No cluster contexts defined")
}
configs.forEach(c => {
k8s.valideConfig(c)
k8s.validateConfig(c)
const cluster = new Cluster({
id: uuid(),
port: this.port,
@@ -117,15 +116,15 @@ export class ClusterManager {
logger.debug(`IPC: addCluster`)
const cluster = await this.addNewCluster(clusterData)
return {
addedCluster: this.clusterResponse(cluster),
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => this.clusterResponse(cluster))
addedCluster: cluster.toClusterInfo(),
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => cluster.toClusterInfo())
}
});
this.promiseIpc.on("getClusters", async (workspaceId: string) => {
logger.debug(`IPC: getClusters, workspace ${workspaceId}`)
const workspaceClusters = Array.from(this.getClusters()).filter((cluster) => cluster.workspace === workspaceId)
return workspaceClusters.map((cluster: Cluster) => this.clusterResponse(cluster))
return workspaceClusters.map((cluster: Cluster) => cluster.toClusterInfo())
});
this.promiseIpc.on("getCluster", async (id: string) => {
@@ -133,7 +132,7 @@ export class ClusterManager {
const cluster = this.getCluster(id)
if (cluster) {
await cluster.refreshCluster()
return this.clusterResponse(cluster)
return cluster.toClusterInfo()
} else {
return null
}
@@ -181,7 +180,7 @@ export class ClusterManager {
if(!cluster.preferences) cluster.preferences = {};
cluster.preferences.icon = clusterIcon
clusterStore.storeCluster(cluster);
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
return {success: true, cluster: cluster.toClusterInfo(), message: ""}
} catch(error) {
return {success: false, message: error}
}
@@ -193,7 +192,7 @@ export class ClusterManager {
if (cluster && cluster.preferences) {
cluster.preferences.icon = null;
clusterStore.storeCluster(cluster)
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
return {success: true, cluster: cluster.toClusterInfo(), message: ""}
} else {
return {success: false, message: "Cluster not found"}
}
@@ -202,7 +201,7 @@ export class ClusterManager {
this.promiseIpc.on("refreshCluster", async (clusterId: string) => {
const cluster = this.clusters.get(clusterId)
await cluster.refreshCluster()
return this.clusterResponse(cluster)
return cluster.toClusterInfo()
});
this.promiseIpc.on("stopCluster", (clusterId: string) => {
@@ -217,7 +216,7 @@ export class ClusterManager {
this.promiseIpc.on("removeCluster", (ctx: string) => {
logger.debug(`IPC: removeCluster: ${ctx}`)
return this.removeCluster(ctx).map((cluster: Cluster) => this.clusterResponse(cluster))
return this.removeCluster(ctx).map((cluster: Cluster) => cluster.toClusterInfo())
});
this.promiseIpc.on("clusterStored", (clusterId: string) => {
@@ -263,7 +262,7 @@ export class ClusterManager {
cluster = this.clusters.get(clusterId)
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, "/api-kube")
req.url = req.url.replace(`/${clusterId}`, apiPrefix.KUBE_BASE)
}
}
} else {
@@ -274,11 +273,6 @@ export class ClusterManager {
return cluster;
}
// TODO: remove this
protected clusterResponse(cluster: Cluster) {
return cluster.toClusterInfo()
}
protected async uploadClusterIcon(cluster: Cluster, fileName: string, src: string): Promise<string> {
await ensureDir(ClusterManager.clusterIconDir)
fileName = filenamify(cluster.contextName + "-" + fileName)
+31 -34
View File
@@ -3,12 +3,13 @@ import { FeatureStatusMap } from "./feature"
import * as k8s from "./k8s"
import { clusterStore } from "../common/cluster-store"
import logger from "./logger"
import { KubeConfig, CoreV1Api, AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
import * as fm from "./feature-manager";
import { Kubectl } from "./kubectl";
import { PromiseIpc } from "electron-promise-ipc"
import * as request from "request-promise-native"
import request from "request-promise-native"
import { KubeconfigManager } from "./kubeconfig-manager"
import { apiPrefix } from "../common/vars";
enum ClusterStatus {
AccessGranted = 2,
@@ -123,16 +124,9 @@ export class Cluster implements ClusterInfo {
this.contextHandler.setClusterPreferences(this.preferences)
const connectionStatus = await this.getConnectionStatus()
if (connectionStatus == ClusterStatus.AccessGranted) {
this.accessible = true
} else {
this.accessible = false
}
if (connectionStatus > ClusterStatus.Offline) {
this.online = true
} else {
this.online = false
}
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
this.online = connectionStatus > ClusterStatus.Offline;
if (this.accessible) {
this.distribution = this.detectKubernetesDistribution(this.version)
this.features = await fm.getFeatures(this.contextHandler)
@@ -146,7 +140,9 @@ export class Cluster implements ClusterInfo {
public updateKubeconfig(kubeconfig: string) {
const storedCluster = clusterStore.getCluster(this.id)
if (!storedCluster) { return }
if (!storedCluster) {
return
}
this.kubeConfig = kubeconfig
this.save()
@@ -164,7 +160,7 @@ export class Cluster implements ClusterInfo {
}
public toClusterInfo(): ClusterInfo {
const clusterInfo: ClusterInfo = {
return {
id: this.id,
workspace: this.workspace,
url: this.url,
@@ -179,20 +175,19 @@ export class Cluster implements ClusterInfo {
isAdmin: this.isAdmin,
features: this.features,
kubeCtl: this.kubeCtl,
kubeConfig: this.kubeConfig,
kubeConfig: this.kubeConfig,
preferences: this.preferences
}
return clusterInfo;
}
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) {
const options = Object.assign({
json: true, timeout: 10000
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}/api-kube${path}`, options)
return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options)
}
protected async getConnectionStatus() {
@@ -201,21 +196,24 @@ export class Cluster implements ClusterInfo {
this.version = response.gitVersion
this.failureReason = null
return ClusterStatus.AccessGranted;
} catch(error) {
} catch (error) {
logger.error(`Failed to connect to cluster ${this.contextName}: ${JSON.stringify(error)}`)
if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials";
return ClusterStatus.AccessDenied;
} else {
}
else {
this.failureReason = error.error || error.message;
return ClusterStatus.Offline;
}
} else if (error.failed === true) {
}
else if (error.failed === true) {
if (error.timedOut === true) {
this.failureReason = "Connection timed out";
return ClusterStatus.Offline;
} else {
}
else {
this.failureReason = "Failed to fetch credentials";
return ClusterStatus.AccessDenied;
}
@@ -234,7 +232,7 @@ export class Cluster implements ClusterInfo {
spec: { resourceAttributes }
})
return accessReview.body.status.allowed === true
} catch(error) {
} catch (error) {
logger.error(`failed to request selfSubjectAccessReview: ${error.message}`)
return false
}
@@ -258,10 +256,10 @@ export class Cluster implements ClusterInfo {
else if (kubernetesVersion.includes("IKS")) {
return "iks"
}
else if(this.apiUrl.endsWith("azmk8s.io")) {
else if (this.apiUrl.endsWith("azmk8s.io")) {
return "aks"
}
else if(this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
else if (this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
return "digitalocean"
}
else if (this.contextHandler.contextName.startsWith("minikube")) {
@@ -274,11 +272,11 @@ export class Cluster implements ClusterInfo {
return "vanilla"
}
protected async getNodeCount() {
protected async getNodeCount() {
try {
const response = await this.k8sRequest("/api/v1/nodes")
return response.items.length
} catch(error) {
} catch (error) {
logger.debug(`failed to request node list: ${error.message}`)
return null
}
@@ -294,18 +292,17 @@ export class Cluster implements ClusterInfo {
const uniqEventSources = new Set();
const warnings = response.body.items.filter(e => e.type !== 'Normal');
for (const w of warnings) {
if(w.involvedObject.kind === 'Pod') {
if (w.involvedObject.kind === 'Pod') {
try {
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
if(k8s.podHasIssues(pod)) {
if (k8s.podHasIssues(pod)) {
uniqEventSources.add(w.involvedObject.uid);
}
continue;
} catch (error) {
continue;
} catch (err) {
}
} else {
}
else {
uniqEventSources.add(w.involvedObject.uid);
}
}
+18 -19
View File
@@ -1,6 +1,5 @@
import { KubeConfig, CoreV1Api } from "@kubernetes/client-node"
import { readFileSync } from "fs"
import * as http from "http"
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"
import http from "http"
import { ServerOptions } from "http-proxy"
import * as url from "url"
import logger from "./logger"
@@ -8,8 +7,7 @@ import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy"
import { Cluster, ClusterPreferences } from "./cluster"
import { prometheusProviders } from "../common/prometheus-providers"
import { PrometheusService, PrometheusProvider } from "./prometheus/provider-registry"
import { PrometheusLens } from "./prometheus/lens"
import { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
export class ContextHandler {
public contextName: string
@@ -76,19 +74,21 @@ export class ContextHandler {
if (clusterPreferences && clusterPreferences.prometheus) {
const prom = clusterPreferences.prometheus
this.prometheusPath = `${prom.namespace}/services/${prom.service}:${prom.port}`
} else {
}
else {
this.prometheusPath = null
}
if(clusterPreferences && clusterPreferences.clusterName) {
if (clusterPreferences && clusterPreferences.clusterName) {
this.clusterName = clusterPreferences.clusterName;
} else {
}
else {
this.clusterName = this.contextName;
}
}
protected async resolvePrometheusPath(): Promise<string> {
const service = await this.getPrometheusService()
return `${service.namespace}/services/${service.service}:${service.port}`
const {service, namespace, port} = await this.getPrometheusService()
return `${namespace}/services/${service}:${port}`
}
public async getPrometheusProvider() {
@@ -110,7 +110,8 @@ export class ContextHandler {
const service = resolvedPrometheusServices.filter(n => n)[0]
if (service) {
return service
} else {
}
else {
return {
id: "lens",
namespace: "lens-metrics",
@@ -128,7 +129,7 @@ export class ContextHandler {
return this.prometheusPath
}
public async getApiTarget(isWatchRequest = false) {
public async getApiTarget(isWatchRequest = false): Promise<ServerOptions> {
if (this.apiTarget && !isWatchRequest) {
return this.apiTarget
}
@@ -140,7 +141,7 @@ export class ContextHandler {
return apiTarget
}
protected async newApiTarget(timeout: number) {
protected async newApiTarget(timeout: number): Promise<ServerOptions> {
return {
changeOrigin: true,
timeout: timeout,
@@ -162,7 +163,7 @@ export class ContextHandler {
let serverPort: number = null
try {
serverPort = await getFreePort()
} catch(error) {
} catch (error) {
logger.error(error)
throw(error)
}
@@ -178,7 +179,7 @@ export class ContextHandler {
public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) {
try {
await callback(this.cluster.kubeconfigPath())
} catch(error) {
} catch (error) {
throw(error)
}
}
@@ -187,7 +188,7 @@ export class ContextHandler {
if (!this.proxyServer) {
const proxyPort = await this.resolveProxyPort()
const proxyEnv = Object.assign({}, process.env)
if (this.cluster.preferences && this.cluster.preferences.httpsProxy) {
if (this.cluster?.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
}
this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv)
@@ -203,8 +204,6 @@ export class ContextHandler {
}
public proxyServerError() {
if (!this.proxyServer) { return null }
return this.proxyServer.lastError
return this.proxyServer?.lastError || ""
}
}
+7 -3
View File
@@ -1,5 +1,5 @@
import * as fs from "fs";
import * as path from "path"
import fs from "fs";
import path from "path"
import * as hb from "handlebars"
import { ResourceApplier } from "./resource-applier"
import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node"
@@ -99,6 +99,10 @@ export abstract class Feature {
}
protected manifestPath() {
return path.join(__dirname, '..', 'features', this.name);
const devPath = path.join(__dirname, "..", 'src/features', this.name);
if(fs.existsSync(devPath)) {
return devPath;
}
return path.join(__dirname, "..", 'features', this.name);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import * as fs from "fs"
import fs from "fs"
export function ensureDir(dirname: string) {
if (!fs.existsSync(dirname)) {
+1 -1
View File
@@ -1,4 +1,4 @@
import * as fs from "fs";
import fs from "fs";
import * as yaml from "js-yaml";
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"
import logger from "./logger";
+10 -11
View File
@@ -1,5 +1,7 @@
import * as path from "path"
import packageInfo from "../../package.json"
import path from "path"
import { LensBinary, LensBinaryOpts } from "./lens-binary"
import { isProduction } from "../common/vars";
export class HelmCli extends LensBinary {
@@ -13,7 +15,7 @@ export class HelmCli extends LensBinary {
super(opts)
}
protected getTarName(): string|null {
protected getTarName(): string | null {
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
}
@@ -26,19 +28,16 @@ export class HelmCli extends LensBinary {
}
protected getOriginalBinaryPath() {
return path.join(this.dirname, this.platformName+"-"+this.arch, this.originalBinaryName)
return path.join(this.dirname, this.platformName + "-" + this.arch, this.originalBinaryName)
}
}
const helmVersion = require("../../package.json").config.bundledHelmVersion
const isDevelopment = process.env.NODE_ENV !== "production"
let baseDir: string = null
const helmVersion = packageInfo.config.bundledHelmVersion;
let baseDir = process.resourcesPath;
if(isDevelopment) {
baseDir = path.join(process.cwd(), "binaries", "client")
} else {
baseDir = path.join(process.resourcesPath)
if (!isProduction) {
baseDir = path.join(process.cwd(), "binaries", "client");
}
export const helmCli = new HelmCli(baseDir, helmVersion)
export const helmCli = new HelmCli(baseDir, helmVersion);
+4 -5
View File
@@ -1,25 +1,24 @@
import * as tempy from "tempy";
import * as fs from "fs";
import fs from "fs";
import * as yaml from "js-yaml";
import * as camelcaseKeys from "camelcase-keys";
import { promiseExec} from "./promise-exec"
import { helmCli } from "./helm-cli";
import { Cluster } from "./cluster";
import { toCamelCase } from "../common/utils/camelCase";
export class HelmReleaseManager {
public async listReleases(pathToKubeconfig: string, namespace?: string) {
const helm = await helmCli.binaryPath()
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"
const { stdout, stderr } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
const output = JSON.parse(stdout)
if (output.length == 0) {
return output
}
const result: any = []
output.forEach((release: any, index: number) => {
output[index] = camelcaseKeys(release)
output[index] = toCamelCase(release)
});
return output
}
+1 -1
View File
@@ -1,4 +1,4 @@
import * as fs from "fs";
import fs from "fs";
import logger from "./logger";
import * as yaml from "js-yaml";
import { promiseExec } from "./promise-exec";
+18 -11
View File
@@ -1,7 +1,10 @@
// Main process
import "../common/system-ca"
import "../common/prometheus-providers"
import { app, dialog, protocol } from "electron"
import { PromiseIpc } from "electron-promise-ipc"
import * as path from "path"
import path from "path"
import { format as formatUrl } from "url"
import logger from "./logger"
import initMenu from "./menu"
@@ -15,20 +18,21 @@ import { shellSync } from "./shell-sync"
import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env"
import { findMainWebContents } from "./webcontents"
import "../common/prometheus-providers"
import { registerStaticProtocol } from "../common/register-static";
import { isMac, vueAppName } from "../common/vars";
mangleProxyEnv()
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
}
const isDevelopment = process.env.NODE_ENV !== "production"
const promiseIpc = new PromiseIpc({ timeout: 2000 })
const promiseIpc = new PromiseIpc({ timeout: 2000 })
let windowManager: WindowManager = null;
let clusterManager: ClusterManager = null;
let proxyServer: proxy.LensProxy = null;
const vmURL = (isDevelopment) ? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}` : formatUrl({
pathname: path.join(__dirname, "index.html"),
const vmURL = formatUrl({
pathname: path.join(__dirname, `${vueAppName}.html`),
protocol: "file",
slashes: true,
})
@@ -40,12 +44,15 @@ async function main() {
updater.start();
tracker.event("app", "start");
registerStaticProtocol();
protocol.registerFileProtocol('store', (request, callback) => {
const url = request.url.substr(8)
callback( path.normalize(`${app.getPath("userData")}/${url}`) )
callback(path.normalize(`${app.getPath("userData")}/${url}`))
}, (error) => {
if (error) console.error('Failed to register protocol')
})
let port: number = null
// find free port
try {
@@ -80,7 +87,7 @@ async function main() {
},
showPreferencesHook: async () => {
// IPC send needs webContents as we're sending it to renderer
promiseIpc.send('navigate', findMainWebContents(), {name: 'preferences-page'}).then((data: any) => {
promiseIpc.send('navigate', findMainWebContents(), { name: 'preferences-page' }).then((data: any) => {
logger.debug("navigate: preferences IPC sent");
})
},
@@ -103,10 +110,10 @@ async function main() {
}
app.on("ready", main)
app.on('window-all-closed', function() {
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform != 'darwin') {
if (!isMac) {
app.quit();
} else {
windowManager = null
@@ -116,7 +123,7 @@ app.on('window-all-closed', function() {
app.on("activate", () => {
if (!windowManager) {
logger.debug("activate main window")
windowManager = new WindowManager(false)
windowManager = new WindowManager({ showSplash: false })
windowManager.showMain(vmURL)
}
})
+1 -3
View File
@@ -1,8 +1,6 @@
import * as k8s from "@kubernetes/client-node"
import * as os from "os"
import { all } from "q";
import * as yaml from "js-yaml"
import { V1beta1ValidatingWebhookConfiguration } from "@kubernetes/client-node";
import logger from "./logger";
const kc = new k8s.KubeConfig()
@@ -31,7 +29,7 @@ export function loadConfig(kubeconfig: string): k8s.KubeConfig {
*
* @param config KubeConfig to check
*/
export function valideConfig(config: k8s.KubeConfig): boolean {
export function validateConfig(config: k8s.KubeConfig): boolean {
logger.debug(`validating kube config: ${JSON.stringify(config)}`)
if(!config.users || config.users.length == 0) {
throw new Error("No users provided in config")
+6 -7
View File
@@ -28,9 +28,7 @@ export class KubeAuthProxy {
public async run(): Promise<void> {
if (this.proxyProcess) {
return new Promise((resolve, reject) => {
resolve()
})
return;
}
const proxyBin = await this.kubectl.kubectlPath()
const configWatcher = watch(this.cluster.kubeconfigPath(), (eventType: string, filename: string) => {
@@ -43,11 +41,10 @@ export class KubeAuthProxy {
}
}
})
configWatcher.on("error", () => {})
const clusterUrl = url.parse(this.cluster.apiUrl)
let args = [
"proxy",
"-p", this.port.toString(),
"--port", this.port.toString(),
"--kubeconfig", this.cluster.kubeconfigPath(),
"--accept-hosts", clusterUrl.hostname,
]
@@ -59,7 +56,9 @@ export class KubeAuthProxy {
})
this.proxyProcess.on("exit", (code) => {
logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`)
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((_) => {})
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((err: Error) => {
logger.debug("failed to send IPC log message: " + err.message)
})
this.proxyProcess = null
configWatcher.close()
})
@@ -96,7 +95,7 @@ export class KubeAuthProxy {
}
protected async sendIpcLogMessage(data: string, stream: string) {
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data: data, stream: stream })
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data, stream })
}
public exit() {
+1 -1
View File
@@ -1,5 +1,5 @@
import { app } from "electron"
import * as fs from "fs"
import fs from "fs"
import { ensureDir, randomFileName} from "./file-helpers"
import logger from "./logger"
+12 -14
View File
@@ -1,17 +1,17 @@
import { app, remote } from "electron"
import * as path from "path"
import * as fs from "fs"
import * as request from "request"
import path from "path"
import fs from "fs"
import request from "request"
import { promiseExec} from "./promise-exec"
import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra"
import * as md5File from "md5-file"
import { globalRequestOpts } from "../common/request"
import * as lockFile from "proper-lockfile"
import { helmCli } from "./helm-cli"
import { userStore } from "../common/user-store"
import { getBundledKubectlVersion} from "../common/utils/app-version"
const bundledVersion = require("../../package.json").config.bundledKubectlVersion
const bundledVersion = getBundledKubectlVersion()
const kubectlMap: Map<string, string> = new Map([
["1.7", "1.8.15"],
["1.8", "1.9.10"],
@@ -195,16 +195,16 @@ export class Kubectl {
const file = fs.createWriteStream(this.path)
stream.on("complete", () => {
logger.debug("kubectl binary download finished")
file.end(() => {})
file.end()
})
stream.on("error", (error) => {
logger.error(error)
fs.unlink(this.path, () => {})
fs.unlink(this.path, null)
reject(error)
})
file.on("close", () => {
logger.debug("kubectl binary download closed")
fs.chmod(this.path, 0o755, () => {})
fs.chmod(this.path, 0o755, null)
resolve()
})
stream.pipe(file)
@@ -285,13 +285,11 @@ export class Kubectl {
}
protected getDownloadMirror() {
if (process.platform == "darwin") {
return packageMirrors.get("default") // MacOS packages are only available from default
}
const mirror = packageMirrors.get(userStore.getPreferences().downloadMirror)
if (mirror) { return mirror }
return packageMirrors.get("default")
if (mirror) {
return mirror
}
return packageMirrors.get("default") // MacOS packages are only available from default
}
}
+29
View File
@@ -0,0 +1,29 @@
import packageInfo from "../../package.json"
import { bundledKubectl, Kubectl } from "../../src/main/kubectl";
import { UserStore } from "../common/user-store";
jest.mock("../common/user-store", () => {
const userStoreMock: Partial<UserStore> = {
getPreferences() {
return {
downloadMirror: "default"
}
}
}
return {
userStore: userStoreMock,
}
})
describe("kubectlVersion", () => {
it("returns bundled version if exactly same version used", async () => {
const kubectl = new Kubectl(bundledKubectl.kubectlVersion)
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
})
it("returns bundled version if same major.minor version is used", async () => {
const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion);
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
})
})
+1 -1
View File
@@ -1,4 +1,4 @@
import * as http from "http";
import http from "http";
export abstract class LensApi {
protected respondJson(res: http.ServerResponse, content: {}, status = 200) {
+27 -20
View File
@@ -1,9 +1,10 @@
import * as path from "path"
import * as fs from "fs"
import * as request from "request"
import path from "path"
import fs from "fs"
import request from "request"
import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra"
import * as tar from "tar"
import { isWindows } from "../common/vars";
export type LensBinaryOpts = {
version: string;
@@ -12,6 +13,7 @@ export type LensBinaryOpts = {
newBinaryName?: string;
requestOpts?: request.Options;
}
export class LensBinary {
public binaryVersion: string
@@ -35,19 +37,21 @@ export class LensBinary {
let arch = null
if(process.arch == "x64") {
if (process.arch == "x64") {
arch = "amd64"
} else if(process.arch == "x86" || process.arch == "ia32") {
}
else if (process.arch == "x86" || process.arch == "ia32") {
arch = "386"
} else {
}
else {
arch = process.arch
}
this.arch = arch
this.platformName = process.platform === "win32" ? "windows" : process.platform
this.platformName = isWindows ? "windows" : process.platform
this.dirname = path.normalize(path.join(baseDir, this.binaryName))
if (process.platform === "win32") {
this.binaryName = this.binaryName+".exe"
this.originalBinaryName = this.originalBinaryName+".exe"
if (isWindows) {
this.binaryName = this.binaryName + ".exe"
this.originalBinaryName = this.originalBinaryName + ".exe"
}
const tarName = this.getTarName()
if (tarName) {
@@ -64,7 +68,7 @@ export class LensBinary {
return this.getBinaryPath()
}
protected getTarName(): string|null {
protected getTarName(): string | null {
return null
}
@@ -88,7 +92,7 @@ export class LensBinary {
try {
await this.ensureBinary()
return this.dirname
} catch(err) {
} catch (err) {
logger.error(err)
return ""
}
@@ -101,10 +105,12 @@ export class LensBinary {
public async ensureBinary() {
const isValid = await this.checkBinary()
if(!isValid) {
await this.downloadBinary().catch((error) => { logger.error(error) });
if (!isValid) {
await this.downloadBinary().catch((error) => {
logger.error(error)
});
if (this.tarPath) await this.untarBinary()
if(this.originalBinaryName != this.binaryName ) await this.renameBinary()
if (this.originalBinaryName != this.binaryName) await this.renameBinary()
logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`)
}
}
@@ -127,7 +133,8 @@ export class LensBinary {
fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => {
if (err) {
reject(err)
} else {
}
else {
resolve()
}
})
@@ -135,7 +142,7 @@ export class LensBinary {
}
protected async downloadBinary() {
const binaryPath = this.tarPath || this.getBinaryPath()
const binaryPath = this.tarPath || this.getBinaryPath()
await ensureDir(this.getBinaryDir(), 0o755)
const file = fs.createWriteStream(binaryPath)
@@ -152,18 +159,18 @@ export class LensBinary {
stream.on("complete", () => {
logger.info(`Download of ${this.originalBinaryName} finished`)
file.end(() => {})
file.end()
})
stream.on("error", (error) => {
logger.error(error)
fs.unlink(binaryPath, () => {})
fs.unlink(binaryPath, null)
throw(error)
})
return new Promise((resolve, reject) => {
file.on("close", () => {
logger.debug(`${this.originalBinaryName} binary download closed`)
if(!this.tarPath) fs.chmod(binaryPath, 0o755, () => {})
if (!this.tarPath) fs.chmod(binaryPath, 0o755, null)
resolve()
})
stream.pipe(file)
+3 -2
View File
@@ -1,10 +1,11 @@
import * as winston from "winston"
import winston from "winston"
import { isDebugging } from "../common/vars";
const options = {
colorize: true,
handleExceptions: false,
json: false,
level: process.env.DEBUG === "true" ? "debug" : "info",
level: isDebugging ? "debug" : "info",
}
const logger = winston.createLogger({
+18 -17
View File
@@ -1,4 +1,7 @@
import {app, dialog, Menu, MenuItemConstructorOptions, shell, webContents, BrowserWindow, MenuItem} from "electron"
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron"
import { isDevelopment, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars";
// todo: refactor + split menu sections to separated files, e.g. menus/file.menu.ts
export interface MenuOptions {
logoutHook: any;
@@ -10,7 +13,6 @@ export interface MenuOptions {
}
function setClusterSettingsEnabled(enabled: boolean) {
const isMac = process.platform === 'darwin';
const menuIndex = isMac ? 1 : 0
Menu.getApplicationMenu().items[menuIndex].submenu.items[1].enabled = enabled
}
@@ -21,7 +23,7 @@ function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) {
]
appDetails.push(`Copyright 2020 Lakend Labs, Inc.`)
let title = "Lens"
if (process.platform === "win32") {
if (isWindows) {
title = ` ${title}`
}
dialog.showMessageBoxSync(browserWindow, {
@@ -40,9 +42,6 @@ function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) {
* @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) {
const isMac = process.platform === 'darwin';
const isDevelopment = process.env.NODE_ENV === 'development';
const mt: MenuItemConstructorOptions[] = [];
const macAppMenu: MenuItemConstructorOptions = {
label: app.getName(),
@@ -67,12 +66,12 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
{ role: 'quit' }
]
};
if(isMac) {
if (isMac) {
mt.push(macAppMenu);
}
let fileMenu: MenuItemConstructorOptions;
if(isMac) {
if (isMac) {
fileMenu = {
label: 'File',
submenu: [{
@@ -86,7 +85,8 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
}
]
}
} else {
}
else {
fileMenu = {
label: 'File',
submenu: [
@@ -134,27 +134,28 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
{
label: 'Back',
accelerator: 'CmdOrCtrl+[',
click () {
click() {
webContents.getFocusedWebContents().executeJavaScript('window.history.back()')
}
},
{
label: 'Forward',
accelerator: 'CmdOrCtrl+]',
click () {
click() {
webContents.getFocusedWebContents().executeJavaScript('window.history.forward()')
}
},
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click () {
click() {
webContents.getFocusedWebContents().reload()
}
},
...(isDevelopment ? [
{ role: 'toggleDevTools'} as MenuItemConstructorOptions,
{ role: 'toggleDevTools' } as MenuItemConstructorOptions,
{
accelerator: "CmdOrCtrl+Shift+I",
label: 'Open Dashboard Devtools',
click() {
webContents.getFocusedWebContents().openDevTools()
@@ -183,20 +184,20 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
{
label: 'Community Slack',
click: async () => {
shell.openExternal('https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI');
shell.openExternal(slackUrl);
},
},
{
label: 'Report an Issue',
click: async () => {
shell.openExternal('https://github.com/lensapp/lens/issues');
shell.openExternal(issuesTrackerUrl);
},
},
{
label: "What's new?",
click: opts.showWhatsNewHook,
},
...(process.platform !== "darwin" ? [{
...(!isMac ? [{
label: "About Lens",
click: showAbout
} as MenuItemConstructorOptions] : [])
@@ -214,4 +215,4 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
promiseIpc.on("disableClusterSettingsMenuItem", () => {
setClusterSettingsEnabled(false)
});
};
}
+1 -2
View File
@@ -1,6 +1,5 @@
import logger from "./logger"
import { createServer } from "net"
import { AddressInfo } from "net"
import { createServer, AddressInfo } from "net"
const getNextAvailablePort = () => {
logger.debug("getNextAvailablePort() start")
+26
View File
@@ -0,0 +1,26 @@
import { EventEmitter } from 'events'
import { getFreePort } from "./port"
jest.mock("net", () => {
return {
createServer() {
return new class MockServer extends EventEmitter {
listen = jest.fn(() => {
this.emit('listening')
return this
})
address = () => {
return { port: 12345 }
}
unref = jest.fn()
close = jest.fn(cb => cb())
}
},
}
});
describe("getFreePort", () => {
it("finds the next free port", async () => {
return expect(getFreePort()).resolves.toEqual(expect.any(Number))
})
})
+14 -27
View File
@@ -1,5 +1,5 @@
import * as http from "http";
import * as httpProxy from "http-proxy";
import http from "http";
import httpProxy from "http-proxy";
import { Socket } from "net";
import * as url from "url";
import * as WebSocket from "ws"
@@ -8,6 +8,7 @@ import logger from "./logger"
import * as shell from "./node-shell-session"
import { ClusterManager } from "./cluster-manager"
import { Router } from "./router"
import { apiPrefix } from "../common/vars";
export class LensProxy {
public static readonly localShellSessions = true
@@ -40,17 +41,15 @@ export class LensProxy {
protected buildProxyServer() {
const proxy = this.createProxy();
const proxyServer = http.createServer(function(req: http.IncomingMessage, res: http.ServerResponse) {
const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
this.handleRequest(proxy, req, res);
}.bind(this));
proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) {
});
proxyServer.on("upgrade", (req: http.IncomingMessage, socket: Socket, head: Buffer) => {
this.handleWsUpgrade(req, socket, head)
}.bind(this));
});
proxyServer.on("error", (err) => {
logger.error(err)
});
return proxyServer;
}
@@ -64,11 +63,10 @@ export class LensProxy {
res.writeHead(proxyRes.statusCode, {
"Content-Type": "text/plain"
})
res.end(cluster.contextHandler.proxyServerError().toString())
res.end(cluster.contextHandler.proxyServerError())
return
}
}
if (req.method !== "GET") {
return
}
@@ -106,11 +104,11 @@ export class LensProxy {
}
protected createWsListener() {
const ws = new WebSocket.Server({ noServer: true})
const ws = new WebSocket.Server({ noServer: true })
ws.on("connection", ((con: WebSocket, req: http.IncomingMessage) => {
const cluster = this.clusterManager.getClusterForRequest(req)
const contextHandler = cluster.contextHandler
const nodeParam = this.getNodeParam(req.url)
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
contextHandler.withTemporaryKubeconfig((kubeconfigPath) => {
return new Promise<boolean>(async (resolve, reject) => {
@@ -120,26 +118,15 @@ export class LensProxy {
})
})
})
}).bind(this))
}))
return ws
}
protected getNodeParam(requestUrl: string) {
const reqUrl = url.parse(requestUrl, true)
const urlParams = reqUrl.query
let nodeParam: string = null
for (const [key, value] of Object.entries(urlParams)) {
if (key === "node") {
nodeParam = value.toString()
}
}
return nodeParam
}
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
if (req.url.startsWith("/api-kube/")) {
const prefix = apiPrefix.KUBE_BASE;
if (req.url.startsWith(prefix)) {
delete req.headers.authorization
req.url = req.url.replace("/api-kube", "")
req.url = req.url.replace(prefix, "")
const isWatchRequest = req.url.includes("watch=")
return await contextHandler.getApiTarget(isWatchRequest)
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { exec } from "child_process";
import * as fs from "fs";
import fs from "fs";
import * as yaml from "js-yaml";
import * as path from "path";
import path from "path";
import * as tempy from "tempy";
import logger from "./logger"
import { Cluster } from "./cluster";
+9 -15
View File
@@ -1,5 +1,8 @@
import * as http from "http"
import * as path from "path"
import Call from "@hapi/call"
import Subtext from "@hapi/subtext"
import http from "http"
import path from "path"
import { readFile } from "fs"
import { Cluster } from "./cluster"
import { configRoute } from "./routes/config"
import { helmApi } from "./helm-api"
@@ -8,16 +11,7 @@ import { kubeconfigRoute } from "./routes/kubeconfig"
import { metricsRoute } from "./routes/metrics"
import { watchRoute } from "./routes/watch"
import { portForwardRoute } from "./routes/port-forward"
import { readFile } from "fs"
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Call = require('@hapi/call');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Subtext = require('@hapi/subtext');
declare const __static: string;
const assetsPath = path.join(__static, "build/client")
import { outDir, reactAppName } from "../common/vars";
const mimeTypes: {[key: string]: string} = {
"html": "text/html",
@@ -88,12 +82,12 @@ export class Router {
return request
}
protected handleStaticFile(file: string, response: http.ServerResponse) {
const asset = path.join(assetsPath, file)
protected handleStaticFile(filePath: string, response: http.ServerResponse) {
const asset = path.resolve(outDir, filePath);
readFile(asset, (err, data) => {
if (err) {
// default to index.html so that react routes work when page is refreshed
this.handleStaticFile("index.html", response)
this.handleStaticFile(`${reactAppName}.html`, response)
} else {
const type = mimeTypes[path.extname(asset).slice(1)] || "text/plain";
response.setHeader("Content-Type", type);
+23 -9
View File
@@ -1,10 +1,24 @@
import { app } from "electron"
import { CoreV1Api } from "@kubernetes/client-node"
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { userStore } from "../../common/user-store"
import { getAppVersion } from "../../common/app-utils"
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
import { Cluster } from "../cluster"
export interface IConfigRoutePayload {
kubeVersion?: string;
clusterName?: string;
lensVersion?: string;
lensTheme?: string;
username?: string;
token?: string;
allowedNamespaces?: string[];
allowedResources?: string[];
isClusterAdmin?: boolean;
chartsEnabled: boolean;
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
}
// TODO: auto-populate all resources dynamically
const apiResources = [
{ resource: "configmaps" },
@@ -44,12 +58,13 @@ async function getAllowedNamespaces(cluster: Cluster) {
return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i])
.map(ns => ns.metadata.name)
} catch(error) {
} catch (error) {
const kc = cluster.contextHandler.kc
const ctx = kc.getContextObject(kc.currentContext)
if (ctx.namespace) {
return [ctx.namespace]
} else {
}
else {
return []
}
}
@@ -67,20 +82,19 @@ async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
)
return apiResources
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
} catch(error) {
} catch (error) {
return []
}
}
class ConfigRoute extends LensApi {
public async routeConfig(request: LensApiRequest) {
const { params, response, cluster} = request
const { params, response, cluster } = request
const namespaces = await getAllowedNamespaces(cluster)
const data = {
const data: IConfigRoutePayload = {
clusterName: cluster.contextName,
lensVersion: getAppVersion(),
lensVersion: app.getVersion(),
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
kubeVersion: cluster.version,
chartsEnabled: true,
+6 -5
View File
@@ -1,9 +1,10 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import * as 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 { apiPrefix } from "../../common/vars";
type MetricsQuery = string | string[] | {
export type IMetricsQuery = string | string[] | {
[metricName: string]: string;
}
@@ -11,13 +12,13 @@ class MetricsRoute extends LensApi {
public async routeMetrics(request: LensApiRequest) {
const { response, cluster} = request
const query: MetricsQuery = request.payload;
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
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}`,
"Content-type": "application/json",
}
const queryParams: MetricsQuery = {}
const queryParams: IMetricsQuery = {}
request.query.forEach((value: string, key: string) => {
queryParams[key] = value
})
+2 -2
View File
@@ -1,6 +1,6 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Watch, KubeConfig, RuntimeRawExtension } from "@kubernetes/client-node"
import { Watch, KubeConfig } from "@kubernetes/client-node"
import { ServerResponse } from "http"
import { Request } from "request"
import logger from "../logger"
@@ -41,7 +41,7 @@ class ApiWatcher {
this.watchRequest.abort()
}
private watchHandler(phase: string, obj: RuntimeRawExtension) {
private watchHandler(phase: string, obj: any) {
this.eventBuffer.push({
type: phase,
object: obj
+12 -15
View File
@@ -1,13 +1,14 @@
import * as pty from "node-pty"
import * as WebSocket from "ws"
import { EventEmitter } from "events";
import * as path from "path"
import shellEnv = require("shell-env")
import path from "path"
import shellEnv from "shell-env"
import { app } from "electron"
import { Kubectl } from "./kubectl"
import { tracker } from "./tracker"
import { Cluster, ClusterPreferences } from "./cluster"
import { helmCli } from "./helm-cli"
import { isWindows } from "../common/vars";
export class ShellSession extends EventEmitter {
static shellEnvs: Map<string, any> = new Map()
@@ -66,13 +67,10 @@ export class ShellSession extends EventEmitter {
switch(path.basename(shell)) {
case "powershell.exe":
return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.kubectlBinDir};${this.helmBinDir};$Env:PATH"}`]
break
case "bash":
return ["--init-file", path.join(this.kubectlBinDir, '.bash_set_path')]
break
case "fish":
return ["--login", "--init-command", `export PATH="${this.kubectlBinDir}:${this.helmBinDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`]
break
case "zsh":
return ["--login"]
default:
@@ -99,7 +97,7 @@ export class ShellSession extends EventEmitter {
const env = JSON.parse(JSON.stringify(await shellEnv()))
const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter)
if(process.platform === "win32") {
if(isWindows) {
env["SystemRoot"] = process.env.SystemRoot
env["PTYSHELL"] = "powershell.exe"
env["PATH"] = pathStr
@@ -131,14 +129,14 @@ export class ShellSession extends EventEmitter {
protected pipeStdout() {
// send shell output to websocket
this.shellProcess.on("data", ((data: string) => {
this.shellProcess.onData(((data: string) => {
this.sendResponse(data)
}).bind(this));
}));
}
protected pipeStdin() {
// write websocket messages to shellProcess
this.websocket.on("message", function(data: string) {
this.websocket.on("message", (data: string) => {
if (!this.running) { return }
const message = Buffer.from(data.slice(1, data.length), "base64").toString()
@@ -151,11 +149,10 @@ export class ShellSession extends EventEmitter {
this.shellProcess.resize(resizeMsgObj["Width"], resizeMsgObj["Height"])
break;
case "9":
this.token = message
this.emit('newToken', this.token)
this.emit('newToken', message)
break;
}
}.bind(this))
})
}
protected exit(code = 1000) {
@@ -164,10 +161,10 @@ export class ShellSession extends EventEmitter {
}
protected closeWebsocketOnProcessExit() {
this.shellProcess.on("exit", (code) => {
this.shellProcess.onExit(({ exitCode }) => {
this.running = false
let timeout = 0
if (code > 0) {
if (exitCode > 0) {
this.sendResponse("Terminal will auto-close in 15 seconds ...")
timeout = 15*1000
}
@@ -186,7 +183,7 @@ export class ShellSession extends EventEmitter {
protected killShellProcess(){
if(this.running) {
// On Windows we need to kill the shell process by pid, since Lens won't respond after a while if using `this.shellProcess.kill()`
if (process.platform == "win32") {
if (isWindows) {
try {
process.kill(this.shellProcess.pid)
} catch(e) {
+2 -3
View File
@@ -1,6 +1,5 @@
import shellEnv = require("shell-env")
import logger from "./logger"
import * as os from "os";
import shellEnv from "shell-env"
import os from "os";
interface Env {
[key: string]: string;
+7 -18
View File
@@ -1,10 +1,8 @@
import { BrowserWindow, shell, Menu } from "electron"
import { BrowserWindow, shell } from "electron"
import { PromiseIpc } from "electron-promise-ipc"
import * as windowStateKeeper from "electron-window-state"
import * as path from "path"
import windowStateKeeper from "electron-window-state"
import { tracker } from "./tracker";
declare const __static: string;
import { getStaticUrl } from "../common/register-static";
export class WindowManager {
public mainWindow: BrowserWindow = null;
@@ -12,7 +10,7 @@ export class WindowManager {
protected promiseIpc: any
protected windowState: windowStateKeeper.State;
constructor(showSplash = true) {
constructor({ showSplash = true } = {}) {
this.promiseIpc = new PromiseIpc({ timeout: 2000 })
// Manage main window size&position with persistence
this.windowState = windowStateKeeper({
@@ -33,11 +31,12 @@ export class WindowManager {
}
})
if (showSplash) {
this.splashWindow.loadFile(path.join(__static, "/splash.html"))
this.splashWindow.loadURL(getStaticUrl("splash.html"))
this.splashWindow.show()
}
this.mainWindow = new BrowserWindow({
show: false,
x: this.windowState.x,
y: this.windowState.y,
width: this.windowState.width,
@@ -48,7 +47,6 @@ export class WindowManager {
nodeIntegration: true,
webviewTag: true
},
show: false
});
// Hook window state manager into window lifecycle
@@ -68,22 +66,13 @@ export class WindowManager {
shell.openExternal(link);
})
// handle developer console
if (process.env.NODE_ENV !== "production") {
this.mainWindow.webContents.on("devtools-opened", () => {
if (this.mainWindow.getBrowserView()) {
this.mainWindow.getBrowserView().webContents.openDevTools({mode: "detach"})
}
})
}
this.mainWindow.on("focus", () => {
tracker.event("app", "focus")
})
}
public showMain(url: string) {
this.mainWindow.loadURL( url ).then(() => {
this.mainWindow.loadURL(url).then(() => {
this.splashWindow.hide()
this.splashWindow.loadURL("data:text/html;charset=utf-8,").then(() => {
this.splashWindow.close()