1
0
mirror of https://github.com/lensapp/lens.git synced 2024-09-20 05:47:24 +03:00

Merge branch 'master' into extensions-api

# Conflicts:
#	package.json
#	src/common/cluster-store.ts
#	src/renderer/bootstrap.tsx
#	src/renderer/components/cluster-manager/clusters-menu.tsx
#	static/RELEASE_NOTES.md
#	webpack.dll.ts
This commit is contained in:
Roman 2020-09-11 12:29:38 +03:00
commit a3dc928430
34 changed files with 273 additions and 139 deletions

View File

@ -47,7 +47,6 @@ test-app:
yarn test
build: install-deps download-bins
yarn install
ifeq "$(DETECTED_OS)" "Windows"
yarn dist:win
else

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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",

View File

@ -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);
}
},
}),

View File

@ -209,11 +209,17 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
export const clusterStore = ClusterStore.getInstance<ClusterStore>();
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 {

View File

@ -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);
})
})
})

View File

@ -51,6 +51,10 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
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<WorkspaceStoreModel> {
@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

View File

@ -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<WorkspaceStore>();
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<WorkspaceStore>();
ws.saveWorkspace({
id: "random",
name: "",
});
expect(ws.workspacesList.length).toBe(1); // default workspace only
})
it("cannot create workspace with ' ' name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({
id: "random",
name: " ",
});
expect(ws.workspacesList.length).toBe(1); // default workspace only
})
it("trim workspace name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
ws.saveWorkspace({
id: "random",
name: "default ",
});
expect(ws.workspacesList.length).toBe(1); // default workspace only
})
})
describe("for a non-empty config", () => {

View File

@ -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:<port>/<uid>
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;

View File

@ -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,

View File

@ -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;
});

View File

@ -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;
}

View File

@ -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);

View File

@ -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<Props> {
@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;

View File

@ -28,7 +28,7 @@ export class ClusterIconSetting extends React.Component<Props> {
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<Props> {
</>
);
}
}
}

View File

@ -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<Props> {
@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;

View File

@ -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<string>[] = [
{ value: "", label: "Auto detect" },
@ -27,14 +27,22 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
}
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 = () => {

View File

@ -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<Props> {
@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;

View File

@ -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<any>, workspaceId: WorkspaceId) => {
if (evt.key == 'Enter') {
// Trigget input validation
evt.currentTarget.blur();
evt.currentTarget.focus();
this.saveWorkspace(workspaceId);
}
}
render() {
return (
<WizardLayout className="Workspaces" infoPanel={this.renderInfo()}>
@ -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 (
<div key={workspaceId} className={className}>
{!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
/>
<Input
className="description"
placeholder={_i18n._(t`Description`)}
value={editingWorkspace.description}
onChange={v => editingWorkspace.description = v}
/>
<Icon
material="cancel"
tooltip={<Trans>Cancel</Trans>}
onClick={() => this.clearEditing(workspaceId)}
onKeyPress={(e) => this.onInputKeypress(e, workspaceId)}
/>
<Icon
material="save"
tooltip={<Trans>Save</Trans>}
onClick={() => this.saveWorkspace(workspaceId)}
/>
<Icon
material="cancel"
tooltip={<Trans>Cancel</Trans>}
onClick={() => this.clearEditing(workspaceId)}
/>
</Fragment>
)}
</div>

View File

@ -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() {

View File

@ -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,
}),
])
}

View File

@ -38,7 +38,7 @@ export class ClusterStatus extends React.Component<Props> {
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<Props> {
if (!hasErrors || this.isReconnecting) {
return (
<>
<CubeSpinner />
<CubeSpinner/>
<pre className="kube-auth-out">
<p>{this.isReconnecting ? "Reconnecting..." : "Connecting..."}</p>
{authOutput.map(({ data, error }, index) => {
@ -75,7 +75,7 @@ export class ClusterStatus extends React.Component<Props> {
}
return (
<>
<Icon material="cloud_off" className="error" />
<Icon material="cloud_off" className="error"/>
<h2>
{cluster.preferences.clusterName}
</h2>

View File

@ -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
})

View File

@ -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"
})
}

View File

@ -92,7 +92,8 @@ export class Dialog extends React.PureComponent<DialogProps, DialogState> {
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<DialogProps, DialogState> {
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);
}
}

View File

@ -40,6 +40,7 @@ export class Drawer extends React.Component<DrawerProps> {
});
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)

