mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
PoC: first run
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
12472aab5b
commit
4e88715b8d
@ -70,7 +70,7 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
...confOptions,
|
...confOptions,
|
||||||
});
|
});
|
||||||
const jsonModel = this.storeConfig.store;
|
const jsonModel = this.storeConfig.store;
|
||||||
logger.info(`[STORE]: loaded from ${this.storeConfig.path}`);
|
logger.info(`💿 Store loaded from ${this.storeConfig.path}`);
|
||||||
this.fromStore(jsonModel);
|
this.fromStore(jsonModel);
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
}
|
}
|
||||||
@ -92,14 +92,14 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
|
|
||||||
protected onConfigChange(data: T, oldValue: Partial<T>) {
|
protected onConfigChange(data: T, oldValue: Partial<T>) {
|
||||||
if (!isEqual(this.toJSON(), data)) {
|
if (!isEqual(this.toJSON(), data)) {
|
||||||
logger.debug(`[STORE]: received update from ${this.name}`, { data, oldValue });
|
logger.debug(`💿 Store received update from ${this.name}`, { data, oldValue });
|
||||||
this.fromStore(data);
|
this.fromStore(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onModelChange(model: T) {
|
protected onModelChange(model: T) {
|
||||||
if (!isEqual(this.storeModel, model)) {
|
if (!isEqual(this.storeModel, model)) {
|
||||||
logger.debug(`[STORE]: saving ${this.name} from runtime`, {
|
logger.debug(`💿 Store ${this.name} is saving updates from app runtime`, {
|
||||||
data: model,
|
data: model,
|
||||||
oldValue: this.storeModel
|
oldValue: this.storeModel
|
||||||
});
|
});
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||||
|
|
||||||
@computed get activeCluster(): Cluster {
|
@computed get activeCluster(): Cluster {
|
||||||
return this.clusters.get(this.activeClusterId);
|
return this.getById(this.activeClusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get clustersList(): Cluster[] {
|
@computed get clustersList(): Cluster[] {
|
||||||
|
|||||||
@ -41,9 +41,8 @@ export class ClusterManager {
|
|||||||
});
|
});
|
||||||
// auto-refresh status for active cluster
|
// auto-refresh status for active cluster
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const { activeCluster } = clusterStore;
|
if (clusterStore.activeCluster) {
|
||||||
if (activeCluster && activeCluster.initialized) {
|
clusterStore.activeCluster.refreshStatus();
|
||||||
activeCluster.refreshStatus();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// listen ipc-events
|
// listen ipc-events
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
||||||
import type { FeatureStatusMap } from "./feature"
|
import type { FeatureStatusMap } from "./feature"
|
||||||
import { action, observable, toJS } from "mobx";
|
import { action, observable, toJS, when } from "mobx";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { ContextHandler } from "./context-handler"
|
import { ContextHandler } from "./context-handler"
|
||||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||||
@ -23,6 +23,8 @@ export class Cluster implements ClusterModel {
|
|||||||
public contextHandler: ContextHandler;
|
public contextHandler: ContextHandler;
|
||||||
protected kubeconfigManager: KubeconfigManager;
|
protected kubeconfigManager: KubeconfigManager;
|
||||||
|
|
||||||
|
public whenReady = when(() => this.initialized);
|
||||||
|
|
||||||
@observable initialized = false;
|
@observable initialized = false;
|
||||||
@observable contextName: string;
|
@observable contextName: string;
|
||||||
@observable workspace: string;
|
@observable workspace: string;
|
||||||
@ -63,14 +65,14 @@ export class Cluster implements ClusterModel {
|
|||||||
this.webContentUrl = `http://${this.id}.localhost:${port}`;
|
this.webContentUrl = `http://${this.id}.localhost:${port}`;
|
||||||
this.kubeconfigManager = new KubeconfigManager(this);
|
this.kubeconfigManager = new KubeconfigManager(this);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
logger.info(`[✔] Cluster(${this.id}) init success`, {
|
logger.info(`✅ ️Cluster(${this.id}) init success`, {
|
||||||
serverUrl: this.apiUrl,
|
serverUrl: this.apiUrl,
|
||||||
webContentUrl: this.webContentUrl,
|
webContentUrl: this.webContentUrl,
|
||||||
kubeProxyUrl: this.kubeProxyUrl,
|
kubeProxyUrl: this.kubeProxyUrl,
|
||||||
kubeAuthProxyUrl: this.kubeAuthProxyUrl,
|
kubeAuthProxyUrl: this.kubeAuthProxyUrl,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`[X] Cluster(${this.id}) init failed: ${err}`);
|
logger.error(`💣 Cluster(${this.id}) init failed: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +84,7 @@ export class Cluster implements ClusterModel {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
async refreshStatus() {
|
async refreshStatus() {
|
||||||
|
await this.whenReady;
|
||||||
const connectionStatus = await this.getConnectionStatus();
|
const connectionStatus = await this.getConnectionStatus();
|
||||||
this.online = connectionStatus > ClusterStatus.Offline;
|
this.online = connectionStatus > ClusterStatus.Offline;
|
||||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||||
|
|||||||
@ -35,7 +35,7 @@ async function main() {
|
|||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
app.setName(appName);
|
app.setName(appName);
|
||||||
app.setPath("userData", workingDir);
|
app.setPath("userData", workingDir);
|
||||||
logger.info(`Start app from "${workingDir}"`)
|
logger.info(`🚀 Starting Lens from "${workingDir}"`)
|
||||||
|
|
||||||
tracker.event("app", "start");
|
tracker.event("app", "start");
|
||||||
const updater = new AppUpdater()
|
const updater = new AppUpdater()
|
||||||
@ -75,12 +75,13 @@ async function main() {
|
|||||||
|
|
||||||
// create window manager and open app
|
// create window manager and open app
|
||||||
windowManager = new WindowManager();
|
windowManager = new WindowManager();
|
||||||
// windowManager.showSplash();
|
windowManager.showSplash();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
app.on("ready", main);
|
app.on("ready", main);
|
||||||
|
|
||||||
|
// fixme: never happens, Cmd+W doesn't work
|
||||||
app.on('window-all-closed', function () {
|
app.on('window-all-closed', function () {
|
||||||
// On OS X it is common for applications and their menu bar
|
// On OS X it is common for applications and their menu bar
|
||||||
// to stay active until the user quits explicitly with Cmd + Q
|
// to stay active until the user quits explicitly with Cmd + Q
|
||||||
@ -98,9 +99,10 @@ app.on("activate", () => {
|
|||||||
logger.debug("app:activate");
|
logger.debug("app:activate");
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
app.on("will-quit", async (event) => {
|
app.on("will-quit", async (event) => {
|
||||||
event.preventDefault(); // To allow mixpanel sending to be executed
|
event.preventDefault(); // To allow mixpanel sending to be executed
|
||||||
if (clusterManager) clusterManager.stop()
|
if (clusterManager) clusterManager.stop()
|
||||||
if (proxyServer) proxyServer.close()
|
if (proxyServer) proxyServer.close()
|
||||||
app.exit(0);
|
app.exit();
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,35 +3,52 @@ import { BrowserWindow, shell } from "electron"
|
|||||||
import windowStateKeeper from "electron-window-state"
|
import windowStateKeeper from "electron-window-state"
|
||||||
import type { ClusterId } from "../common/cluster-store";
|
import type { ClusterId } from "../common/cluster-store";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
import { clusterStore } from "../common/cluster-store";
|
||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
protected activeView: BrowserWindow;
|
protected activeView: BrowserWindow;
|
||||||
protected views = new Map<ClusterId, BrowserWindow>();
|
protected views = new Map<ClusterId, BrowserWindow>();
|
||||||
|
protected disposers: CallableFunction[] = [];
|
||||||
|
protected splashWindow: BrowserWindow;
|
||||||
|
protected windowState: windowStateKeeper.State;
|
||||||
|
|
||||||
protected disposers = [
|
constructor() {
|
||||||
// auto-destroy views for removed clusters
|
this.splashWindow = new BrowserWindow({
|
||||||
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
|
width: 500,
|
||||||
removedClusters.forEach(cluster => {
|
height: 300,
|
||||||
this.destroyView(cluster.id);
|
backgroundColor: "#1e2124",
|
||||||
});
|
center: true,
|
||||||
})
|
frame: false,
|
||||||
];
|
resizable: false,
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
|
||||||
protected splashWindow = new BrowserWindow({
|
// Manage main window size and position with state persistence
|
||||||
width: 500,
|
this.windowState = windowStateKeeper({
|
||||||
height: 300,
|
defaultHeight: 900,
|
||||||
backgroundColor: "#1e2124",
|
defaultWidth: 1440,
|
||||||
center: true,
|
});
|
||||||
frame: false,
|
|
||||||
resizable: false,
|
|
||||||
show: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manage main window size and position with state persistence
|
// init events and show active cluster view
|
||||||
protected windowState = windowStateKeeper({
|
this.bindEvents();
|
||||||
defaultHeight: 900,
|
}
|
||||||
defaultWidth: 1440,
|
|
||||||
});
|
protected bindEvents() {
|
||||||
|
this.disposers.push(
|
||||||
|
// auto-destroy views for removed clusters
|
||||||
|
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
|
||||||
|
removedClusters.forEach(cluster => {
|
||||||
|
this.destroyView(cluster.id);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
// auto-show active cluster view
|
||||||
|
reaction(() => clusterStore.activeClusterId, clusterId => {
|
||||||
|
this.activateView(clusterId);
|
||||||
|
}, {
|
||||||
|
fireImmediately: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async showSplash() {
|
async showSplash() {
|
||||||
await this.splashWindow.loadURL("static://splash.html")
|
await this.splashWindow.loadURL("static://splash.html")
|
||||||
@ -49,36 +66,45 @@ export class WindowManager {
|
|||||||
async activateView(clusterId: ClusterId) {
|
async activateView(clusterId: ClusterId) {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
throw new Error(`Can't load lens for non-existing cluster="${clusterId}"`);
|
logger.error(`Can't show a view for non-existing cluster(${clusterId})`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const activeView = this.activeView;
|
try {
|
||||||
const isFresh = !this.getView(clusterId);
|
const activeView = this.activeView;
|
||||||
const view = this.initView(clusterId);
|
const isFresh = !this.getView(clusterId);
|
||||||
if (view !== activeView) {
|
const view = this.initView(clusterId);
|
||||||
if (isFresh) {
|
if (view !== activeView) {
|
||||||
await view.loadURL(cluster.webContentUrl);
|
if (isFresh) {
|
||||||
|
await cluster.whenReady;
|
||||||
|
await view.loadURL(cluster.webContentUrl);
|
||||||
|
}
|
||||||
|
if (activeView) {
|
||||||
|
view.setBounds(activeView.getBounds()); // refresh position and swap windows
|
||||||
|
activeView.hide();
|
||||||
|
}
|
||||||
|
view.show();
|
||||||
|
this.hideSplash();
|
||||||
|
this.activeView = view;
|
||||||
}
|
}
|
||||||
if (activeView) {
|
} catch (err) {
|
||||||
view.setBounds(activeView.getBounds()); // refresh position for "invisible swap"
|
logger.error(`Activating cluster(${clusterId}) view has failed: ${err.stack}`);
|
||||||
activeView.hide();
|
|
||||||
}
|
|
||||||
view.show();
|
|
||||||
this.activeView = view;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected initView(clusterId: ClusterId) {
|
protected initView(clusterId: ClusterId) {
|
||||||
let view = this.getView(clusterId);
|
let view = this.getView(clusterId);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
|
const { width, height, x, y } = this.windowState;
|
||||||
view = new BrowserWindow({
|
view = new BrowserWindow({
|
||||||
show: false,
|
show: false,
|
||||||
x: this.windowState.x,
|
x: x, y: y,
|
||||||
y: this.windowState.y,
|
width: width,
|
||||||
width: this.windowState.width,
|
height: height,
|
||||||
height: this.windowState.height,
|
|
||||||
titleBarStyle: "hidden",
|
titleBarStyle: "hidden",
|
||||||
|
backgroundColor: "#1e2124",
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// open external links in default browser (target=_blank, window.open)
|
// open external links in default browser (target=_blank, window.open)
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
.ClusterIcon {
|
.ClusterIcon {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
--size: 40px;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
.Badge {
|
.Badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@ -7,15 +7,16 @@ import { Cluster } from "../../../main/cluster";
|
|||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
|
|
||||||
interface Props extends DOMAttributes<HTMLElement>, Omit<HashiconProps, "value"> {
|
interface Props extends DOMAttributes<HTMLElement> {
|
||||||
className?: IClassName;
|
|
||||||
showBadge?: boolean;
|
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
|
className?: IClassName;
|
||||||
|
errorClass?: IClassName;
|
||||||
|
showErrorCount?: boolean;
|
||||||
|
options?: HashiconProps["options"]
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps: Partial<Props> = {
|
const defaultProps: Partial<Props> = {
|
||||||
size: 38,
|
showErrorCount: true,
|
||||||
showBadge: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -23,23 +24,20 @@ export class ClusterIcon extends React.Component<Props> {
|
|||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, cluster, showBadge, options, size, ...elemProps } = this.props;
|
const { className, cluster, showErrorCount, errorClass, options, children, ...elemProps } = this.props;
|
||||||
const { isAdmin, eventCount, preferences } = cluster;
|
const { isAdmin, eventCount, preferences } = cluster;
|
||||||
const { clusterName, icon } = preferences;
|
const { clusterName, icon } = preferences;
|
||||||
const eventsCount = eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount;
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("ClusterIcon flex inline", className)} {...elemProps}>
|
<div className={cssNames("ClusterIcon flex inline", className)} {...elemProps}>
|
||||||
{icon && <img src={icon} width={size} height={size} alt={clusterName}/>}
|
{icon && <img src={icon} alt={clusterName}/>}
|
||||||
{!icon && (
|
{!icon && <Hashicon value={clusterName} options={options}/>}
|
||||||
<Hashicon
|
{showErrorCount && isAdmin && eventCount > 0 && (
|
||||||
value={clusterName}
|
<Badge
|
||||||
size={size}
|
className={cssNames("events-count", errorClass)}
|
||||||
options={options}
|
label={eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showBadge && isAdmin && eventsCount && (
|
{children}
|
||||||
<Badge label={eventsCount} className="events-count"/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,10 @@
|
|||||||
padding: $padding * 1.5;
|
padding: $padding * 1.5;
|
||||||
background: $menuBgc;
|
background: $menuBgc;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.add-cluster {
|
.add-cluster {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@ -27,8 +31,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: reuse in cluster-icon.tsx
|
.Badge.counter {
|
||||||
.Badge.new-contexts {
|
|
||||||
$boxSize: 17px;
|
$boxSize: 17px;
|
||||||
$offset: -7px;
|
$offset: -7px;
|
||||||
|
|
||||||
@ -45,6 +48,7 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,15 +64,16 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
<ClusterIcon
|
<ClusterIcon
|
||||||
key={cluster.id}
|
key={cluster.id}
|
||||||
cluster={cluster}
|
cluster={cluster}
|
||||||
|
showErrorCount={true}
|
||||||
className={cssNames({ active: isActive })}
|
className={cssNames({ active: isActive })}
|
||||||
onClick={() => this.selectCluster(cluster)}
|
onClick={() => this.selectCluster(cluster)}
|
||||||
onContextMenu={() => this.showContextMenu(cluster)}
|
onContextMenu={() => this.showContextMenu(cluster)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<div className="add-cluster">
|
<div className="add-cluster" onClick={this.addCluster}>
|
||||||
<Icon
|
<Icon
|
||||||
big material="add" className="add" onClick={this.addCluster}
|
big material="add" className="add"
|
||||||
tooltip={(
|
tooltip={(
|
||||||
<div className="flex column gaps">
|
<div className="flex column gaps">
|
||||||
<p><Trans>This is the quick launch menu.</Trans></p>
|
<p><Trans>This is the quick launch menu.</Trans></p>
|
||||||
@ -85,7 +86,7 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{newContexts.length > 0 && (
|
{newContexts.length > 0 && (
|
||||||
<Badge className="new-contexts" label={newContexts.length}/>
|
<Badge className="counter" label={newContexts.length}/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -65,8 +65,7 @@ export class Menu extends React.Component<MenuProps, State> {
|
|||||||
const parent = this.elem.parentElement;
|
const parent = this.elem.parentElement;
|
||||||
const position = window.getComputedStyle(parent).position;
|
const position = window.getComputedStyle(parent).position;
|
||||||
if (position === 'static') parent.style.position = 'relative';
|
if (position === 'static') parent.style.position = 'relative';
|
||||||
}
|
} else if (this.isOpen) {
|
||||||
else if (this.isOpen) {
|
|
||||||
this.refreshPosition();
|
this.refreshPosition();
|
||||||
}
|
}
|
||||||
this.opener = document.getElementById(this.props.htmlFor); // might not exist in sub-menus
|
this.opener = document.getElementById(this.props.htmlFor); // might not exist in sub-menus
|
||||||
@ -109,8 +108,7 @@ export class Menu extends React.Component<MenuProps, State> {
|
|||||||
let nextItem = reverse ? items[activeIndex - 1] : items[activeIndex + 1];
|
let nextItem = reverse ? items[activeIndex - 1] : items[activeIndex + 1];
|
||||||
if (!nextItem) nextItem = items[activeIndex];
|
if (!nextItem) nextItem = items[activeIndex];
|
||||||
nextItem.elem.focus();
|
nextItem.elem.focus();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
items[0].elem.focus();
|
items[0].elem.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,7 +222,7 @@ export class Menu extends React.Component<MenuProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { position } = this.props;
|
const { position, id } = this.props;
|
||||||
let { className, usePortal } = this.props;
|
let { className, usePortal } = this.props;
|
||||||
className = cssNames('Menu', className, this.state.position || position, {
|
className = cssNames('Menu', className, this.state.position || position, {
|
||||||
portal: usePortal,
|
portal: usePortal,
|
||||||
@ -246,7 +244,7 @@ export class Menu extends React.Component<MenuProps, State> {
|
|||||||
const menu = (
|
const menu = (
|
||||||
<MenuContext.Provider value={this}>
|
<MenuContext.Provider value={this}>
|
||||||
<Animate enter={this.isOpen}>
|
<Animate enter={this.isOpen}>
|
||||||
<ul className={className} ref={this.bindRef}>
|
<ul id={id} className={className} ref={this.bindRef}>
|
||||||
{menuItems}
|
{menuItems}
|
||||||
</ul>
|
</ul>
|
||||||
</Animate>
|
</Animate>
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Lens - The Kubernetes IDE</title>
|
<title>Lens - The Kubernetes IDE</title>
|
||||||
<!--<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />-->
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user