diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index e4febf532a..bef96f9b5d 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -1,10 +1,8 @@ import type { WorkspaceId } from "./workspace-store"; import path from "path"; -import filenamify from "filenamify"; import { app, ipcRenderer, remote } from "electron"; -import { copyFile, ensureDir, unlink } from "fs-extra"; +import { unlink } from "fs-extra"; import { action, computed, observable, toJS } from "mobx"; -import { appProto, noClustersHost } from "./vars"; import { BaseStore } from "./base-store"; import { Cluster, ClusterState } from "../main/cluster"; import migrations from "../migrations/cluster-store" @@ -177,12 +175,11 @@ export class ClusterStore extends BaseStore { export const clusterStore = ClusterStore.getInstance(); -export function isNoClustersView() { - return location.hostname === noClustersHost -} - -export function getHostedClusterId() { - return location.hostname.split(".")[0]; +export function getHostedClusterId(): ClusterId { + const clusterHost = location.hostname.match(/^(.*?)\.localhost/); + if (clusterHost) { + return clusterHost[1] + } } export function getHostedCluster(): Cluster { diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index 3709f0ac94..65737b6134 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -156,7 +156,7 @@ export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig | export async function getKubeConfigLocal(): Promise { try { - const configFile = path.join(process.env.HOME, '.kube', 'config'); + const configFile = path.join(os.homedir(), '.kube', 'config'); const file = await readFile(configFile, "utf8"); const obj = yaml.safeLoad(file); if (obj.contexts) { diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 8d343a62e9..67c779aadb 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -71,11 +71,12 @@ export class UserStore extends BaseStore { if (kubeConfig) { this.newContexts.clear(); const localContexts = loadConfig(kubeConfig).getContexts(); - console.log(localContexts) - localContexts - .filter(ctx => ctx.cluster) - .filter(ctx => !this.seenContexts.has(ctx.name)) - .forEach(ctx => this.newContexts.add(ctx.name)); + localContexts.forEach(({ cluster, name }) => { + if (!cluster) return; + if (!this.seenContexts.has(name)) { + this.newContexts.add(name) + } + }) } } diff --git a/src/common/vars.ts b/src/common/vars.ts index 871ef89416..d7e18e3467 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -10,8 +10,6 @@ export const isDevelopment = isDebugging || !isProduction; export const isTestEnv = !!process.env.JEST_WORKER_ID; export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}` -export const appProto = "lens" // app.getPath("userData") folder -export const staticProto = "static" // static folder (e.g. "static://RELEASE_NOTES.md") // System paths export const contextDir = process.cwd(); @@ -22,9 +20,6 @@ export const rendererDir = path.join(contextDir, "src/renderer"); export const htmlTemplate = path.resolve(rendererDir, "template.html"); export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss"); -// System pages -export const noClustersHost = "no-clusters.localhost" - // Apis export const apiPrefix = "/api" // local router apis export const apiKubePrefix = "/api-kube" // k8s cluster apis diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 3935dda02e..047d0dfedd 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -1,4 +1,4 @@ -import { action, computed, observable, toJS } from "mobx"; +import { action, computed, observable, reaction, toJS } from "mobx"; import { BaseStore } from "./base-store"; import { clusterStore } from "./cluster-store" @@ -22,6 +22,15 @@ export class WorkspaceStore extends BaseStore { super({ configName: "lens-workspace-store", }); + + // switch to first available cluster in current workspace + reaction(() => this.currentWorkspaceId, workspaceId => { + const clusters = clusterStore.getByWorkspaceId(workspaceId); + const activeClusterInWorkspace = clusters.some(cluster => cluster.id === clusterStore.activeClusterId); + if (!activeClusterInWorkspace) { + clusterStore.activeClusterId = clusters.length ? clusters[0].id : null; + } + }) } @observable currentWorkspaceId = WorkspaceStore.defaultId; diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 09e6404c88..97b9a80871 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,6 +1,5 @@ import type http from "http" import { autorun } from "mobx"; -import { apiKubePrefix } from "../common/vars"; import { ClusterId, clusterStore } from "../common/cluster-store" import { Cluster } from "./cluster" import { clusterIpc } from "../common/cluster-ipc"; @@ -50,23 +49,8 @@ export class ClusterManager { } getClusterForRequest(req: http.IncomingMessage): Cluster { - let cluster: Cluster = null - - // lens-server is connecting to 127.0.0.1:/ - if (req.headers.host.startsWith("127.0.0.1")) { - const clusterId = req.url.split("/")[1] - if (clusterId) { - cluster = this.getCluster(clusterId) - if (cluster) { - // we need to swap path prefix so that request is proxied to kube api - req.url = req.url.replace(`/${clusterId}`, apiKubePrefix) - } - } - } else { - const id = req.headers.host.split(".")[0] - cluster = this.getCluster(id) - } - - return cluster; + logger.info(`getClusterForRequest(): ${req.headers.host}${req.url}`) + const clusterId = req.headers.host.split(".")[0] + return this.getCluster(clusterId) } } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index a6692cc4c0..82037a1fb4 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,7 +1,8 @@ import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" -import type { FeatureStatusMap } from "./feature" +import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; -import { action, observable, reaction, toJS, when } from "mobx"; +import type { FeatureStatusMap } from "./feature" +import { action, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; import { broadcastIpc } from "../common/ipc"; import { ContextHandler } from "./context-handler" @@ -52,7 +53,6 @@ export class Cluster implements ClusterModel { @observable kubeConfigPath: string; @observable apiUrl: string; // cluster server url @observable kubeProxyUrl: string; // lens-proxy to kube-api url - @observable webContentUrl: string; // page content url for loading in renderer @observable online: boolean; @observable accessible: boolean; @observable disconnected: boolean; @@ -67,6 +67,11 @@ export class Cluster implements ClusterModel { @observable allowedNamespaces: string[] = []; @observable allowedResources: string[] = []; + @computed get host() { + const proxyHost = new URL(this.kubeProxyUrl).host; + return `${this.id}.${proxyHost}` + } + constructor(model: ClusterModel) { this.updateModel(model); } @@ -80,20 +85,15 @@ export class Cluster implements ClusterModel { @action async init(port: number) { - if (this.initialized) { - return; - } try { this.contextHandler = new ContextHandler(this); this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler); this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; - this.webContentUrl = `http://${this.id}.localhost:${port}`; this.initialized = true; - logger.info(`[CLUSTER]: init success`, { + logger.info(`[CLUSTER]: "${this.contextName}" init success`, { id: this.id, - serverUrl: this.apiUrl, - webContentUrl: this.webContentUrl, - kubeProxyUrl: this.kubeProxyUrl, + context: this.contextName, + apiUrl: this.apiUrl }); } catch (err) { logger.error(`[CLUSTER]: init failed: ${err}`, { @@ -155,7 +155,7 @@ export class Cluster implements ClusterModel { @action async refresh() { logger.info(`[CLUSTER]: refresh`, this.getMeta()); - await this.refreshConnectionStatus(); + await this.refreshConnectionStatus(); // refresh "version", "online", etc. if (this.accessible) { this.kubeCtl = new Kubectl(this.version) this.distribution = this.detectKubernetesDistribution(this.version) @@ -217,22 +217,28 @@ export class Cluster implements ClusterModel { return uninstallFeature(name, this) } - getPrometheusApiPrefix() { - return this.preferences.prometheus?.prefix || "" - } - - protected async k8sRequest(path: string, options: RequestPromiseOptions = {}) { + protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { const apiUrl = this.kubeProxyUrl + path; return request(apiUrl, { json: true, timeout: 5000, headers: { + Host: this.host, // provide cluster-id for ClusterManager.getClusterForRequest() ...(options.headers || {}), - Host: new URL(this.webContentUrl).host, }, }) } + getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) { + const prometheusPrefix = this.preferences.prometheus?.prefix || ""; + const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`; + return this.k8sRequest(metricsPath, { + resolveWithFullResponse: false, + json: true, + qs: queryParams, + }) + } + protected async getConnectionStatus(): Promise { try { const response = await this.k8sRequest("/version") diff --git a/src/main/index.ts b/src/main/index.ts index 73a5843840..0c5558ec33 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,9 +3,8 @@ import "../common/system-ca" import "../common/prometheus-providers" import { app, dialog } from "electron" -import { appName, appProto, staticDir, staticProto } from "../common/vars"; +import { appName, staticDir } from "../common/vars"; import path from "path" -import { initMenu } from "./menu" import { LensProxy } from "./lens-proxy" import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; @@ -41,8 +40,7 @@ async function main() { const updater = new AppUpdater() updater.start(); - registerFileProtocol(appProto, app.getPath("userData")); - registerFileProtocol(staticProto, staticDir); + registerFileProtocol("static", staticDir); // find free port let proxyPort: number @@ -74,7 +72,6 @@ async function main() { // create window manager and open app windowManager = new WindowManager(proxyPort); - initMenu(windowManager); } app.on("ready", main); diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 42646a929e..7cc4584750 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -7,10 +7,11 @@ import { openShell } from "./node-shell-session"; import { Router } from "./router" import { ClusterManager } from "./cluster-manager" import { ContextHandler } from "./context-handler"; -import { apiKubePrefix, noClustersHost } from "../common/vars"; +import { apiKubePrefix } from "../common/vars"; import logger from "./logger" export class LensProxy { + protected origin: string protected proxyServer: http.Server protected router: Router protected closed = false @@ -21,12 +22,13 @@ export class LensProxy { } private constructor(protected port: number, protected clusterManager: ClusterManager) { + this.origin = `http://localhost:${port}` this.router = new Router(); } listen(port = this.port): this { this.proxyServer = this.buildCustomProxy().listen(port); - logger.info(`LensProxy server has started http://localhost:${port}`); + logger.info(`LensProxy server has started at ${this.origin}`); return this; } @@ -117,26 +119,17 @@ export class LensProxy { } protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) { - if (req.headers.host.split(":")[0] === noClustersHost) { - this.router.handleStaticFile(req.url, res); - return; - } const cluster = this.clusterManager.getClusterForRequest(req) - if (!cluster) { - const reqId = this.getRequestId(req); - logger.error("Got request to unknown cluster", { reqId }) - res.statusCode = 503 - res.end() - return - } - const contextHandler = cluster.contextHandler - await contextHandler.ensureServer(); - const proxyTarget = await this.getProxyTarget(req, contextHandler) - if (proxyTarget) { - proxy.web(req, res, proxyTarget) - } else { - this.router.route(cluster, req, res); + if (cluster) { + await cluster.contextHandler.ensureServer(); + const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler) + if (proxyTarget) { + // allow to fetch apis in "clusterId.localhost:port" from "localhost:port" + res.setHeader("Access-Control-Allow-Origin", this.origin); + return proxy.web(req, res, proxyTarget); + } } + this.router.route(cluster, req, res); } protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) { diff --git a/src/main/menu.ts b/src/main/menu.ts index 09cd668976..67daf37fc2 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -1,7 +1,6 @@ import type { WindowManager } from "./window-manager"; import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron" import { autorun } from "mobx"; -import { broadcastIpc } from "../common/ipc"; import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars"; import { clusterStore } from "../common/cluster-store"; import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route"; @@ -17,19 +16,7 @@ export function initMenu(windowManager: WindowManager) { }); } -function buildMenu(windowManager: WindowManager) { - const hasClusters = clusterStore.hasClusters(); - const activeClusterId = clusterStore.activeClusterId; - - function navigate(url: string) { - const clusterView = windowManager.getClusterView(activeClusterId); - broadcastIpc({ - channel: "menu:navigate", - webContentId: clusterView ? clusterView.id : undefined /*no-clusters*/, - args: [url], - }); - } - +export function buildMenu(windowManager: WindowManager) { function macOnly(menuItems: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] { if (!isMac) return []; return menuItems; @@ -41,20 +28,20 @@ function buildMenu(windowManager: WindowManager) { { label: 'Add Cluster', click() { - navigate(addClusterURL()) + windowManager.navigateMain(addClusterURL()) } }, - ...(hasClusters ? [{ + ...(clusterStore.activeCluster ? [{ label: 'Cluster Settings', click() { - navigate(clusterSettingsURL()) + windowManager.navigateMain(clusterSettingsURL()) } }] : []), { type: 'separator' }, { label: 'Preferences', click() { - navigate(preferencesURL()) + windowManager.navigateMain(preferencesURL()) } }, ...macOnly([ @@ -125,7 +112,7 @@ function buildMenu(windowManager: WindowManager) { { label: "What's new?", click() { - navigate(whatsNewURL()) + windowManager.navigateMain(whatsNewURL()) }, }, { diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index ccbe187b6e..9cf4a280e2 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -1,7 +1,5 @@ -import url from "url" import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" -import requestPromise from "request-promise-native" import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry" export type IMetricsQuery = string | string[] | { @@ -9,25 +7,17 @@ export type IMetricsQuery = string | string[] | { } class MetricsRoute extends LensApi { - - public async routeMetrics(request: LensApiRequest) { + async routeMetrics(request: LensApiRequest) { const { response, cluster, payload } = request - const { contextHandler, kubeProxyUrl } = cluster; - const headers: Record = { - "Host": url.parse(cluster.webContentUrl).host, - "Content-type": "application/json", - } const queryParams: IMetricsQuery = {} request.query.forEach((value: string, key: string) => { queryParams[key] = value }) - - let metricsUrl: string + let prometheusPath: string let prometheusProvider: PrometheusProvider try { - const prometheusPath = await contextHandler.getPrometheusPath() - metricsUrl = `${kubeProxyUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range` - prometheusProvider = await contextHandler.getPrometheusProvider() + prometheusPath = await cluster.contextHandler.getPrometheusPath() + prometheusProvider = await cluster.contextHandler.getPrometheusProvider() } catch { this.respondJson(response, {}) return @@ -35,18 +25,10 @@ class MetricsRoute extends LensApi { // prometheus metrics loader const attempts: { [query: string]: number } = {}; const maxAttempts = 5; - const loadMetrics = (orgQuery: string): Promise => { - const query = orgQuery.trim() + const loadMetrics = (promQuery: string): Promise => { + const query = promQuery.trim() const attempt = attempts[query] = (attempts[query] || 0) + 1; - return requestPromise(metricsUrl, { - resolveWithFullResponse: false, - headers: headers, - json: true, - qs: { - query: query, - ...queryParams - } - }).catch(async (error) => { + return cluster.getMetrics(prometheusPath, { query, ...queryParams }).catch(async error => { if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) { await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request return loadMetrics(query); diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 241228eb65..c4ae9a0d96 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,66 +1,56 @@ -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 { noClustersHost } from "../common/vars"; -import logger from "./logger"; +import { initMenu } from "./menu"; export class WindowManager { - protected activeView: BrowserWindow; + protected mainView: BrowserWindow; protected splashWindow: BrowserWindow; - protected noClustersWindow: BrowserWindow; - protected views = new Map(); - protected disposers: CallableFunction[] = []; protected windowState: windowStateKeeper.State; - constructor(protected proxyPort: number, showSplash = true) { + constructor(protected proxyPort: number) { + initMenu(this); + // Manage main window size and position with state persistence this.windowState = windowStateKeeper({ defaultHeight: 900, defaultWidth: 1440, }); - // Show while app not ready - if (showSplash) { - this.showSplash(); - } + const { width, height, x, y } = this.windowState; + this.mainView = new BrowserWindow({ + x, y, width, height, + show: false, + minWidth: 900, + minHeight: 760, + titleBarStyle: "hidden", + backgroundColor: "#1e2124", + webPreferences: { + nodeIntegration: true, + enableRemoteModule: true, + }, + }); + this.windowState.manage(this.mainView); - // Manage reactive state - this.disposers.push( - // auto-show/hide "no-clusters" window when necessary - reaction(() => clusterStore.hasClusters(), hasClusters => { - this.handleNoClustersView({ activate: !hasClusters }); - }, { - fireImmediately: true - }), + // open external links in default browser (target=_blank, window.open) + this.mainView.webContents.on("new-window", (event, url) => { + event.preventDefault(); + shell.openExternal(url); + }); - // auto-show active cluster window - reaction(() => clusterStore.activeClusterId, this.activateView, { - fireImmediately: true, - }), - - // auto-destroy views for removed clusters - reaction(() => clusterStore.removedClusters.toJS(), removedClusters => { - removedClusters.forEach(cluster => { - this.destroyClusterView(cluster.id); - }); - }, { - delay: 25, // fix: destroy later and allow to use view's state in next activateView() - }), - ); + // load & show app + this.showMain(); } - protected handleNoClustersView = async ({ activate = false } = {}) => { - if (!this.noClustersWindow) { - this.noClustersWindow = this.initClusterView(null); - await this.noClustersWindow.loadURL(`http://${noClustersHost}:${this.proxyPort}`); - } - if (activate) { - this.activeView = this.noClustersWindow; - this.noClustersWindow.show(); - this.hideSplash(); - } + // fixme + navigateMain(url: string) { + this.mainView.webContents.executeJavaScript("console.log('implement me!')") + } + + async showMain() { + await this.showSplash(); + await this.mainView.loadURL(`http://localhost:${this.proxyPort}`) + this.mainView.show(); + this.splashWindow.hide(); } async showSplash() { @@ -79,95 +69,9 @@ export class WindowManager { this.splashWindow.show(); } - hideSplash() { - this.splashWindow.hide(); - } - - getClusterView(clusterId: ClusterId): BrowserWindow { - return this.views.get(clusterId); - } - - activateView = async (clusterId: ClusterId): Promise => { - const cluster = clusterStore.getById(clusterId); - if (!cluster) return; - try { - const prevActiveView = this.activeView; - const isLoadedBefore = !!this.getClusterView(clusterId); - const view = this.initClusterView(clusterId); - logger.info(`[WINDOW-MANAGER]: activating cluster view`, { - id: view.id, - clusterId: cluster.id, - contextName: cluster.contextName, - isLoadedBefore: isLoadedBefore, - }); - if (prevActiveView !== view) { - this.activeView = view; - if (!isLoadedBefore) { - await cluster.whenInitialized; // wait for url - await view.loadURL(cluster.webContentUrl); - this.hideSplash(); - } - // refresh position and hide previous active window - if (prevActiveView) { - view.setBounds(prevActiveView.getBounds()); - prevActiveView.hide(); - } - view.show(); - return view.id; - } - } catch (err) { - logger.error(`[WINDOW-MANAGER]: can't activate cluster view`, { - clusterId: cluster.id, - err: String(err), - }); - } - } - - protected initClusterView(clusterId: ClusterId): BrowserWindow { - let view = this.getClusterView(clusterId); - if (!view) { - const { width, height, x, y } = this.windowState; - view = new BrowserWindow({ - show: false, - x: x, y: y, - width: width, - height: height, - minWidth: 900, - minHeight: 760, - titleBarStyle: "hidden", - backgroundColor: "#1e2124", - webPreferences: { - nodeIntegration: true, - enableRemoteModule: 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.windowState.manage(view); - } - return view; - } - - protected destroyClusterView(clusterId: ClusterId) { - const view = this.views.get(clusterId); - if (view) { - view.destroy(); - this.views.delete(clusterId); - } - } - destroy() { this.windowState.unmanage(); - this.disposers.forEach(dispose => dispose()); - this.disposers.length = 0; - this.views.forEach(view => view.destroy()); - this.views.clear(); this.splashWindow.destroy(); - this.splashWindow = null; - this.activeView = null; + this.mainView.destroy(); } } diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts index c2d047b0c8..7dd2306e74 100644 --- a/src/renderer/api/index.ts +++ b/src/renderer/api/index.ts @@ -4,16 +4,16 @@ import { Notifications } from "../components/notifications"; import { apiKubePrefix, apiPrefix, isDevelopment } from "../../common/vars"; export const apiBase = new JsonApi({ + apiBase: apiPrefix, debug: isDevelopment, - apiPrefix: apiPrefix, }); export const apiKube = new KubeJsonApi({ + apiBase: apiKubePrefix, debug: isDevelopment, - apiPrefix: apiKubePrefix, }); // Common handler for HTTP api errors -function onApiError(error: JsonApiErrorParsed, res: Response) { +export function onApiError(error: JsonApiErrorParsed, res: Response) { switch (res.status) { case 403: error.isUsedForNotification = true; diff --git a/src/renderer/api/json-api.ts b/src/renderer/api/json-api.ts index 5f50296b0d..2d274d5f6c 100644 --- a/src/renderer/api/json-api.ts +++ b/src/renderer/api/json-api.ts @@ -27,7 +27,7 @@ export interface JsonApiLog { } export interface JsonApiConfig { - apiPrefix: string; + apiBase: string; debug?: boolean; } @@ -72,7 +72,7 @@ export class JsonApi { } protected request(path: string, params?: P, init: RequestInit = {}) { - let reqUrl = this.config.apiPrefix + path; + let reqUrl = this.config.apiBase + path; const reqInit: RequestInit = { ...this.reqInit, ...init }; const { data, query } = params || {} as P; if (data && !reqInit.body) { diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index f4416aeabd..464b785dfb 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -61,7 +61,7 @@ export class KubeWatchApi { } protected getQuery(): Partial { - const { isAdmin, allowedNamespaces } = getHostedCluster(); + const { isAdmin, allowedNamespaces } = getHostedCluster() return { api: this.activeApis.map(api => { if (isAdmin) return api.getWatchUrl(); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx new file mode 100644 index 0000000000..0fcb216cd9 --- /dev/null +++ b/src/renderer/bootstrap.tsx @@ -0,0 +1,38 @@ +import "./components/app.scss" +import React from "react"; +import { render } from "react-dom"; +import { isMac } from "../common/vars"; +import { userStore } from "../common/user-store"; +import { workspaceStore } from "../common/workspace-store"; +import { clusterStore, getHostedClusterId } from "../common/cluster-store"; +import { i18nStore } from "./i18n"; +import { themeStore } from "./theme.store"; +import { App } from "./components/app"; +import { LensApp } from "./lens-app"; + +type AppComponent = React.ComponentType & { + init?(): void; +} + +export async function bootstrap(App: AppComponent) { + const rootElem = document.getElementById("app") + rootElem.classList.toggle("is-mac", isMac); + + // preload common stores + await Promise.all([ + userStore.load(), + workspaceStore.load(), + clusterStore.load(), + i18nStore.init(), + themeStore.init(), + ]); + + // init app's dependencies if any + if (App.init) { + await App.init(); + } + render(, rootElem); +} + +// run +bootstrap(getHostedClusterId() ? App : LensApp); diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts index b32843be8a..5120a63053 100644 --- a/src/renderer/components/+apps-releases/release.store.ts +++ b/src/renderer/components/+apps-releases/release.store.ts @@ -58,7 +58,7 @@ export class ReleaseStore extends ItemStore { this.isLoading = true; let items; try { - const { isAdmin, allowedNamespaces } = getHostedCluster(); + const { isAdmin, allowedNamespaces } = getHostedCluster() items = await this.loadItems(!isAdmin ? allowedNamespaces : null); } finally { if (items) { diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index 777562060b..60c5925513 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -1,7 +1,7 @@ import "./cluster-settings.scss" import React from "react"; import { observer } from "mobx-react"; -import { Features } from "./features" +import { Features } from "./features" import { Removal } from "./removal" import { Status } from "./status" import { General } from "./general" @@ -12,7 +12,6 @@ import { WizardLayout } from "../layout/wizard-layout"; export class ClusterSettings extends React.Component { render() { const cluster = getHostedCluster(); - return ( diff --git a/src/renderer/components/+cluster/cluster.routes.ts b/src/renderer/components/+cluster/cluster.route.ts similarity index 100% rename from src/renderer/components/+cluster/cluster.routes.ts rename to src/renderer/components/+cluster/cluster.route.ts diff --git a/src/renderer/components/+cluster/index.ts b/src/renderer/components/+cluster/index.ts index dfc259440e..62a1be24fb 100644 --- a/src/renderer/components/+cluster/index.ts +++ b/src/renderer/components/+cluster/index.ts @@ -1,2 +1,2 @@ -export * from "./cluster.routes" +export * from "./cluster.route" diff --git a/src/renderer/components/+landing-page/landing-page.tsx b/src/renderer/components/+landing-page/landing-page.tsx index 8de9810210..134c950f92 100644 --- a/src/renderer/components/+landing-page/landing-page.tsx +++ b/src/renderer/components/+landing-page/landing-page.tsx @@ -1,16 +1,18 @@ import "./landing-page.scss" import React from "react"; import { observer } from "mobx-react"; -import { clusterStore } from "../../../common/cluster-store"; import { Trans } from "@lingui/macro"; +import { clusterStore } from "../../../common/cluster-store"; +import { workspaceStore } from "../../../common/workspace-store"; @observer export class LandingPage extends React.Component { render() { - const noClusters = !clusterStore.hasClusters(); + const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId); + const noClustersInScope = !clusters.length; return (
- {noClusters && ( + {noClustersInScope && (

Welcome! diff --git a/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx b/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx index 8d9f0da888..55dc94068a 100644 --- a/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx +++ b/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { Trans } from "@lingui/macro"; import { RouteComponentProps } from "react-router"; import { Icon } from "../icon"; -import { IRoleBindingsRouteParams } from "../+user-management/user-management.routes"; +import { IRoleBindingsRouteParams } from "../+user-management/user-management.route"; import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { clusterRoleBindingApi, RoleBinding, roleBindingApi } from "../../api/endpoints"; import { roleBindingsStore } from "./role-bindings.store"; diff --git a/src/renderer/components/+user-management-roles/roles.tsx b/src/renderer/components/+user-management-roles/roles.tsx index f542fafc42..f9b9e8c239 100644 --- a/src/renderer/components/+user-management-roles/roles.tsx +++ b/src/renderer/components/+user-management-roles/roles.tsx @@ -4,7 +4,7 @@ import React from "react"; import { observer } from "mobx-react"; import { Trans } from "@lingui/macro"; import { RouteComponentProps } from "react-router"; -import { IRolesRouteParams } from "../+user-management/user-management.routes"; +import { IRolesRouteParams } from "../+user-management/user-management.route"; import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { rolesStore } from "./roles.store"; import { clusterRoleApi, Role, roleApi } from "../../api/endpoints"; diff --git a/src/renderer/components/+user-management/index.ts b/src/renderer/components/+user-management/index.ts index f9b243aa50..8250079e60 100644 --- a/src/renderer/components/+user-management/index.ts +++ b/src/renderer/components/+user-management/index.ts @@ -1,2 +1,2 @@ export * from "./user-management" -export * from "./user-management.routes" \ No newline at end of file +export * from "./user-management.route" \ No newline at end of file diff --git a/src/renderer/components/+user-management/user-management.routes.ts b/src/renderer/components/+user-management/user-management.route.ts similarity index 100% rename from src/renderer/components/+user-management/user-management.routes.ts rename to src/renderer/components/+user-management/user-management.route.ts diff --git a/src/renderer/components/+user-management/user-management.tsx b/src/renderer/components/+user-management/user-management.tsx index cff902c84f..28a7964e76 100644 --- a/src/renderer/components/+user-management/user-management.tsx +++ b/src/renderer/components/+user-management/user-management.tsx @@ -8,7 +8,7 @@ import { MainLayout, TabRoute } from "../layout/main-layout"; import { Roles } from "../+user-management-roles"; import { RoleBindings } from "../+user-management-roles-bindings"; import { ServiceAccounts } from "../+user-management-service-accounts"; -import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL, usersManagementURL } from "./user-management.routes"; +import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL, usersManagementURL } from "./user-management.route"; import { namespaceStore } from "../+namespaces/namespace.store"; import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies"; import { isAllowedResource } from "../../../common/rbac"; diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index f0180af17c..c51afbdc14 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1,13 +1,14 @@ -import "./app.scss"; import React from "react"; -import { disposeOnUnmount, observer } from "mobx-react"; -import { observable, reaction } from "mobx"; -import { Redirect, Route, Switch } from "react-router"; +import { observer } from "mobx-react"; +import { Redirect, Route, Router, Switch } from "react-router"; +import { I18nProvider } from "@lingui/react"; +import { _i18n } from "../i18n"; +import { history } from "../navigation"; import { Notifications } from "./notifications"; import { NotFound } from "./+404"; import { UserManagement } from "./+user-management/user-management"; import { ConfirmDialog } from "./confirm-dialog"; -import { usersManagementRoute } from "./+user-management/user-management.routes"; +import { usersManagementRoute } from "./+user-management/user-management.route"; import { clusterRoute, clusterURL } from "./+cluster"; import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog"; import { Nodes, nodesRoute } from "./+nodes"; @@ -27,94 +28,54 @@ import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale import { CustomResources } from "./+custom-resources/custom-resources"; import { crdRoute } from "./+custom-resources"; import { isAllowedResource } from "../../common/rbac"; -import { AddCluster, addClusterRoute } from "./+add-cluster"; -import { LandingPage, landingRoute, landingURL } from "./+landing-page"; import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings"; -import { Workspaces, workspacesRoute } from "./+workspaces"; import { ErrorBoundary } from "./error-boundary"; -import { clusterIpc } from "../../common/cluster-ipc"; -import { getHostedCluster } from "../../common/cluster-store"; -import { clusterStatusRoute, clusterStatusURL } from "./cluster-manager/cluster-status.route"; -import { Preferences, preferencesRoute } from "./+preferences"; -import { ClusterStatus } from "./cluster-manager/cluster-status"; -import { CubeSpinner } from "./spinner"; -import { navigate, navigation } from "../navigation"; +import { Terminal } from "./dock/terminal"; @observer export class App extends React.Component { - @observable isReady = false; - - get cluster() { - return getHostedCluster() - } - - async componentDidMount() { - if (this.cluster) { - await clusterIpc.activate.invokeFromRenderer(); // refresh state, reconnect, etc. - disposeOnUnmount(this, [ - reaction(() => this.cluster.accessible, this.onClusterAccessChange, { - fireImmediately: true - }) - ]) - } - this.isReady = true; - } - - protected onClusterAccessChange = (accessible: boolean) => { - const path = navigation.getPath(); - if (!accessible || path === "/") { - navigate(this.startURL); - } + static async init() { + await Terminal.preloadFonts() } get startURL() { - if (this.cluster) { - if (!this.cluster.accessible) { - return clusterStatusURL(); - } - if (isAllowedResource(["events", "nodes", "pods"])) { - return clusterURL(); - } - return workloadsURL(); + if (isAllowedResource(["events", "nodes", "pods"])) { + return clusterURL(); } - return landingURL(); + return workloadsURL(); } render() { - if (!this.isReady) { - return - } return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } } diff --git a/src/renderer/components/cluster-manager/cluster-manager.scss b/src/renderer/components/cluster-manager/cluster-manager.scss index 4b9040596d..ae6916d147 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.scss +++ b/src/renderer/components/cluster-manager/cluster-manager.scss @@ -8,13 +8,6 @@ #lens-view { position: relative; grid-area: lens-view; - - &.inactive { - opacity: .85; - filter: grayscale(1); - user-select: none; - pointer-events: none; - } } .ClustersMenu { diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 0247742e61..4b72704aa3 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -1,16 +1,17 @@ import "./cluster-manager.scss" import React from "react"; -import { computed } from "mobx"; import { observer } from "mobx-react"; -import { App } from "../app"; import { ClustersMenu } from "./clusters-menu"; import { BottomBar } from "./bottom-bar"; import { cssNames, IClassName } from "../../utils"; -import { Terminal } from "../dock/terminal"; -import { i18nStore } from "../../i18n"; -import { themeStore } from "../../theme.store"; -import { clusterStore, getHostedClusterId, isNoClustersView } from "../../../common/cluster-store"; -import { CubeSpinner } from "../spinner"; +import { ClusterId } from "../../../common/cluster-store"; +import { Route, Switch } from "react-router"; +import { LandingPage, landingRoute } from "../+landing-page"; +import { Preferences, preferencesRoute } from "../+preferences"; +import { Workspaces, workspacesRoute } from "../+workspaces"; +import { AddCluster, addClusterRoute } from "../+add-cluster"; +import { ClusterStatus } from "./cluster-status"; +import { clusterStatusRoute } from "./cluster-status.route"; interface Props { className?: IClassName; @@ -19,34 +20,26 @@ interface Props { @observer export class ClusterManager extends React.Component { - static async init() { - await Promise.all([ - i18nStore.init(), - themeStore.init(), - Terminal.preloadFonts(), - ]) - } - - @computed get isInactive() { - const { activeCluster, activeClusterId, clusters } = clusterStore; - const isActivatedBefore = activeCluster?.initialized; - return clusters.size > 0 && !isActivatedBefore && activeClusterId !== getHostedClusterId(); + activateView(clusterId: ClusterId) { } render() { - const { className, contentClass } = this.props; - const lensViewClass = cssNames("flex column", contentClass, { - inactive: this.isInactive, - }); + const { className } = this.props; return (
-
- +
+ + + + + + +

Lens

}/> +
- {this.isInactive && }
) } diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 0cfe81f724..db7b318d2e 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -6,19 +6,20 @@ import { disposeOnUnmount, observer } from "mobx-react"; import { ipcRenderer } from "electron"; import { autorun, computed, observable } from "mobx"; import { clusterIpc } from "../../../common/cluster-ipc"; -import { getHostedCluster } from "../../../common/cluster-store"; import { Icon } from "../icon"; import { Button } from "../button"; import { cssNames } from "../../utils"; import { navigate } from "../../navigation"; +import { Cluster } from "../../../main/cluster"; @observer export class ClusterStatus extends React.Component { @observable authOutput: KubeAuthProxyLog[] = []; @observable isReconnecting = false; - @computed get cluster() { - return getHostedCluster(); + // fixme + @computed get cluster(): Cluster { + return null; } @computed get hasErrors(): boolean { @@ -33,6 +34,9 @@ export class ClusterStatus extends React.Component { }) async componentDidMount() { + if (this.cluster.disconnected) { + return; + } this.authOutput = [{ data: "Connecting..." }]; ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => { this.authOutput.push({ @@ -40,16 +44,21 @@ export class ClusterStatus extends React.Component { error: res.error, }); }) + await this.refreshClusterState(); } componentWillUnmount() { ipcRenderer.removeAllListeners(`kube-auth:${this.cluster.id}`); } + async refreshClusterState() { + return clusterIpc.activate.invokeFromRenderer(); + } + reconnect = async () => { this.authOutput = [{ data: "Reconnecting..." }]; this.isReconnecting = true; - await clusterIpc.activate.invokeFromRenderer(); + await this.refreshClusterState(); this.isReconnecting = false; } diff --git a/src/renderer/components/cluster-manager/clusters-menu.scss b/src/renderer/components/cluster-manager/clusters-menu.scss index d301578c13..ce1dab92d5 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.scss +++ b/src/renderer/components/cluster-manager/clusters-menu.scss @@ -42,6 +42,7 @@ > .add-cluster { position: relative; margin-top: $padding; + min-width: 43px; .Icon { border-radius: $radius; diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index e4adeb8951..2d58c82367 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -85,11 +85,10 @@ export class ClustersMenu extends React.Component { render() { const { className } = this.props; const { newContexts } = userStore; - const { currentWorkspaceId } = workspaceStore; - const clusters = clusterStore.getByWorkspaceId(currentWorkspaceId); - const noClusters = !clusterStore.clusters.size; + const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId); + const noClustersInScope = clusters.length === 0; const isLanding = navigation.getPath() === landingURL(); - const showStartupHint = this.showHint && isLanding && noClusters; + const showStartupHint = this.showHint && isLanding && noClustersInScope; return (
{ render() { const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props; - const { contextName: clusterName } = getHostedCluster(); const routePath = navigation.location.pathname; return (
- {clusterName} + + {getHostedCluster().contextName} +