View File

@ -1 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><style>.cls-2{fill:#3c90ce;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-2" d="M256,496C123.67,496,16,388.33,16,256S123.67,16,256,16,496,123.67,496,256,388.33,496,256,496Zm0-470C129.17,26,26,129.18,26,256S129.17,486,256,486,486,382.82,486,256,382.83,26,256,26Z"/><path class="cls-2" d="M403.22,282l65.66-81.68A220.52,220.52,0,0,0,351.61,57.81Z"/><path class="cls-2" d="M476,256a220.86,220.86,0,0,0-4-41.5L334.74,385.3H434A219,219,0,0,0,476,256Z"/><path class="cls-2" d="M247.63,121.17,139.38,69.48A220,220,0,0,0,38.81,221Z"/><path class="cls-2" d="M140.75,184.81,37.07,234.35C36.38,241.48,36,248.69,36,256A219.13,219.13,0,0,0,91,401.4Z"/><path class="cls-2" d="M210.57,396.65l63.2,78.57a219.52,219.52,0,0,0,151.38-78.57Z"/><path class="cls-2" d="M124.21,307.38l-23.91,104A219.33,219.33,0,0,0,256,476c1.26,0,2.5-.07,3.76-.1Z"/><path class="cls-2" d="M364.52,164.42,338.67,52.12A220.21,220.21,0,0,0,151.16,62.54Z"/><path class="cls-2" d="M351.71,276.74c-.46-.11-1.12-.29-1.58-.37a45.28,45.28,0,0,0-5.2-.42,50.71,50.71,0,0,1-9.73-1.6,6.13,6.13,0,0,1-2.35-2.36l-2.19-.63a70.52,70.52,0,0,0-11.31-48.85l1.93-1.73a4.3,4.3,0,0,1,1-3.08,50.88,50.88,0,0,1,8.07-5.67,43.85,43.85,0,0,0,4.51-2.63c.35-.26.83-.67,1.2-1,2.6-2.08,3.2-5.66,1.33-8s-5.49-2.57-8.09-.49c-.38.29-.88.67-1.21,1a43.42,43.42,0,0,0-3.58,3.79,51.27,51.27,0,0,1-7.32,6.63,6.1,6.1,0,0,1-3.3.36l-2.06,1.47a71.09,71.09,0,0,0-45.06-21.77c-.05-.72-.11-2-.12-2.42a4.33,4.33,0,0,1-1.78-2.72,50.16,50.16,0,0,1,.62-9.84,45.29,45.29,0,0,0,.74-5.17c0-.43,0-1.07,0-1.54,0-3.33-2.43-6-5.43-6s-5.43,2.7-5.43,6c0,.05,0,.1,0,.15,0,.45,0,1,0,1.39a45.29,45.29,0,0,0,.74,5.17,50.87,50.87,0,0,1,.61,9.84,5.9,5.9,0,0,1-1.77,2.81l-.12,2.29a71.38,71.38,0,0,0-9.82,1.51,69.83,69.83,0,0,0-35.46,20.26c-.6-.41-1.65-1.16-2-1.39a4.32,4.32,0,0,1-3.23-.31,50.46,50.46,0,0,1-7.31-6.61,45,45,0,0,0-3.58-3.79c-.33-.29-.83-.67-1.2-1a6.42,6.42,0,0,0-3.78-1.42,5.22,5.22,0,0,0-4.33,1.91c-1.87,2.34-1.27,5.93,1.33,8l.09.06c.35.29.79.66,1.12.91a46.81,46.81,0,0,0,4.5,2.63,49.61,49.61,0,0,1,8.07,5.67,6,6,0,0,1,1.09,3.13l1.74,1.55a70.25,70.25,0,0,0-11.07,49l-2.28.66a7.61,7.61,0,0,1-2.33,2.36,51.5,51.5,0,0,1-9.73,1.6,45.38,45.38,0,0,0-5.21.41c-.41.08-1,.23-1.45.34h0l-.08,0c-3.2.78-5.26,3.72-4.6,6.61a5.75,5.75,0,0,0,7,4h.08l.1,0c.45-.1,1-.21,1.41-.32a45.65,45.65,0,0,0,4.87-1.87,51.49,51.49,0,0,1,9.46-2.78,6.07,6.07,0,0,1,3.12,1.1l2.37-.4A70.59,70.59,0,0,0,225,322.21l-1,2.37a5.46,5.46,0,0,1,.48,3.07,53.5,53.5,0,0,1-4.92,8.83,46,46,0,0,0-2.91,4.33c-.21.41-.48,1-.69,1.47a5.73,5.73,0,0,0,2.31,7.71c2.69,1.3,6-.07,7.49-3.06v0h0c.21-.42.5-1,.67-1.38a45,45,0,0,0,1.57-5c1.43-3.61,2.22-7.4,4.2-9.75a4.31,4.31,0,0,1,2.34-1.14l1.23-2.23a70.15,70.15,0,0,0,40.78,2.93,71.11,71.11,0,0,0,9.31-2.8l1.16,2.09a4.26,4.26,0,0,1,2.77,1.68,50.16,50.16,0,0,1,3.72,9.12,44.6,44.6,0,0,0,1.58,5c.18.4.47,1,.67,1.39,1.45,3,4.81,4.38,7.51,3.07s3.7-4.72,2.3-7.71c-.2-.43-.49-1.05-.7-1.46A45.8,45.8,0,0,0,302,336.4a51.18,51.18,0,0,1-4.82-8.62,4.27,4.27,0,0,1,.42-3.2,20.73,20.73,0,0,1-.9-2.19A70.66,70.66,0,0,0,328,283c.7.11,1.92.33,2.32.41a4.28,4.28,0,0,1,3-1.13,50.68,50.68,0,0,1,9.47,2.79,45.66,45.66,0,0,0,4.87,1.88c.39.1,1,.2,1.4.3l.11,0h.08a5.72,5.72,0,0,0,7-4C357,280.45,354.91,277.51,351.71,276.74Zm-51.57-55.29-23.2,16.45-.08,0a4.79,4.79,0,0,1-6.56-.88,4.71,4.71,0,0,1-1.05-2.77h0l-1.61-28.43A56.44,56.44,0,0,1,300.14,221.45Zm-43.7,31.16h8.73l5.43,6.79-2,8.46-7.84,3.77L253,267.85,251,259.39Zm-8.22-45.84a56.58,56.58,0,0,1,5.79-1l-1.61,28.47-.12.06a4.79,4.79,0,0,1-7.59,3.67l-.05,0-23.35-16.55A55.92,55.92,0,0,1,248.22,206.77Zm-35.39,25.3,21.32,19.07,0,.12a4.79,4.79,0,0,1-1.88,8.22l0,.09-27.32,7.89A55.83,55.83,0,0,1,212.83,232.07Zm28.54,50.65L230.51,309A56.27,56.27,0,0,1,208,280.73L236,276l0,.06A4.37,4.37,0,0,1,237,276a4.78,4.78,0,0,1,4.33,6.67Zm32,33.74a56.07,56.07,0,0,1-30.62-1.58L256.56,290h0a4.77,4.77,0,0,1,4-2.53A4.86,4.86,0,0,1,265,290h.1l13.82,25Q276.24,315.82,273.41,316.46Zm17.81-7.4-11-26.5,0-.05a4.78,4.78,0,0,1,2.32-6.2,4.65,4.65,0,0,1,1.83-.48,5.05,5.05,0,0,1,1.1.08l.05-.05,28.26,4.77A56,56,0,0,1,291.22,309.06Zm25.59-41.69-27.46-7.91,0-.12a4.79,4.79,0,0,1-3.4-5.68,4.7,4.7,0,0,1,1.52-2.54v-.06l21.2-19a56.92,56.92,0,0,1,8.17,35.28Z"/></g></g></svg>
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.rect{fill:none!important;}
</style>
<rect class="rect" width="512" height="512"/>
<g>
<path d="M237,496h184.8l-42.6-181.7L237,496z"/>
<path d="M16,392.3V496h194.2l81.2-103.7H16z"/>
<path d="M280.1,130.3L496,235.4v-209L280.1,130.3z"/>
<path d="M443.6,496H496V258.8l-122-59.4L443.6,496z"/>
<path d="M469,16H200.1l-32.4,145L469,16z"/>
<path d="M16,170v201.1h160L16,170z"/>
<path d="M178.5,16H16v120.1l105.9,133.1L178.5,16z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 730 B

View File

@ -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;
}
}

View File

@ -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

View File

@ -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(),
]
}

View File

@ -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

View File

@ -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"