1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-08-05 19:08:20 +03:00
parent 1f5acdb9cd
commit e5138e7c5d
38 changed files with 260 additions and 435 deletions

View File

@ -1,10 +1,8 @@
import type { WorkspaceId } from "./workspace-store"; import type { WorkspaceId } from "./workspace-store";
import path from "path"; import path from "path";
import filenamify from "filenamify";
import { app, ipcRenderer, remote } from "electron"; 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 { action, computed, observable, toJS } from "mobx";
import { appProto, noClustersHost } from "./vars";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster"; import { Cluster, ClusterState } from "../main/cluster";
import migrations from "../migrations/cluster-store" import migrations from "../migrations/cluster-store"
@ -177,12 +175,11 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
export const clusterStore = ClusterStore.getInstance<ClusterStore>(); export const clusterStore = ClusterStore.getInstance<ClusterStore>();
export function isNoClustersView() { export function getHostedClusterId(): ClusterId {
return location.hostname === noClustersHost const clusterHost = location.hostname.match(/^(.*?)\.localhost/);
if (clusterHost) {
return clusterHost[1]
} }
export function getHostedClusterId() {
return location.hostname.split(".")[0];
} }
export function getHostedCluster(): Cluster { export function getHostedCluster(): Cluster {

View File

@ -156,7 +156,7 @@ export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig |
export async function getKubeConfigLocal(): Promise<string> { export async function getKubeConfigLocal(): Promise<string> {
try { 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 file = await readFile(configFile, "utf8");
const obj = yaml.safeLoad(file); const obj = yaml.safeLoad(file);
if (obj.contexts) { if (obj.contexts) {

View File

@ -71,11 +71,12 @@ export class UserStore extends BaseStore<UserStoreModel> {
if (kubeConfig) { if (kubeConfig) {
this.newContexts.clear(); this.newContexts.clear();
const localContexts = loadConfig(kubeConfig).getContexts(); const localContexts = loadConfig(kubeConfig).getContexts();
console.log(localContexts) localContexts.forEach(({ cluster, name }) => {
localContexts if (!cluster) return;
.filter(ctx => ctx.cluster) if (!this.seenContexts.has(name)) {
.filter(ctx => !this.seenContexts.has(ctx.name)) this.newContexts.add(name)
.forEach(ctx => this.newContexts.add(ctx.name)); }
})
} }
} }

View File

@ -10,8 +10,6 @@ export const isDevelopment = isDebugging || !isProduction;
export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isTestEnv = !!process.env.JEST_WORKER_ID;
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}` 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 // System paths
export const contextDir = process.cwd(); 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 htmlTemplate = path.resolve(rendererDir, "template.html");
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss"); export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
// System pages
export const noClustersHost = "no-clusters.localhost"
// Apis // Apis
export const apiPrefix = "/api" // local router apis export const apiPrefix = "/api" // local router apis
export const apiKubePrefix = "/api-kube" // k8s cluster apis export const apiKubePrefix = "/api-kube" // k8s cluster apis

View File

@ -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 { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store" import { clusterStore } from "./cluster-store"
@ -22,6 +22,15 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
super({ super({
configName: "lens-workspace-store", 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; @observable currentWorkspaceId = WorkspaceStore.defaultId;

View File

@ -1,6 +1,5 @@
import type http from "http" import type http from "http"
import { autorun } from "mobx"; import { autorun } from "mobx";
import { apiKubePrefix } from "../common/vars";
import { ClusterId, clusterStore } from "../common/cluster-store" import { ClusterId, clusterStore } from "../common/cluster-store"
import { Cluster } from "./cluster" import { Cluster } from "./cluster"
import { clusterIpc } from "../common/cluster-ipc"; import { clusterIpc } from "../common/cluster-ipc";
@ -50,23 +49,8 @@ export class ClusterManager {
} }
getClusterForRequest(req: http.IncomingMessage): Cluster { getClusterForRequest(req: http.IncomingMessage): Cluster {
let cluster: Cluster = null logger.info(`getClusterForRequest(): ${req.headers.host}${req.url}`)
const clusterId = req.headers.host.split(".")[0]
// lens-server is connecting to 127.0.0.1:<port>/<uid> return this.getCluster(clusterId)
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;
} }
} }

View File

@ -1,7 +1,8 @@
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature" import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store"; 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 { apiKubePrefix } from "../common/vars";
import { broadcastIpc } from "../common/ipc"; import { broadcastIpc } from "../common/ipc";
import { ContextHandler } from "./context-handler" import { ContextHandler } from "./context-handler"
@ -52,7 +53,6 @@ export class Cluster implements ClusterModel {
@observable kubeConfigPath: string; @observable kubeConfigPath: string;
@observable apiUrl: string; // cluster server url @observable apiUrl: string; // cluster server url
@observable kubeProxyUrl: string; // lens-proxy to kube-api url @observable kubeProxyUrl: string; // lens-proxy to kube-api url
@observable webContentUrl: string; // page content url for loading in renderer
@observable online: boolean; @observable online: boolean;
@observable accessible: boolean; @observable accessible: boolean;
@observable disconnected: boolean; @observable disconnected: boolean;
@ -67,6 +67,11 @@ export class Cluster implements ClusterModel {
@observable allowedNamespaces: string[] = []; @observable allowedNamespaces: string[] = [];
@observable allowedResources: string[] = []; @observable allowedResources: string[] = [];
@computed get host() {
const proxyHost = new URL(this.kubeProxyUrl).host;
return `${this.id}.${proxyHost}`
}
constructor(model: ClusterModel) { constructor(model: ClusterModel) {
this.updateModel(model); this.updateModel(model);
} }
@ -80,20 +85,15 @@ export class Cluster implements ClusterModel {
@action @action
async init(port: number) { async init(port: number) {
if (this.initialized) {
return;
}
try { try {
this.contextHandler = new ContextHandler(this); this.contextHandler = new ContextHandler(this);
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler); this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler);
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
this.webContentUrl = `http://${this.id}.localhost:${port}`;
this.initialized = true; this.initialized = true;
logger.info(`[CLUSTER]: init success`, { logger.info(`[CLUSTER]: "${this.contextName}" init success`, {
id: this.id, id: this.id,
serverUrl: this.apiUrl, context: this.contextName,
webContentUrl: this.webContentUrl, apiUrl: this.apiUrl
kubeProxyUrl: this.kubeProxyUrl,
}); });
} catch (err) { } catch (err) {
logger.error(`[CLUSTER]: init failed: ${err}`, { logger.error(`[CLUSTER]: init failed: ${err}`, {
@ -155,7 +155,7 @@ export class Cluster implements ClusterModel {
@action @action
async refresh() { async refresh() {
logger.info(`[CLUSTER]: refresh`, this.getMeta()); logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.refreshConnectionStatus(); await this.refreshConnectionStatus(); // refresh "version", "online", etc.
if (this.accessible) { if (this.accessible) {
this.kubeCtl = new Kubectl(this.version) this.kubeCtl = new Kubectl(this.version)
this.distribution = this.detectKubernetesDistribution(this.version) this.distribution = this.detectKubernetesDistribution(this.version)
@ -217,22 +217,28 @@ export class Cluster implements ClusterModel {
return uninstallFeature(name, this) return uninstallFeature(name, this)
} }
getPrometheusApiPrefix() { protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
return this.preferences.prometheus?.prefix || ""
}
protected async k8sRequest(path: string, options: RequestPromiseOptions = {}) {
const apiUrl = this.kubeProxyUrl + path; const apiUrl = this.kubeProxyUrl + path;
return request(apiUrl, { return request(apiUrl, {
json: true, json: true,
timeout: 5000, timeout: 5000,
headers: { headers: {
Host: this.host, // provide cluster-id for ClusterManager.getClusterForRequest()
...(options.headers || {}), ...(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<ClusterStatus> { protected async getConnectionStatus(): Promise<ClusterStatus> {
try { try {
const response = await this.k8sRequest("/version") const response = await this.k8sRequest("/version")

View File

@ -3,9 +3,8 @@
import "../common/system-ca" import "../common/system-ca"
import "../common/prometheus-providers" import "../common/prometheus-providers"
import { app, dialog } from "electron" import { app, dialog } from "electron"
import { appName, appProto, staticDir, staticProto } from "../common/vars"; import { appName, staticDir } from "../common/vars";
import path from "path" import path from "path"
import { initMenu } from "./menu"
import { LensProxy } from "./lens-proxy" import { LensProxy } from "./lens-proxy"
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
@ -41,8 +40,7 @@ async function main() {
const updater = new AppUpdater() const updater = new AppUpdater()
updater.start(); updater.start();
registerFileProtocol(appProto, app.getPath("userData")); registerFileProtocol("static", staticDir);
registerFileProtocol(staticProto, staticDir);
// find free port // find free port
let proxyPort: number let proxyPort: number
@ -74,7 +72,6 @@ async function main() {
// create window manager and open app // create window manager and open app
windowManager = new WindowManager(proxyPort); windowManager = new WindowManager(proxyPort);
initMenu(windowManager);
} }
app.on("ready", main); app.on("ready", main);

View File

@ -7,10 +7,11 @@ import { openShell } from "./node-shell-session";
import { Router } from "./router" import { Router } from "./router"
import { ClusterManager } from "./cluster-manager" import { ClusterManager } from "./cluster-manager"
import { ContextHandler } from "./context-handler"; import { ContextHandler } from "./context-handler";
import { apiKubePrefix, noClustersHost } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import logger from "./logger" import logger from "./logger"
export class LensProxy { export class LensProxy {
protected origin: string
protected proxyServer: http.Server protected proxyServer: http.Server
protected router: Router protected router: Router
protected closed = false protected closed = false
@ -21,12 +22,13 @@ export class LensProxy {
} }
private constructor(protected port: number, protected clusterManager: ClusterManager) { private constructor(protected port: number, protected clusterManager: ClusterManager) {
this.origin = `http://localhost:${port}`
this.router = new Router(); this.router = new Router();
} }
listen(port = this.port): this { listen(port = this.port): this {
this.proxyServer = this.buildCustomProxy().listen(port); 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; return this;
} }
@ -117,27 +119,18 @@ export class LensProxy {
} }
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) { 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) const cluster = this.clusterManager.getClusterForRequest(req)
if (!cluster) { if (cluster) {
const reqId = this.getRequestId(req); await cluster.contextHandler.ensureServer();
logger.error("Got request to unknown cluster", { reqId }) const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler)
res.statusCode = 503
res.end()
return
}
const contextHandler = cluster.contextHandler
await contextHandler.ensureServer();
const proxyTarget = await this.getProxyTarget(req, contextHandler)
if (proxyTarget) { if (proxyTarget) {
proxy.web(req, res, proxyTarget) // allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
} else { res.setHeader("Access-Control-Allow-Origin", this.origin);
this.router.route(cluster, req, res); return proxy.web(req, res, proxyTarget);
} }
} }
this.router.route(cluster, req, res);
}
protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) { protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const wsServer = this.createWsListener(); const wsServer = this.createWsListener();

View File

@ -1,7 +1,6 @@
import type { WindowManager } from "./window-manager"; import type { WindowManager } from "./window-manager";
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron" import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron"
import { autorun } from "mobx"; import { autorun } from "mobx";
import { broadcastIpc } from "../common/ipc";
import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars"; import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars";
import { clusterStore } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store";
import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route"; import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
@ -17,19 +16,7 @@ export function initMenu(windowManager: WindowManager) {
}); });
} }
function buildMenu(windowManager: WindowManager) { export 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],
});
}
function macOnly(menuItems: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] { function macOnly(menuItems: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] {
if (!isMac) return []; if (!isMac) return [];
return menuItems; return menuItems;
@ -41,20 +28,20 @@ function buildMenu(windowManager: WindowManager) {
{ {
label: 'Add Cluster', label: 'Add Cluster',
click() { click() {
navigate(addClusterURL()) windowManager.navigateMain(addClusterURL())
} }
}, },
...(hasClusters ? [{ ...(clusterStore.activeCluster ? [{
label: 'Cluster Settings', label: 'Cluster Settings',
click() { click() {
navigate(clusterSettingsURL()) windowManager.navigateMain(clusterSettingsURL())
} }
}] : []), }] : []),
{ type: 'separator' }, { type: 'separator' },
{ {
label: 'Preferences', label: 'Preferences',
click() { click() {
navigate(preferencesURL()) windowManager.navigateMain(preferencesURL())
} }
}, },
...macOnly([ ...macOnly([
@ -125,7 +112,7 @@ function buildMenu(windowManager: WindowManager) {
{ {
label: "What's new?", label: "What's new?",
click() { click() {
navigate(whatsNewURL()) windowManager.navigateMain(whatsNewURL())
}, },
}, },
{ {

View File

@ -1,7 +1,5 @@
import url from "url"
import { LensApiRequest } from "../router" import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" import { LensApi } from "../lens-api"
import requestPromise from "request-promise-native"
import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry" import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry"
export type IMetricsQuery = string | string[] | { export type IMetricsQuery = string | string[] | {
@ -9,25 +7,17 @@ export type IMetricsQuery = string | string[] | {
} }
class MetricsRoute extends LensApi { class MetricsRoute extends LensApi {
async routeMetrics(request: LensApiRequest) {
public async routeMetrics(request: LensApiRequest) {
const { response, cluster, payload } = request const { response, cluster, payload } = request
const { contextHandler, kubeProxyUrl } = cluster;
const headers: Record<string, string> = {
"Host": url.parse(cluster.webContentUrl).host,
"Content-type": "application/json",
}
const queryParams: IMetricsQuery = {} const queryParams: IMetricsQuery = {}
request.query.forEach((value: string, key: string) => { request.query.forEach((value: string, key: string) => {
queryParams[key] = value queryParams[key] = value
}) })
let prometheusPath: string
let metricsUrl: string
let prometheusProvider: PrometheusProvider let prometheusProvider: PrometheusProvider
try { try {
const prometheusPath = await contextHandler.getPrometheusPath() prometheusPath = await cluster.contextHandler.getPrometheusPath()
metricsUrl = `${kubeProxyUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range` prometheusProvider = await cluster.contextHandler.getPrometheusProvider()
prometheusProvider = await contextHandler.getPrometheusProvider()
} catch { } catch {
this.respondJson(response, {}) this.respondJson(response, {})
return return
@ -35,18 +25,10 @@ class MetricsRoute extends LensApi {
// prometheus metrics loader // prometheus metrics loader
const attempts: { [query: string]: number } = {}; const attempts: { [query: string]: number } = {};
const maxAttempts = 5; const maxAttempts = 5;
const loadMetrics = (orgQuery: string): Promise<any> => { const loadMetrics = (promQuery: string): Promise<any> => {
const query = orgQuery.trim() const query = promQuery.trim()
const attempt = attempts[query] = (attempts[query] || 0) + 1; const attempt = attempts[query] = (attempts[query] || 0) + 1;
return requestPromise(metricsUrl, { return cluster.getMetrics(prometheusPath, { query, ...queryParams }).catch(async error => {
resolveWithFullResponse: false,
headers: headers,
json: true,
qs: {
query: query,
...queryParams
}
}).catch(async (error) => {
if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) { if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) {
await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
return loadMetrics(query); return loadMetrics(query);

View File

@ -1,66 +1,56 @@
import { reaction } from "mobx";
import { BrowserWindow, shell } from "electron" import { BrowserWindow, shell } from "electron"
import windowStateKeeper from "electron-window-state" import windowStateKeeper from "electron-window-state"
import type { ClusterId } from "../common/cluster-store"; import { initMenu } from "./menu";
import { clusterStore } from "../common/cluster-store";
import { noClustersHost } from "../common/vars";
import logger from "./logger";
export class WindowManager { export class WindowManager {
protected activeView: BrowserWindow; protected mainView: BrowserWindow;
protected splashWindow: BrowserWindow; protected splashWindow: BrowserWindow;
protected noClustersWindow: BrowserWindow;
protected views = new Map<ClusterId, BrowserWindow>();
protected disposers: CallableFunction[] = [];
protected windowState: windowStateKeeper.State; 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 // Manage main window size and position with state persistence
this.windowState = windowStateKeeper({ this.windowState = windowStateKeeper({
defaultHeight: 900, defaultHeight: 900,
defaultWidth: 1440, defaultWidth: 1440,
}); });
// Show while app not ready const { width, height, x, y } = this.windowState;
if (showSplash) { this.mainView = new BrowserWindow({
this.showSplash(); x, y, width, height,
} show: false,
minWidth: 900,
// Manage reactive state minHeight: 760,
this.disposers.push( titleBarStyle: "hidden",
// auto-show/hide "no-clusters" window when necessary backgroundColor: "#1e2124",
reaction(() => clusterStore.hasClusters(), hasClusters => { webPreferences: {
this.handleNoClustersView({ activate: !hasClusters }); nodeIntegration: true,
}, { enableRemoteModule: true,
fireImmediately: true },
}),
// 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);
}); });
}, { this.windowState.manage(this.mainView);
delay: 25, // fix: destroy later and allow to use view's state in next activateView()
}), // open external links in default browser (target=_blank, window.open)
); this.mainView.webContents.on("new-window", (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
// load & show app
this.showMain();
} }
protected handleNoClustersView = async ({ activate = false } = {}) => { // fixme
if (!this.noClustersWindow) { navigateMain(url: string) {
this.noClustersWindow = this.initClusterView(null); this.mainView.webContents.executeJavaScript("console.log('implement me!')")
await this.noClustersWindow.loadURL(`http://${noClustersHost}:${this.proxyPort}`);
}
if (activate) {
this.activeView = this.noClustersWindow;
this.noClustersWindow.show();
this.hideSplash();
} }
async showMain() {
await this.showSplash();
await this.mainView.loadURL(`http://localhost:${this.proxyPort}`)
this.mainView.show();
this.splashWindow.hide();
} }
async showSplash() { async showSplash() {
@ -79,95 +69,9 @@ export class WindowManager {
this.splashWindow.show(); this.splashWindow.show();
} }
hideSplash() {
this.splashWindow.hide();
}
getClusterView(clusterId: ClusterId): BrowserWindow {
return this.views.get(clusterId);
}
activateView = async (clusterId: ClusterId): Promise<number> => {
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() { destroy() {
this.windowState.unmanage(); 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.destroy();
this.splashWindow = null; this.mainView.destroy();
this.activeView = null;
} }
} }

View File

@ -4,16 +4,16 @@ import { Notifications } from "../components/notifications";
import { apiKubePrefix, apiPrefix, isDevelopment } from "../../common/vars"; import { apiKubePrefix, apiPrefix, isDevelopment } from "../../common/vars";
export const apiBase = new JsonApi({ export const apiBase = new JsonApi({
apiBase: apiPrefix,
debug: isDevelopment, debug: isDevelopment,
apiPrefix: apiPrefix,
}); });
export const apiKube = new KubeJsonApi({ export const apiKube = new KubeJsonApi({
apiBase: apiKubePrefix,
debug: isDevelopment, debug: isDevelopment,
apiPrefix: apiKubePrefix,
}); });
// Common handler for HTTP api errors // Common handler for HTTP api errors
function onApiError(error: JsonApiErrorParsed, res: Response) { export function onApiError(error: JsonApiErrorParsed, res: Response) {
switch (res.status) { switch (res.status) {
case 403: case 403:
error.isUsedForNotification = true; error.isUsedForNotification = true;

View File

@ -27,7 +27,7 @@ export interface JsonApiLog {
} }
export interface JsonApiConfig { export interface JsonApiConfig {
apiPrefix: string; apiBase: string;
debug?: boolean; debug?: boolean;
} }
@ -72,7 +72,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
} }
protected request<D>(path: string, params?: P, init: RequestInit = {}) { protected request<D>(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 reqInit: RequestInit = { ...this.reqInit, ...init };
const { data, query } = params || {} as P; const { data, query } = params || {} as P;
if (data && !reqInit.body) { if (data && !reqInit.body) {

View File

@ -61,7 +61,7 @@ export class KubeWatchApi {
} }
protected getQuery(): Partial<IKubeWatchRouteQuery> { protected getQuery(): Partial<IKubeWatchRouteQuery> {
const { isAdmin, allowedNamespaces } = getHostedCluster(); const { isAdmin, allowedNamespaces } = getHostedCluster()
return { return {
api: this.activeApis.map(api => { api: this.activeApis.map(api => {
if (isAdmin) return api.getWatchUrl(); if (isAdmin) return api.getWatchUrl();

View File

@ -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(<App/>, rootElem);
}
// run
bootstrap(getHostedClusterId() ? App : LensApp);

View File

@ -58,7 +58,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
this.isLoading = true; this.isLoading = true;
let items; let items;
try { try {
const { isAdmin, allowedNamespaces } = getHostedCluster(); const { isAdmin, allowedNamespaces } = getHostedCluster()
items = await this.loadItems(!isAdmin ? allowedNamespaces : null); items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
} finally { } finally {
if (items) { if (items) {

View File

@ -12,7 +12,6 @@ import { WizardLayout } from "../layout/wizard-layout";
export class ClusterSettings extends React.Component { export class ClusterSettings extends React.Component {
render() { render() {
const cluster = getHostedCluster(); const cluster = getHostedCluster();
return ( return (
<WizardLayout className="ClusterSettings"> <WizardLayout className="ClusterSettings">
<Status cluster={cluster}></Status> <Status cluster={cluster}></Status>

View File

@ -1,2 +1,2 @@
export * from "./cluster.routes" export * from "./cluster.route"

View File

@ -1,16 +1,18 @@
import "./landing-page.scss" import "./landing-page.scss"
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { clusterStore } from "../../../common/cluster-store";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store";
@observer @observer
export class LandingPage extends React.Component { export class LandingPage extends React.Component {
render() { render() {
const noClusters = !clusterStore.hasClusters(); const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
const noClustersInScope = !clusters.length;
return ( return (
<div className="LandingPage flex"> <div className="LandingPage flex">
{noClusters && ( {noClustersInScope && (
<div className="no-clusters flex column gaps box center"> <div className="no-clusters flex column gaps box center">
<h1> <h1>
<Trans>Welcome!</Trans> <Trans>Welcome!</Trans>

View File

@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { Icon } from "../icon"; 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 { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { clusterRoleBindingApi, RoleBinding, roleBindingApi } from "../../api/endpoints"; import { clusterRoleBindingApi, RoleBinding, roleBindingApi } from "../../api/endpoints";
import { roleBindingsStore } from "./role-bindings.store"; import { roleBindingsStore } from "./role-bindings.store";

View File

@ -4,7 +4,7 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import { RouteComponentProps } from "react-router"; 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 { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { rolesStore } from "./roles.store"; import { rolesStore } from "./roles.store";
import { clusterRoleApi, Role, roleApi } from "../../api/endpoints"; import { clusterRoleApi, Role, roleApi } from "../../api/endpoints";

View File

@ -1,2 +1,2 @@
export * from "./user-management" export * from "./user-management"
export * from "./user-management.routes" export * from "./user-management.route"

View File

@ -8,7 +8,7 @@ import { MainLayout, TabRoute } from "../layout/main-layout";
import { Roles } from "../+user-management-roles"; import { Roles } from "../+user-management-roles";
import { RoleBindings } from "../+user-management-roles-bindings"; import { RoleBindings } from "../+user-management-roles-bindings";
import { ServiceAccounts } from "../+user-management-service-accounts"; 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 { namespaceStore } from "../+namespaces/namespace.store";
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies"; import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
import { isAllowedResource } from "../../../common/rbac"; import { isAllowedResource } from "../../../common/rbac";

View File

@ -1,13 +1,14 @@
import "./app.scss";
import React from "react"; import React from "react";
import { disposeOnUnmount, observer } from "mobx-react"; import { observer } from "mobx-react";
import { observable, reaction } from "mobx"; import { Redirect, Route, Router, Switch } from "react-router";
import { Redirect, Route, Switch } from "react-router"; import { I18nProvider } from "@lingui/react";
import { _i18n } from "../i18n";
import { history } from "../navigation";
import { Notifications } from "./notifications"; import { Notifications } from "./notifications";
import { NotFound } from "./+404"; import { NotFound } from "./+404";
import { UserManagement } from "./+user-management/user-management"; import { UserManagement } from "./+user-management/user-management";
import { ConfirmDialog } from "./confirm-dialog"; 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 { clusterRoute, clusterURL } from "./+cluster";
import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog"; import { KubeConfigDialog } from "./kubeconfig-dialog/kubeconfig-dialog";
import { Nodes, nodesRoute } from "./+nodes"; import { Nodes, nodesRoute } from "./+nodes";
@ -27,72 +28,30 @@ import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale
import { CustomResources } from "./+custom-resources/custom-resources"; import { CustomResources } from "./+custom-resources/custom-resources";
import { crdRoute } from "./+custom-resources"; import { crdRoute } from "./+custom-resources";
import { isAllowedResource } from "../../common/rbac"; 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 { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
import { Workspaces, workspacesRoute } from "./+workspaces";
import { ErrorBoundary } from "./error-boundary"; import { ErrorBoundary } from "./error-boundary";
import { clusterIpc } from "../../common/cluster-ipc"; import { Terminal } from "./dock/terminal";
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";
@observer @observer
export class App extends React.Component { export class App extends React.Component {
@observable isReady = false; static async init() {
await Terminal.preloadFonts()
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);
}
} }
get startURL() { get startURL() {
if (this.cluster) {
if (!this.cluster.accessible) {
return clusterStatusURL();
}
if (isAllowedResource(["events", "nodes", "pods"])) { if (isAllowedResource(["events", "nodes", "pods"])) {
return clusterURL(); return clusterURL();
} }
return workloadsURL(); return workloadsURL();
} }
return landingURL();
}
render() { render() {
if (!this.isReady) {
return <CubeSpinner className="box center"/>
}
return ( return (
<I18nProvider i18n={_i18n}>
<Router history={history}>
<ErrorBoundary> <ErrorBoundary>
<Switch> <Switch>
<Route component={LandingPage} {...landingRoute}/>
<Route component={Preferences} {...preferencesRoute}/>
<Route component={Workspaces} {...workspacesRoute}/>
<Route component={AddCluster} {...addClusterRoute}/>
<Route component={Cluster} {...clusterRoute}/> <Route component={Cluster} {...clusterRoute}/>
<Route component={ClusterStatus} {...clusterStatusRoute}/>
<Route component={ClusterSettings} {...clusterSettingsRoute}/> <Route component={ClusterSettings} {...clusterSettingsRoute}/>
<Route component={Nodes} {...nodesRoute}/> <Route component={Nodes} {...nodesRoute}/>
<Route component={Workloads} {...workloadsRoute}/> <Route component={Workloads} {...workloadsRoute}/>
@ -115,6 +74,8 @@ export class App extends React.Component {
<PodLogsDialog/> <PodLogsDialog/>
<DeploymentScaleDialog/> <DeploymentScaleDialog/>
</ErrorBoundary> </ErrorBoundary>
</Router>
</I18nProvider>
) )
} }
} }

View File

@ -8,13 +8,6 @@
#lens-view { #lens-view {
position: relative; position: relative;
grid-area: lens-view; grid-area: lens-view;
&.inactive {
opacity: .85;
filter: grayscale(1);
user-select: none;
pointer-events: none;
}
} }
.ClustersMenu { .ClustersMenu {

View File

@ -1,16 +1,17 @@
import "./cluster-manager.scss" import "./cluster-manager.scss"
import React from "react"; import React from "react";
import { computed } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { App } from "../app";
import { ClustersMenu } from "./clusters-menu"; import { ClustersMenu } from "./clusters-menu";
import { BottomBar } from "./bottom-bar"; import { BottomBar } from "./bottom-bar";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { Terminal } from "../dock/terminal"; import { ClusterId } from "../../../common/cluster-store";
import { i18nStore } from "../../i18n"; import { Route, Switch } from "react-router";
import { themeStore } from "../../theme.store"; import { LandingPage, landingRoute } from "../+landing-page";
import { clusterStore, getHostedClusterId, isNoClustersView } from "../../../common/cluster-store"; import { Preferences, preferencesRoute } from "../+preferences";
import { CubeSpinner } from "../spinner"; import { Workspaces, workspacesRoute } from "../+workspaces";
import { AddCluster, addClusterRoute } from "../+add-cluster";
import { ClusterStatus } from "./cluster-status";
import { clusterStatusRoute } from "./cluster-status.route";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
@ -19,34 +20,26 @@ interface Props {
@observer @observer
export class ClusterManager extends React.Component<Props> { export class ClusterManager extends React.Component<Props> {
static async init() { activateView(clusterId: ClusterId) {
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();
} }
render() { render() {
const { className, contentClass } = this.props; const { className } = this.props;
const lensViewClass = cssNames("flex column", contentClass, {
inactive: this.isInactive,
});
return ( return (
<div className={cssNames("ClusterManager", className)}> <div className={cssNames("ClusterManager", className)}>
<div id="draggable-top"/> <div id="draggable-top"/>
<div id="lens-view" className={lensViewClass}> <div id="lens-view">
<App/> <Switch>
<Route component={LandingPage} {...landingRoute}/>
<Route component={Preferences} {...preferencesRoute}/>
<Route component={Workspaces} {...workspacesRoute}/>
<Route component={AddCluster} {...addClusterRoute}/>
<Route component={ClusterStatus} {...clusterStatusRoute}/>
<Route render={() => <p>Lens</p>}/>
</Switch>
</div> </div>
<ClustersMenu/> <ClustersMenu/>
<BottomBar/> <BottomBar/>
{this.isInactive && <CubeSpinner center/>}
</div> </div>
) )
} }

View File

@ -6,19 +6,20 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { autorun, computed, observable } from "mobx"; import { autorun, computed, observable } from "mobx";
import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterIpc } from "../../../common/cluster-ipc";
import { getHostedCluster } from "../../../common/cluster-store";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Button } from "../button"; import { Button } from "../button";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { Cluster } from "../../../main/cluster";
@observer @observer
export class ClusterStatus extends React.Component { export class ClusterStatus extends React.Component {
@observable authOutput: KubeAuthProxyLog[] = []; @observable authOutput: KubeAuthProxyLog[] = [];
@observable isReconnecting = false; @observable isReconnecting = false;
@computed get cluster() { // fixme
return getHostedCluster(); @computed get cluster(): Cluster {
return null;
} }
@computed get hasErrors(): boolean { @computed get hasErrors(): boolean {
@ -33,6 +34,9 @@ export class ClusterStatus extends React.Component {
}) })
async componentDidMount() { async componentDidMount() {
if (this.cluster.disconnected) {
return;
}
this.authOutput = [{ data: "Connecting..." }]; this.authOutput = [{ data: "Connecting..." }];
ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => { ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => {
this.authOutput.push({ this.authOutput.push({
@ -40,16 +44,21 @@ export class ClusterStatus extends React.Component {
error: res.error, error: res.error,
}); });
}) })
await this.refreshClusterState();
} }
componentWillUnmount() { componentWillUnmount() {
ipcRenderer.removeAllListeners(`kube-auth:${this.cluster.id}`); ipcRenderer.removeAllListeners(`kube-auth:${this.cluster.id}`);
} }
async refreshClusterState() {
return clusterIpc.activate.invokeFromRenderer();
}
reconnect = async () => { reconnect = async () => {
this.authOutput = [{ data: "Reconnecting..." }]; this.authOutput = [{ data: "Reconnecting..." }];
this.isReconnecting = true; this.isReconnecting = true;
await clusterIpc.activate.invokeFromRenderer(); await this.refreshClusterState();
this.isReconnecting = false; this.isReconnecting = false;
} }

View File

@ -42,6 +42,7 @@
> .add-cluster { > .add-cluster {
position: relative; position: relative;
margin-top: $padding; margin-top: $padding;
min-width: 43px;
.Icon { .Icon {
border-radius: $radius; border-radius: $radius;

View File

@ -85,11 +85,10 @@ export class ClustersMenu extends React.Component<Props> {
render() { render() {
const { className } = this.props; const { className } = this.props;
const { newContexts } = userStore; const { newContexts } = userStore;
const { currentWorkspaceId } = workspaceStore; const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
const clusters = clusterStore.getByWorkspaceId(currentWorkspaceId); const noClustersInScope = clusters.length === 0;
const noClusters = !clusterStore.clusters.size;
const isLanding = navigation.getPath() === landingURL(); const isLanding = navigation.getPath() === landingURL();
const showStartupHint = this.showHint && isLanding && noClusters; const showStartupHint = this.showHint && isLanding && noClustersInScope;
return ( return (
<div <div
className={cssNames("ClustersMenu flex column gaps", className)} className={cssNames("ClustersMenu flex column gaps", className)}

View File

@ -10,8 +10,8 @@ import { Sidebar } from "./sidebar";
import { ErrorBoundary } from "../error-boundary"; import { ErrorBoundary } from "../error-boundary";
import { Dock } from "../dock"; import { Dock } from "../dock";
import { navigate, navigation } from "../../navigation"; import { navigate, navigation } from "../../navigation";
import { themeStore } from "../../theme.store";
import { getHostedCluster } from "../../../common/cluster-store"; import { getHostedCluster } from "../../../common/cluster-store";
import { themeStore } from "../../theme.store";
export interface TabRoute extends RouteProps { export interface TabRoute extends RouteProps {
title: React.ReactNode; title: React.ReactNode;
@ -47,12 +47,13 @@ export class MainLayout extends React.Component<Props> {
render() { render() {
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props; const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
const { contextName: clusterName } = getHostedCluster();
const routePath = navigation.location.pathname; const routePath = navigation.location.pathname;
return ( return (
<div className={cssNames("MainLayout", className, themeStore.activeTheme.type)}> <div className={cssNames("MainLayout", className, themeStore.activeTheme.type)}>
<header className={cssNames("flex gaps align-center", headerClass)}> <header className={cssNames("flex gaps align-center", headerClass)}>
<span className="cluster">{clusterName}</span> <span className="cluster">
{getHostedCluster().contextName}
</span>
</header> </header>
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}> <aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>

View File

@ -11,7 +11,7 @@ import { Icon } from "../icon";
import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route"; import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route";
import { namespacesURL } from "../+namespaces/namespaces.route"; import { namespacesURL } from "../+namespaces/namespaces.route";
import { nodesURL } from "../+nodes/nodes.route"; import { nodesURL } from "../+nodes/nodes.route";
import { usersManagementRoute, usersManagementURL } from "../+user-management/user-management.routes"; import { usersManagementRoute, usersManagementURL } from "../+user-management/user-management.route";
import { networkRoute, networkURL } from "../+network/network.route"; import { networkRoute, networkURL } from "../+network/network.route";
import { storageRoute, storageURL } from "../+storage/storage.route"; import { storageRoute, storageURL } from "../+storage/storage.route";
import { clusterURL } from "../+cluster"; import { clusterURL } from "../+cluster";
@ -43,7 +43,9 @@ interface Props {
@observer @observer
export class Sidebar extends React.Component<Props> { export class Sidebar extends React.Component<Props> {
async componentDidMount() { async componentDidMount() {
if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) crdStore.loadAll() if (!crdStore.isLoaded && isAllowedResource('customresourcedefinitions')) {
crdStore.loadAll()
}
} }
renderCustomResources() { renderCustomResources() {

View File

@ -1,33 +1,17 @@
import "../common/system-ca" import "../common/system-ca"
import React from "react"; import React from "react";
import { render } from "react-dom";
import { Route, Router, Switch } from "react-router"; import { Route, Router, Switch } from "react-router";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { userStore } from "../common/user-store"; import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
import { clusterStore } from "../common/cluster-store";
import { I18nProvider } from "@lingui/react"; import { I18nProvider } from "@lingui/react";
import { history } from "./navigation"; import { history } from "./navigation";
import { isMac } from "../common/vars";
import { _i18n } from "./i18n"; import { _i18n } from "./i18n";
import { ClusterManager } from "./components/cluster-manager"; import { ClusterManager } from "./components/cluster-manager";
import { ErrorBoundary } from "./components/error-boundary"; import { ErrorBoundary } from "./components/error-boundary";
import { WhatsNew, whatsNewRoute } from "./components/+whats-new"; import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
@observer @observer
class LensApp extends React.Component { export class LensApp extends React.Component {
static async init() {
const rootElem = document.getElementById("app");
rootElem.classList.toggle("is-mac", isMac);
await Promise.all([
userStore.load(),
workspaceStore.load(),
clusterStore.load(),
]);
await ClusterManager.init();
render(<LensApp/>, rootElem);
}
render() { render() {
return ( return (
<I18nProvider i18n={_i18n}> <I18nProvider i18n={_i18n}>
@ -44,6 +28,3 @@ class LensApp extends React.Component {
) )
} }
} }
// run
LensApp.init();

View File

@ -1,6 +1,5 @@
// Navigation helpers // Navigation helpers
import { ipcRenderer } from "electron";
import { compile } from "path-to-regexp" import { compile } from "path-to-regexp"
import { createBrowserHistory, createMemoryHistory, Location, LocationDescriptor } from "history"; import { createBrowserHistory, createMemoryHistory, Location, LocationDescriptor } from "history";
import { createObservableHistory } from "mobx-observable-history"; import { createObservableHistory } from "mobx-observable-history";
@ -8,13 +7,6 @@ import { createObservableHistory } from "mobx-observable-history";
export const history = typeof window !== "undefined" ? createBrowserHistory() : createMemoryHistory(); export const history = typeof window !== "undefined" ? createBrowserHistory() : createMemoryHistory();
export const navigation = createObservableHistory(history); export const navigation = createObservableHistory(history);
if (ipcRenderer) {
// subscribe for navigation via menu.ts
ipcRenderer.on("menu:navigate", (event, path: string) => {
navigate(path);
});
}
export function navigate(location: LocationDescriptor) { export function navigate(location: LocationDescriptor) {
navigation.location = location as Location; navigation.location = location as Location;
} }

View File

@ -1,5 +1,5 @@
import { computed, observable, reaction } from "mobx"; import { computed, observable, reaction } from "mobx";
import { autobind } from "./utils"; import { autobind } from "./utils/autobind";
import { userStore } from "../common/user-store"; import { userStore } from "../common/user-store";
import logger from "../main/logger"; import logger from "../main/logger";

View File

@ -1,11 +1,10 @@
import { htmlTemplate, isDevelopment, isProduction, outDir, appName, rendererDir, sassCommonVars } from "./src/common/vars"; import { appName, htmlTemplate, isDevelopment, isProduction, outDir, rendererDir, sassCommonVars } from "./src/common/vars";
import path from "path"; import path from "path";
import webpack from "webpack"; import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin"; import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin";
import TerserPlugin from "terser-webpack-plugin"; import TerserPlugin from "terser-webpack-plugin";
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin" import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"
import CircularDependencyPlugin from "circular-dependency-plugin"
export default function (): webpack.Configuration { export default function (): webpack.Configuration {
return { return {
@ -15,7 +14,7 @@ export default function (): webpack.Configuration {
mode: isProduction ? "production" : "development", mode: isProduction ? "production" : "development",
cache: isDevelopment, cache: isDevelopment,
entry: { entry: {
[appName]: path.resolve(rendererDir, "index.tsx"), [appName]: path.resolve(rendererDir, "bootstrap.tsx"),
}, },
output: { output: {
publicPath: "/", publicPath: "/",