mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
part 1
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
1f5acdb9cd
commit
e5138e7c5d
@ -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 {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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));
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,26 +119,17 @@ 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
|
if (proxyTarget) {
|
||||||
res.end()
|
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
||||||
return
|
res.setHeader("Access-Control-Allow-Origin", this.origin);
|
||||||
}
|
return proxy.web(req, res, proxyTarget);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
|
|||||||
@ -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())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
minHeight: 760,
|
||||||
|
titleBarStyle: "hidden",
|
||||||
|
backgroundColor: "#1e2124",
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.windowState.manage(this.mainView);
|
||||||
|
|
||||||
// Manage reactive state
|
// open external links in default browser (target=_blank, window.open)
|
||||||
this.disposers.push(
|
this.mainView.webContents.on("new-window", (event, url) => {
|
||||||
// auto-show/hide "no-clusters" window when necessary
|
event.preventDefault();
|
||||||
reaction(() => clusterStore.hasClusters(), hasClusters => {
|
shell.openExternal(url);
|
||||||
this.handleNoClustersView({ activate: !hasClusters });
|
});
|
||||||
}, {
|
|
||||||
fireImmediately: true
|
|
||||||
}),
|
|
||||||
|
|
||||||
// auto-show active cluster window
|
// load & show app
|
||||||
reaction(() => clusterStore.activeClusterId, this.activateView, {
|
this.showMain();
|
||||||
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()
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
async showMain() {
|
||||||
this.activeView = this.noClustersWindow;
|
await this.showSplash();
|
||||||
this.noClustersWindow.show();
|
await this.mainView.loadURL(`http://localhost:${this.proxyPort}`)
|
||||||
this.hideSplash();
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
38
src/renderer/bootstrap.tsx
Normal file
38
src/renderer/bootstrap.tsx
Normal 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);
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./cluster.routes"
|
export * from "./cluster.route"
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./user-management"
|
export * from "./user-management"
|
||||||
export * from "./user-management.routes"
|
export * from "./user-management.route"
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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,94 +28,54 @@ 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 (isAllowedResource(["events", "nodes", "pods"])) {
|
||||||
if (!this.cluster.accessible) {
|
return clusterURL();
|
||||||
return clusterStatusURL();
|
|
||||||
}
|
|
||||||
if (isAllowedResource(["events", "nodes", "pods"])) {
|
|
||||||
return clusterURL();
|
|
||||||
}
|
|
||||||
return workloadsURL();
|
|
||||||
}
|
}
|
||||||
return landingURL();
|
return workloadsURL();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.isReady) {
|
|
||||||
return <CubeSpinner className="box center"/>
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<I18nProvider i18n={_i18n}>
|
||||||
<Switch>
|
<Router history={history}>
|
||||||
<Route component={LandingPage} {...landingRoute}/>
|
<ErrorBoundary>
|
||||||
<Route component={Preferences} {...preferencesRoute}/>
|
<Switch>
|
||||||
<Route component={Workspaces} {...workspacesRoute}/>
|
<Route component={Cluster} {...clusterRoute}/>
|
||||||
<Route component={AddCluster} {...addClusterRoute}/>
|
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
|
||||||
<Route component={Cluster} {...clusterRoute}/>
|
<Route component={Nodes} {...nodesRoute}/>
|
||||||
<Route component={ClusterStatus} {...clusterStatusRoute}/>
|
<Route component={Workloads} {...workloadsRoute}/>
|
||||||
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
|
<Route component={Config} {...configRoute}/>
|
||||||
<Route component={Nodes} {...nodesRoute}/>
|
<Route component={Network} {...networkRoute}/>
|
||||||
<Route component={Workloads} {...workloadsRoute}/>
|
<Route component={Storage} {...storageRoute}/>
|
||||||
<Route component={Config} {...configRoute}/>
|
<Route component={Namespaces} {...namespacesRoute}/>
|
||||||
<Route component={Network} {...networkRoute}/>
|
<Route component={Events} {...eventRoute}/>
|
||||||
<Route component={Storage} {...storageRoute}/>
|
<Route component={CustomResources} {...crdRoute}/>
|
||||||
<Route component={Namespaces} {...namespacesRoute}/>
|
<Route component={UserManagement} {...usersManagementRoute}/>
|
||||||
<Route component={Events} {...eventRoute}/>
|
<Route component={Apps} {...appsRoute}/>
|
||||||
<Route component={CustomResources} {...crdRoute}/>
|
<Redirect exact from="/" to={this.startURL}/>
|
||||||
<Route component={UserManagement} {...usersManagementRoute}/>
|
<Route component={NotFound}/>
|
||||||
<Route component={Apps} {...appsRoute}/>
|
</Switch>
|
||||||
<Redirect exact from="/" to={this.startURL}/>
|
<KubeObjectDetails/>
|
||||||
<Route component={NotFound}/>
|
<Notifications/>
|
||||||
</Switch>
|
<ConfirmDialog/>
|
||||||
<KubeObjectDetails/>
|
<KubeConfigDialog/>
|
||||||
<Notifications/>
|
<AddRoleBindingDialog/>
|
||||||
<ConfirmDialog/>
|
<PodLogsDialog/>
|
||||||
<KubeConfigDialog/>
|
<DeploymentScaleDialog/>
|
||||||
<AddRoleBindingDialog/>
|
</ErrorBoundary>
|
||||||
<PodLogsDialog/>
|
</Router>
|
||||||
<DeploymentScaleDialog/>
|
</I18nProvider>
|
||||||
</ErrorBoundary>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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 })}>
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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();
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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: "/",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user