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 committed 2020-09-11 12:29:38 +03:00
commit a3dc928430
34 files changed
+273 -139

No files matched your search

+6 -13
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);
}
},
}),
+11 -5
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 {
+10 -4
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);
})
})
})
+10 -3
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
+44
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", () => {
+7 -13
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;
+8 -5
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,
+2 -2
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;
});
+1 -11
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;
}
+2 -2
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);
@@ -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;
@@ -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> {
</>
);
}
}
}
@@ -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;
@@ -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 = () => {
@@ -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;
@@ -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>
+2 -2
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() {
@@ -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,
}),
])
}
@@ -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>
@@ -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
})
@@ -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"
})
}
+3 -2
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);
}
}
@@ -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)
+17 -1
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

+3 -2
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;
}
}