diff --git a/Makefile b/Makefile index 8123e75e07..e492aa012c 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,6 @@ test-app: yarn test build: install-deps download-bins - yarn install ifeq "$(DETECTED_OS)" "Windows" yarn dist:win else diff --git a/build/icon.ico b/build/icon.ico index a203847c7c..af1ec205bd 100644 Binary files a/build/icon.ico and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png index 762a3ed50f..2c953f6efd 100644 Binary files a/build/icon.png and b/build/icon.png differ diff --git a/build/icons/512x512.png b/build/icons/512x512.png index 762a3ed50f..2c953f6efd 100644 Binary files a/build/icons/512x512.png and b/build/icons/512x512.png differ diff --git a/package.json b/package.json index 920d90afb5..fde556adcc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "3.6.0-rc.1", + "version": "3.6.0-rc.2", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", @@ -196,7 +196,6 @@ "jsonpath": "^1.0.2", "lodash": "^4.17.15", "mac-ca": "^1.0.4", - "make-synchronous": "^0.1.1", "marked": "^1.1.0", "md5-file": "^5.0.0", "mobx": "^5.15.5", @@ -250,6 +249,7 @@ "@types/material-ui": "^0.21.7", "@types/md5-file": "^4.0.2", "@types/mini-css-extract-plugin": "^0.9.1", + "@types/progress-bar-webpack-plugin": "^2.1.0", "@types/react": "^16.9.35", "@types/react-router-dom": "^5.1.5", "@types/react-select": "^3.0.13", @@ -305,6 +305,7 @@ "nodemon": "^2.0.4", "patch-package": "^6.2.2", "postinstall-postinstall": "^2.1.0", + "progress-bar-webpack-plugin": "^2.1.0", "raw-loader": "^4.0.1", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index e8f681cd7f..b8f9f5f84b 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -3,21 +3,14 @@ import { ClusterId, clusterStore } from "./cluster-store"; import { tracker } from "./tracker"; export const clusterIpc = { - initView: createIpcChannel({ - channel: "cluster:init", - handle: async (clusterId: ClusterId, frameId: number) => { - const cluster = clusterStore.getById(clusterId); - if (cluster) { - cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates - return cluster.pushState(); - } - }, - }), - activate: createIpcChannel({ channel: "cluster:activate", - handle: (clusterId: ClusterId) => { - return clusterStore.getById(clusterId)?.activate(); + handle: (clusterId: ClusterId, frameId?: number) => { + const cluster = clusterStore.getById(clusterId); + if (cluster) { + if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates + return cluster.activate(true); + } }, }), diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index a30c4d7e94..b870858927 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -209,11 +209,17 @@ export class ClusterStore extends BaseStore { export const clusterStore = ClusterStore.getInstance(); -export function getHostedClusterId(): ClusterId { - const clusterHost = location.hostname.match(/^(.*?)\.localhost/); - if (clusterHost) { - return clusterHost[1] - } +export function getClusterIdFromHost(hostname: string): ClusterId { + const subDomains = hostname.split(":")[0].split("."); + return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345" +} + +export function getClusterFrameUrl(clusterId: ClusterId) { + return `//${clusterId}.${location.host}`; +} + +export function getHostedClusterId() { + return getClusterIdFromHost(location.hostname); } export function getHostedCluster(): Cluster { diff --git a/src/common/cluster-store_test.ts b/src/common/cluster-store_test.ts index cee18078bc..d7d6df4d2a 100644 --- a/src/common/cluster-store_test.ts +++ b/src/common/cluster-store_test.ts @@ -31,9 +31,10 @@ describe("empty config", () => { it("adds new cluster to store", async () => { const cluster = new Cluster({ id: "foo", + contextName: "minikube", preferences: { terminalCWD: "/tmp", - icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", + icon: "data:;base64,iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", clusterName: "minikube" }, kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", "fancy foo config"), @@ -54,6 +55,7 @@ describe("empty config", () => { it("check if store can contain multiple clusters", () => { const prodCluster = new Cluster({ id: "prod", + contextName: "prod", preferences: { clusterName: "prod" }, @@ -62,6 +64,7 @@ describe("empty config", () => { }); const devCluster = new Cluster({ id: "dev", + contextName: "dev", preferences: { clusterName: "dev" }, @@ -142,11 +145,13 @@ describe("config with existing clusters", () => { { id: 'cluster1', kubeConfig: 'foo', + contextName: 'foo', preferences: { terminalCWD: '/foo' } }, { id: 'cluster2', kubeConfig: 'foo2', + contextName: 'foo2', preferences: { terminalCWD: '/foo2' } } ] @@ -285,7 +290,7 @@ describe("pre 2.6.0 config with a cluster icon", () => { const storedClusterData = clusterStore.clustersList[0]; expect(storedClusterData.hasOwnProperty('icon')).toBe(false); expect(storedClusterData.preferences.hasOwnProperty('icon')).toBe(true); - expect(storedClusterData.preferences.icon.startsWith("data:image/jpeg;base64,")).toBe(true); + expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true); }) }) @@ -339,6 +344,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => { { id: 'cluster1', kubeConfig: 'kubeconfig content', + contextName: 'cluster', preferences: { icon: "store://icon_path", } @@ -364,6 +370,6 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => { it("migrates to modern format with icon not in file", async () => { const { icon } = clusterStore.clustersList[0].preferences; - expect(icon.startsWith("data:image/jpeg;base64, ")).toBe(true); + expect(icon.startsWith("data:;base64,")).toBe(true); }) -}) \ No newline at end of file +}) diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 43afb0d5c6..752a53f41b 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -51,6 +51,10 @@ export class WorkspaceStore extends BaseStore { return this.workspaces.get(id); } + getByName(name: string): Workspace { + return this.workspacesList.find(workspace => workspace.name === name); + } + @action setActive(id = WorkspaceStore.defaultId, { redirectToLanding = true, resetActiveCluster = true } = {}) { if (id === this.currentWorkspaceId) return; @@ -68,13 +72,16 @@ export class WorkspaceStore extends BaseStore { @action saveWorkspace(workspace: Workspace) { - const id = workspace.id; + const { id, name } = workspace; const existingWorkspace = this.getById(id); + if (!name.trim() || this.getByName(name.trim())) { + return; + } if (existingWorkspace) { Object.assign(existingWorkspace, workspace); - } else { - this.workspaces.set(id, workspace); } + this.workspaces.set(id, workspace); + return workspace; } @action diff --git a/src/common/workspace-store_test.ts b/src/common/workspace-store_test.ts index 55e9672663..232f0b013a 100644 --- a/src/common/workspace-store_test.ts +++ b/src/common/workspace-store_test.ts @@ -92,6 +92,50 @@ describe("workspace store tests", () => { expect(ws.workspaces.size).toBe(2); }) + + it("cannot create workspace with existent name", () => { + const ws = WorkspaceStore.getInstance(); + + ws.saveWorkspace({ + id: "someid", + name: "default", + }); + + expect(ws.workspacesList.length).toBe(1); // default workspace only + }) + + it("cannot create workspace with empty name", () => { + const ws = WorkspaceStore.getInstance(); + + ws.saveWorkspace({ + id: "random", + name: "", + }); + + expect(ws.workspacesList.length).toBe(1); // default workspace only + }) + + it("cannot create workspace with ' ' name", () => { + const ws = WorkspaceStore.getInstance(); + + ws.saveWorkspace({ + id: "random", + name: " ", + }); + + expect(ws.workspacesList.length).toBe(1); // default workspace only + }) + + it("trim workspace name", () => { + const ws = WorkspaceStore.getInstance(); + + ws.saveWorkspace({ + id: "random", + name: "default ", + }); + + expect(ws.workspacesList.length).toBe(1); // default workspace only + }) }) describe("for a non-empty config", () => { diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index af1e0d5657..ff5eaae32d 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,7 +1,7 @@ import "../common/cluster-ipc"; import type http from "http" import { autorun } from "mobx"; -import { ClusterId, clusterStore } from "../common/cluster-store" +import { clusterStore, getClusterIdFromHost } from "../common/cluster-store" import { Cluster } from "./cluster" import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; @@ -38,26 +38,20 @@ export class ClusterManager { }) } - protected getCluster(id: ClusterId) { - return clusterStore.getById(id); - } - getClusterForRequest(req: http.IncomingMessage): Cluster { let cluster: Cluster = null // lens-server is connecting to 127.0.0.1:/ if (req.headers.host.startsWith("127.0.0.1")) { const clusterId = req.url.split("/")[1] - if (clusterId) { - cluster = this.getCluster(clusterId) - if (cluster) { - // we need to swap path prefix so that request is proxied to kube api - req.url = req.url.replace(`/${clusterId}`, apiKubePrefix) - } + const cluster = clusterStore.getById(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) + const clusterId = getClusterIdFromHost(req.headers.host); + cluster = clusterStore.getById(clusterId) } return cluster; diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 8f17216c18..80bd208a40 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -2,7 +2,7 @@ import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/clus import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; import type { FeatureStatusMap } from "./feature" -import { action, computed, intercept, observable, reaction, toJS, when } from "mobx"; +import { action, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; import { broadcastIpc } from "../common/ipc"; import { ContextHandler } from "./context-handler" @@ -77,13 +77,15 @@ export class Cluster implements ClusterModel { constructor(model: ClusterModel) { this.updateModel(model); + const kubeconfig = this.getKubeconfig() + if (kubeconfig.getContextObject(this.contextName)) { + this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server + } } @action updateModel(model: ClusterModel) { Object.assign(this, model); - this.apiUrl = this.getKubeconfig().getCurrentCluster()?.server; - this.contextName = this.contextName || this.preferences.clusterName; } @action @@ -124,13 +126,13 @@ export class Cluster implements ClusterModel { this.eventDisposers.length = 0; } - async activate() { + async activate(init = false) { logger.info(`[CLUSTER]: activate`, this.getMeta()); await this.whenInitialized; if (!this.eventDisposers.length) { this.bindEvents(); } - if (this.disconnected || !this.accessible) { + if (this.disconnected || (!init && !this.accessible)) { await this.reconnect(); } await this.refresh(); @@ -409,6 +411,7 @@ export class Cluster implements ClusterModel { id: this.id, name: this.contextName, initialized: this.initialized, + ready: this.ready, online: this.online, accessible: this.accessible, disconnected: this.disconnected, diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 6f7ab6478a..be5a95e47a 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,9 +1,9 @@ +import type { ClusterId } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store"; import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron" import windowStateKeeper from "electron-window-state" import { observable } from "mobx"; import { initMenu } from "./menu"; -import type { ClusterId } from "../common/cluster-store"; export class WindowManager { protected mainView: BrowserWindow; @@ -42,7 +42,7 @@ export class WindowManager { }); // track visible cluster from ui - ipcMain.on("cluster-view:change", (event, clusterId: ClusterId) => { + ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => { this.activeClusterId = clusterId; }); diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts index 18ce07a638..412c77ab96 100644 --- a/src/migrations/cluster-store/3.6.0-beta.1.ts +++ b/src/migrations/cluster-store/3.6.0-beta.1.ts @@ -7,11 +7,6 @@ import { migration } from "../migration-wrapper"; import fse from "fs-extra" import { ClusterModel, ClusterStore } from "../../common/cluster-store"; import { loadConfig } from "../../common/kube-helpers"; -import makeSynchronous from "make-synchronous" - -const AsyncFunction = Object.getPrototypeOf(async function () { return }).constructor; -const getFileTypeFnString = `return require("file-type").fromBuffer(fileData)`; -const getFileType = new AsyncFunction("fileData", getFileTypeFnString); export default migration({ version: "3.6.0-beta.1", @@ -48,13 +43,8 @@ export default migration({ printLog(`migrating ${cluster.preferences.icon} for ${cluster.preferences.clusterName}`) const iconPath = cluster.preferences.icon.replace("store://", "") const fileData = fse.readFileSync(path.join(userDataPath, iconPath)); - const { mime = "" } = makeSynchronous(getFileType)(fileData); - if (!mime) { - printLog(`mime type not detected for ${cluster.preferences.clusterName}'s icon: ${iconPath}`) - } - - cluster.preferences.icon = `data:${mime};base64, ${fileData.toString('base64')}`; + cluster.preferences.icon = `data:;base64,${fileData.toString('base64')}`; } else { delete cluster.preferences?.icon; } diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index ee4dd6dedf..29670ba99c 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -5,7 +5,7 @@ import { isMac } from "../common/vars"; import { userStore } from "../common/user-store"; import { workspaceStore } from "../common/workspace-store"; import { extensionStore } from "../extensions/extension-store"; -import { clusterStore, getHostedClusterId } from "../common/cluster-store"; +import { clusterStore } from "../common/cluster-store"; import { i18nStore } from "./i18n"; import { themeStore } from "./theme.store"; import { App } from "./components/app"; @@ -39,4 +39,4 @@ export async function bootstrap(App: AppComponent) { } // run -bootstrap(getHostedClusterId() ? App : LensApp); +bootstrap(process.isMainFrame ? LensApp : App); diff --git a/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx index f998035b44..acb23da08a 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { observable } from "mobx"; -import { observer } from "mobx-react"; +import { observable, autorun } from "mobx"; +import { observer, disposeOnUnmount } from "mobx-react"; import { Cluster } from "../../../../main/cluster"; import { Input } from "../../input"; import { SubTitle } from "../../layout/sub-title"; @@ -11,7 +11,15 @@ interface Props { @observer export class ClusterHomeDirSetting extends React.Component { - @observable directory = this.props.cluster.preferences.terminalCWD || ""; + @observable directory = ""; + + componentDidMount() { + disposeOnUnmount(this, + autorun(() => { + this.directory = this.props.cluster.preferences.terminalCWD || ""; + }) + ); + } save = () => { this.props.cluster.preferences.terminalCWD = this.directory; diff --git a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx index 04b7ef1fc6..fb35778f6c 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx @@ -28,7 +28,7 @@ export class ClusterIconSetting extends React.Component { try { if (file) { const buf = Buffer.from(await file.arrayBuffer()); - cluster.preferences.icon = `data:${file.type};base64, ${buf.toString('base64')}`; + cluster.preferences.icon = `data:${file.type};base64,${buf.toString('base64')}`; } else { // this has to be done as a seperate branch (and not always) because `cluster` // is observable and triggers an update loop. @@ -73,4 +73,4 @@ export class ClusterIconSetting extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx index 8e2f8a2afa..a11f8aed6e 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Cluster } from "../../../../main/cluster"; import { Input } from "../../input"; -import { observable } from "mobx"; -import { observer } from "mobx-react"; +import { observable, autorun } from "mobx"; +import { observer, disposeOnUnmount } from "mobx-react"; import { SubTitle } from "../../layout/sub-title"; import { isRequired } from "../../input/input.validators"; @@ -12,7 +12,15 @@ interface Props { @observer export class ClusterNameSetting extends React.Component { - @observable name = this.props.cluster.preferences.clusterName || ""; + @observable name = ""; + + componentDidMount() { + disposeOnUnmount(this, + autorun(() => { + this.name = this.props.cluster.preferences.clusterName; + }) + ); + } save = () => { this.props.cluster.preferences.clusterName = this.name; diff --git a/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx index 729090629d..a7f42a7c85 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx @@ -1,11 +1,11 @@ import React from "react"; -import { observer } from "mobx-react"; +import { observer, disposeOnUnmount } from "mobx-react"; import { prometheusProviders } from "../../../../common/prometheus-providers"; import { Cluster } from "../../../../main/cluster"; import { SubTitle } from "../../layout/sub-title"; import { Select, SelectOption } from "../../select"; import { Input } from "../../input"; -import { observable, computed } from "mobx"; +import { observable, computed, autorun } from "mobx"; const options: SelectOption[] = [ { value: "", label: "Auto detect" }, @@ -27,14 +27,22 @@ export class ClusterPrometheusSetting extends React.Component { } componentDidMount() { - const { prometheus, prometheusProvider } = this.props.cluster.preferences; - if (prometheus) { - const prefix = prometheus.prefix || ""; - this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`; - } - if (prometheusProvider) { - this.provider = prometheusProvider.type; - } + disposeOnUnmount(this, + autorun(() => { + const { prometheus, prometheusProvider } = this.props.cluster.preferences; + if (prometheus) { + const prefix = prometheus.prefix || ""; + this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`; + } else { + this.path = ""; + } + if (prometheusProvider) { + this.provider = prometheusProvider.type; + } else { + this.provider = ""; + } + }) + ); } parsePrometheusPath = () => { diff --git a/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx index 1b94992e5b..f7b4327c52 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { observable } from "mobx"; -import { observer } from "mobx-react"; +import { observable, autorun } from "mobx"; +import { observer, disposeOnUnmount } from "mobx-react"; import { Cluster } from "../../../../main/cluster"; import { Input } from "../../input"; import { isUrl } from "../../input/input.validators"; @@ -12,7 +12,15 @@ interface Props { @observer export class ClusterProxySetting extends React.Component { - @observable proxy = this.props.cluster.preferences.httpsProxy || ""; + @observable proxy = ""; + + componentDidMount() { + disposeOnUnmount(this, + autorun(() => { + this.proxy = this.props.cluster.preferences.httpsProxy || ""; + }) + ); + } save = () => { this.props.cluster.preferences.httpsProxy = this.proxy; diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx index 96007a95fa..a2e7dbf74f 100644 --- a/src/renderer/components/+workspaces/workspaces.tsx +++ b/src/renderer/components/+workspaces/workspaces.tsx @@ -12,6 +12,7 @@ import { Icon } from "../icon"; import { Input } from "../input"; import { cssNames, prevDefault } from "../../utils"; import { Button } from "../button"; +import { isRequired, Validator } from "../input/input.validators"; @observer export class Workspaces extends React.Component { @@ -41,9 +42,9 @@ export class Workspaces extends React.Component { saveWorkspace = (id: WorkspaceId) => { const draft = toJS(this.editingWorkspaces.get(id)); - if (draft) { + const workspace = workspaceStore.saveWorkspace(draft); + if (workspace) { this.clearEditing(id); - workspaceStore.saveWorkspace(draft); } } @@ -90,6 +91,15 @@ export class Workspaces extends React.Component { }) } + onInputKeypress = (evt: React.KeyboardEvent, workspaceId: WorkspaceId) => { + if (evt.key == 'Enter') { + // Trigget input validation + evt.currentTarget.blur(); + evt.currentTarget.focus(); + this.saveWorkspace(workspaceId); + } + } + render() { return ( @@ -102,11 +112,15 @@ export class Workspaces extends React.Component { const isDefault = workspaceStore.isDefault(workspaceId); const isEditing = this.editingWorkspaces.has(workspaceId); const editingWorkspace = this.editingWorkspaces.get(workspaceId); - const className = cssNames("workspace flex gaps align-center", { + const className = cssNames("workspace flex gaps", { active: isActive, editing: isEditing, default: isDefault, }); + const existenceValidator: Validator = { + message: () => `Workspace '${name}' already exists`, + validate: value => !workspaceStore.getByName(value.trim()) + } return (
{!isEditing && ( @@ -139,23 +153,27 @@ export class Workspaces extends React.Component { placeholder={_i18n._(t`Name`)} value={editingWorkspace.name} onChange={v => editingWorkspace.name = v} + onKeyPress={(e) => this.onInputKeypress(e, workspaceId)} + validators={[isRequired, existenceValidator]} + autoFocus /> editingWorkspace.description = v} - /> - Cancel} - onClick={() => this.clearEditing(workspaceId)} + onKeyPress={(e) => this.onInputKeypress(e, workspaceId)} /> Save} onClick={() => this.saveWorkspace(workspaceId)} /> + Cancel} + onClick={() => this.clearEditing(workspaceId)} + /> )}
diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index a878fd47b7..81e1cc0db4 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -44,8 +44,8 @@ export class App extends React.Component { const clusterId = getHostedClusterId(); logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`) await Terminal.preloadFonts() - await clusterIpc.initView.invokeFromRenderer(clusterId, frameId); - await getHostedCluster().whenInitialized; + await clusterIpc.activate.invokeFromRenderer(clusterId, frameId); + await getHostedCluster().whenReady; // cluster.refresh() is done at this point } get startURL() { diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 10ee59f467..a726e2e5cf 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -1,7 +1,7 @@ import "./cluster-manager.scss" import React from "react"; import { Redirect, Route, Switch } from "react-router"; -import { reaction } from "mobx"; +import { comparer, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { ClustersMenu } from "./clusters-menu"; import { BottomBar } from "./bottom-bar"; @@ -25,11 +25,14 @@ export class ClusterManager extends React.Component { fireImmediately: true }), reaction(() => [ + getMatchedClusterId(), // refresh when active cluster-view changed hasLoadedView(getMatchedClusterId()), // refresh when cluster's webview loaded getMatchedCluster()?.available, // refresh on disconnect active-cluster + getMatchedCluster()?.ready, // refresh when cluster ready-state change ], refreshViews, { - fireImmediately: true - }) + fireImmediately: true, + equals: comparer.shallow, + }), ]) } diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 1f5d52f814..b0fae210dd 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -38,7 +38,7 @@ export class ClusterStatus extends React.Component { error: res.error, }); }) - if (!this.cluster.initialized || this.cluster.disconnected) { + if (this.cluster.disconnected) { await this.refreshCluster(); } } @@ -63,7 +63,7 @@ export class ClusterStatus extends React.Component { if (!hasErrors || this.isReconnecting) { return ( <> - +
             

{this.isReconnecting ? "Reconnecting..." : "Connecting..."}

{authOutput.map(({ data, error }, index) => { @@ -75,7 +75,7 @@ export class ClusterStatus extends React.Component { } return ( <> - +

{cluster.preferences.clusterName}

diff --git a/src/renderer/components/cluster-manager/cluster-view.route.ts b/src/renderer/components/cluster-manager/cluster-view.route.ts index d2e39cbb0c..e1fb0d1f54 100644 --- a/src/renderer/components/cluster-manager/cluster-view.route.ts +++ b/src/renderer/components/cluster-manager/cluster-view.route.ts @@ -2,7 +2,7 @@ import { reaction } from "mobx"; import { ipcRenderer } from "electron"; import { matchPath, RouteProps } from "react-router"; import { buildURL, navigation } from "../../navigation"; -import { clusterStore, getHostedClusterId } from "../../../common/cluster-store"; +import { clusterStore } from "../../../common/cluster-store"; import { clusterSettingsRoute } from "../+cluster-settings/cluster-settings.route"; export interface IClusterViewRouteParams { @@ -34,11 +34,10 @@ export function getMatchedCluster() { } if (ipcRenderer) { - // Refresh global menu depending on active route's type (common/cluster view) - const isMainView = !getHostedClusterId(); - if (isMainView) { + if (process.isMainFrame) { + // Keep track of active cluster-id for handling IPC/menus/etc. reaction(() => getMatchedClusterId(), clusterId => { - ipcRenderer.send("cluster-view:change", clusterId); + ipcRenderer.send("cluster-view:current-id", clusterId); }, { fireImmediately: true }) diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index 9ae08c5e4e..370ed665c3 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -1,5 +1,5 @@ import { observable, when } from "mobx"; -import { ClusterId, clusterStore } from "../../../common/cluster-store"; +import { ClusterId, clusterStore, getClusterFrameUrl } from "../../../common/cluster-store"; import { getMatchedCluster } from "./cluster-view.route" import logger from "../../../main/logger"; @@ -21,29 +21,38 @@ export async function initView(clusterId: ClusterId) { } logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`) const cluster = clusterStore.getById(clusterId); - await cluster.whenReady; const parentElem = document.getElementById("lens-views"); const iframe = document.createElement("iframe"); iframe.name = cluster.contextName; - iframe.setAttribute("src", `//${clusterId}.${location.host}`) - iframe.addEventListener("load", async () => { + iframe.setAttribute("src", getClusterFrameUrl(clusterId)) + iframe.addEventListener("load", () => { logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`) lensViews.get(clusterId).isLoaded = true; - }) + }, { once: true }); lensViews.set(clusterId, { clusterId, view: iframe }); parentElem.appendChild(iframe); - // auto-clean when cluster removed + await autoCleanOnRemove(clusterId, iframe); +} + +export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) { await when(() => !clusterStore.getById(clusterId)); logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`) - parentElem.removeChild(iframe) lensViews.delete(clusterId) + // Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed. + // In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx) + // Issue: https://github.com/lensapp/lens/issues/811 + iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`; + iframe.removeAttribute("src") + iframe.removeAttribute("name") } export function refreshViews() { const cluster = getMatchedCluster(); lensViews.forEach(({ clusterId, view, isLoaded }) => { - const isVisible = cluster && cluster.available && cluster.id === clusterId; - view.style.display = isLoaded && isVisible ? "flex" : "none" + const isCurrent = clusterId === cluster?.id; + const isReady = cluster?.available && cluster?.ready; + const isVisible = isCurrent && isLoaded && isReady; + view.style.display = isVisible ? "flex" : "none" }) } diff --git a/src/renderer/components/dialog/dialog.tsx b/src/renderer/components/dialog/dialog.tsx index 071c6aa811..5a4ce48f4e 100644 --- a/src/renderer/components/dialog/dialog.tsx +++ b/src/renderer/components/dialog/dialog.tsx @@ -92,7 +92,8 @@ export class Dialog extends React.PureComponent { this.props.onOpen(); if (!this.props.pinned) { if (this.elem) this.elem.addEventListener('click', this.onClickOutside); - window.addEventListener('keydown', this.onEscapeKey); + // Using document.body target to handle keydown event before Drawer does + document.body.addEventListener('keydown', this.onEscapeKey); } } @@ -100,7 +101,7 @@ export class Dialog extends React.PureComponent { this.props.onClose(); if (!this.props.pinned) { if (this.elem) this.elem.removeEventListener('click', this.onClickOutside); - window.removeEventListener('keydown', this.onEscapeKey); + document.body.removeEventListener('keydown', this.onEscapeKey); } } diff --git a/src/renderer/components/drawer/drawer.tsx b/src/renderer/components/drawer/drawer.tsx index c4c1a3894a..d76e99f5c0 100644 --- a/src/renderer/components/drawer/drawer.tsx +++ b/src/renderer/components/drawer/drawer.tsx @@ -40,6 +40,7 @@ export class Drawer extends React.Component { }); componentDidMount() { + // Using window target for events to make sure they will be catched after other places (e.g. Dialog) window.addEventListener("mousedown", this.onMouseDown) window.addEventListener("click", this.onClickOutside) window.addEventListener("keydown", this.onEscapeKey) diff --git a/src/renderer/components/icon/logo-full.svg b/src/renderer/components/icon/logo-full.svg index b3b553f865..9529b61e9a 100644 --- a/src/renderer/components/icon/logo-full.svg +++ b/src/renderer/components/icon/logo-full.svg @@ -1 +1,17 @@ - + + + + + + + + + + + + + + diff --git a/src/renderer/components/layout/sidebar.scss b/src/renderer/components/layout/sidebar.scss index f863d374cd..7b1406fb7b 100644 --- a/src/renderer/components/layout/sidebar.scss +++ b/src/renderer/components/layout/sidebar.scss @@ -40,18 +40,19 @@ div.logo-text { position: absolute; left: 42px; - top: 11.5px; + top: 11px; } .logo-icon { width: 28px; height: 28px; margin-left: 2px; - margin-top: 3px; + margin-top: 2px; margin-right: 10px; svg { --size: 28px; + padding: 2px; } } diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index d0553e542a..64f4c386a1 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,12 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 3.6.0-rc.1 (current version) +## 3.6.0-rc.2 (current version) +- Refresh input values on cluster change +- Update logo +- Fix margins in cluster menu + +## 3.6.0-rc.1 - Allow user to configure directory where Kubectl binaries are downloaded - Allow user to configure path to Kubectl binary, instead of using bundled Kubectl - Log application logs also to log file diff --git a/webpack.main.ts b/webpack.main.ts index 06c52f47dd..c9536da63d 100755 --- a/webpack.main.ts +++ b/webpack.main.ts @@ -3,6 +3,7 @@ import webpack from "webpack"; import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin" import { isDevelopment, isProduction, mainDir, buildDir } from "./src/common/vars"; import nodeExternals from "webpack-node-externals"; +import ProgressBarPlugin from "progress-bar-webpack-plugin"; export default function (): webpack.Configuration { console.info('WEBPACK:main', require("./src/common/vars")) @@ -48,6 +49,7 @@ export default function (): webpack.Configuration { ] }, plugins: [ + new ProgressBarPlugin(), new ForkTsCheckerPlugin(), ] } diff --git a/webpack.renderer.ts b/webpack.renderer.ts index 1d60e2afc4..ca8e07438a 100755 --- a/webpack.renderer.ts +++ b/webpack.renderer.ts @@ -5,6 +5,7 @@ import HtmlWebpackPlugin from "html-webpack-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; import TerserPlugin from "terser-webpack-plugin"; import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin" +import ProgressBarPlugin from "progress-bar-webpack-plugin"; export default [ webpackLensRenderer, @@ -154,6 +155,7 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura }, plugins: [ + new ProgressBarPlugin(), new ForkTsCheckerPlugin(), // todo: fix remain warnings about circular dependencies diff --git a/yarn.lock b/yarn.lock index 8e84da93d1..7decd84ec7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2011,6 +2011,21 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.1.tgz#b6e98083f13faa1e5231bfa3bdb1b0feff536b6d" integrity sha512-boy4xPNEtiw6N3abRhBi/e7hNvy3Tt8E9ZRAQrwAGzoCGZS/1wjo9KY7JHhnfnEsG5wSjDbymCozUM9a3ea7OQ== +"@types/progress-bar-webpack-plugin@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/progress-bar-webpack-plugin/-/progress-bar-webpack-plugin-2.1.0.tgz#f95b7e1199b5f5e8156321d2411f6fb06c833559" + integrity sha512-HCyeEuuFzsXvIkbchGKJUhXRBGio7BlvVn0ULuBRE50UofuFy2Q8HvR+Q8C2w+QlBUoM+AjerGJU8DhyqYVw7g== + dependencies: + "@types/progress" "*" + "@types/webpack" "*" + +"@types/progress@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/progress/-/progress-2.0.3.tgz#7ccbd9c6d4d601319126c469e73b5bb90dfc8ccc" + integrity sha512-bPOsfCZ4tsTlKiBjBhKnM8jpY5nmIll166IPD58D92hR7G7kZDfx5iB9wGF4NfZrdKolebjeAr3GouYkSGoJ/A== + dependencies: + "@types/node" "*" + "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" @@ -7801,14 +7816,6 @@ make-plural@^6.2.1: resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-6.2.1.tgz#2790af1d05fb2fc35a111ce759ffdb0aca1339a3" integrity sha512-AmkruwJ9EjvyTv6AM8MBMK3TAeOJvhgTv5YQXzF0EP2qawhpvMjDpHvsdOIIT0Vn+BB0+IogmYZ1z+Ulm/m0Fg== -make-synchronous@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/make-synchronous/-/make-synchronous-0.1.1.tgz#0169f6ec769c3cf8948d66790da262740c1209e7" - integrity sha512-Y4SxxqhaoyMDokJQ0AZz0E+bLhRkOSR7Z/IQoTKPdS6HYi3aobal2kMHoHHoqBadPWjf07P4K1FQLXOx3wf9Yw== - dependencies: - subsume "^3.0.0" - type-fest "^0.16.0" - makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -9351,6 +9358,14 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +progress-bar-webpack-plugin@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/progress-bar-webpack-plugin/-/progress-bar-webpack-plugin-2.1.0.tgz#f7f8c8c461f40b87a8ff168443f494289b07ee65" + integrity sha512-UtlZbnxpYk1wufEWfhIjRn2U52zlY38uvnzFhs8rRxJxC1hSqw88JNR2Mbpqq9Kix8L1nGb3uQ+/1BiUWbigAg== + dependencies: + chalk "^3.0.0" + progress "^2.0.3" + progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -10916,14 +10931,6 @@ style-loader@^1.2.1: loader-utils "^2.0.0" schema-utils "^2.6.6" -subsume@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/subsume/-/subsume-3.0.0.tgz#22c92730f441ad72ee9af4bdad42dc4ff830cfaf" - integrity sha512-6n/UfV8UWKwJNO8OAOiKntwEMihuBeeoJfzpL542C+OuvT4iWG9SwjrXkOmsxjb4SteHUsos9SvrdqZ9+ICwTQ== - dependencies: - escape-string-regexp "^2.0.0" - unique-string "^2.0.0" - sumchecker@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" @@ -11432,11 +11439,6 @@ type-fest@^0.13.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== -type-fest@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" - integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== - type-fest@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"