1
0
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:
Roman 2020-07-13 19:32:55 +03:00
parent 12472aab5b
commit 4e88715b8d
12 changed files with 116 additions and 80 deletions

View File

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

View File

@ -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[] {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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