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,
});
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.isLoaded = true;
}
@ -92,14 +92,14 @@ export class BaseStore<T = any> extends Singleton {
protected onConfigChange(data: T, oldValue: Partial<T>) {
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);
}
}
protected onModelChange(model: T) {
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,
oldValue: this.storeModel
});

View File

@ -53,7 +53,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@observable clusters = observable.map<ClusterId, Cluster>();
@computed get activeCluster(): Cluster {
return this.clusters.get(this.activeClusterId);
return this.getById(this.activeClusterId);
}
@computed get clustersList(): Cluster[] {

View File

@ -41,9 +41,8 @@ export class ClusterManager {
});
// auto-refresh status for active cluster
autorun(() => {
const { activeCluster } = clusterStore;
if (activeCluster && activeCluster.initialized) {
activeCluster.refreshStatus();
if (clusterStore.activeCluster) {
clusterStore.activeCluster.refreshStatus();
}
});
// listen ipc-events

View File

@ -1,6 +1,6 @@
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature"
import { action, observable, toJS } from "mobx";
import { action, observable, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars";
import { ContextHandler } from "./context-handler"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
@ -23,6 +23,8 @@ export class Cluster implements ClusterModel {
public contextHandler: ContextHandler;
protected kubeconfigManager: KubeconfigManager;
public whenReady = when(() => this.initialized);
@observable initialized = false;
@observable contextName: string;
@observable workspace: string;
@ -63,14 +65,14 @@ export class Cluster implements ClusterModel {
this.webContentUrl = `http://${this.id}.localhost:${port}`;
this.kubeconfigManager = new KubeconfigManager(this);
this.initialized = true;
logger.info(`[✔] Cluster(${this.id}) init success`, {
logger.info(`Cluster(${this.id}) init success`, {
serverUrl: this.apiUrl,
webContentUrl: this.webContentUrl,
kubeProxyUrl: this.kubeProxyUrl,
kubeAuthProxyUrl: this.kubeAuthProxyUrl,
});
} 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
async refreshStatus() {
await this.whenReady;
const connectionStatus = await this.getConnectionStatus();
this.online = connectionStatus > ClusterStatus.Offline;
this.accessible = connectionStatus == ClusterStatus.AccessGranted;

View File

@ -35,7 +35,7 @@ async function main() {
const workingDir = path.join(app.getPath("appData"), appName);
app.setName(appName);
app.setPath("userData", workingDir);
logger.info(`Start app from "${workingDir}"`)
logger.info(`🚀 Starting Lens from "${workingDir}"`)
tracker.event("app", "start");
const updater = new AppUpdater()
@ -75,12 +75,13 @@ async function main() {
// create window manager and open app
windowManager = new WindowManager();
// windowManager.showSplash();
windowManager.showSplash();
}
// Events
app.on("ready", main);
// fixme: never happens, Cmd+W doesn't work
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
@ -98,9 +99,10 @@ app.on("activate", () => {
logger.debug("app:activate");
})
app.on("will-quit", async (event) => {
event.preventDefault(); // To allow mixpanel sending to be executed
if (clusterManager) clusterManager.stop()
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 type { ClusterId } from "../common/cluster-store";
import { clusterStore } from "../common/cluster-store";
import logger from "./logger";
export class WindowManager {
protected activeView: BrowserWindow;
protected views = new Map<ClusterId, BrowserWindow>();
protected disposers: CallableFunction[] = [];
protected splashWindow: BrowserWindow;
protected windowState: windowStateKeeper.State;
protected disposers = [
// auto-destroy views for removed clusters
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
removedClusters.forEach(cluster => {
this.destroyView(cluster.id);
});
})
];
constructor() {
this.splashWindow = new BrowserWindow({
width: 500,
height: 300,
backgroundColor: "#1e2124",
center: true,
frame: false,
resizable: false,
show: false,
});
protected splashWindow = new BrowserWindow({
width: 500,
height: 300,
backgroundColor: "#1e2124",
center: true,
frame: false,
resizable: false,
show: false,
});
// Manage main window size and position with state persistence
this.windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
// Manage main window size and position with state persistence
protected windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
// init events and show active cluster view
this.bindEvents();
}
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() {
await this.splashWindow.loadURL("static://splash.html")
@ -49,36 +66,45 @@ export class WindowManager {
async activateView(clusterId: ClusterId) {
const cluster = clusterStore.getById(clusterId);
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;
const isFresh = !this.getView(clusterId);
const view = this.initView(clusterId);
if (view !== activeView) {
if (isFresh) {
await view.loadURL(cluster.webContentUrl);
try {
const activeView = this.activeView;
const isFresh = !this.getView(clusterId);
const view = this.initView(clusterId);
if (view !== activeView) {
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) {
view.setBounds(activeView.getBounds()); // refresh position for "invisible swap"
activeView.hide();
}
view.show();
this.activeView = view;
} catch (err) {
logger.error(`Activating cluster(${clusterId}) view has failed: ${err.stack}`);
}
}
protected initView(clusterId: ClusterId) {
let view = this.getView(clusterId);
if (!view) {
const { width, height, x, y } = this.windowState;
view = new BrowserWindow({
show: false,
x: this.windowState.x,
y: this.windowState.y,
width: this.windowState.width,
height: this.windowState.height,
x: x, y: y,
width: width,
height: height,
titleBarStyle: "hidden",
backgroundColor: "#1e2124",
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
},
});
// open external links in default browser (target=_blank, window.open)

View File

@ -1,5 +1,11 @@
.ClusterIcon {
position: relative;
--size: 40px;
> img {
width: var(--size);
height: var(--size);
}
.Badge {
position: absolute;

View File

@ -7,15 +7,16 @@ import { Cluster } from "../../../main/cluster";
import { cssNames, IClassName } from "../../utils";
import { Badge } from "../badge";
interface Props extends DOMAttributes<HTMLElement>, Omit<HashiconProps, "value"> {
className?: IClassName;
showBadge?: boolean;
interface Props extends DOMAttributes<HTMLElement> {
cluster: Cluster;
className?: IClassName;
errorClass?: IClassName;
showErrorCount?: boolean;
options?: HashiconProps["options"]
}
const defaultProps: Partial<Props> = {
size: 38,
showBadge: true,
showErrorCount: true,
};
@observer
@ -23,23 +24,20 @@ export class ClusterIcon extends React.Component<Props> {
static defaultProps = defaultProps as object;
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 { clusterName, icon } = preferences;
const eventsCount = eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount;
return (
<div className={cssNames("ClusterIcon flex inline", className)} {...elemProps}>
{icon && <img src={icon} width={size} height={size} alt={clusterName}/>}
{!icon && (
<Hashicon
value={clusterName}
size={size}
options={options}
{icon && <img src={icon} alt={clusterName}/>}
{!icon && <Hashicon value={clusterName} options={options}/>}
{showErrorCount && isAdmin && eventCount > 0 && (
<Badge
className={cssNames("events-count", errorClass)}
label={eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount}
/>
)}
{showBadge && isAdmin && eventsCount && (
<Badge label={eventsCount} className="events-count"/>
)}
{children}
</div>
);
}

View File

@ -5,6 +5,10 @@
padding: $padding * 1.5;
background: $menuBgc;
> * {
cursor: pointer;
}
.add-cluster {
position: relative;
@ -27,8 +31,7 @@
}
}
// todo: reuse in cluster-icon.tsx
.Badge.new-contexts {
.Badge.counter {
$boxSize: 17px;
$offset: -7px;
@ -45,6 +48,7 @@
font-weight: normal;
border-radius: $radius;
padding: 0;
pointer-events: none;
}
}
}

View File

@ -64,15 +64,16 @@ export class ClustersMenu extends React.Component<Props> {
<ClusterIcon
key={cluster.id}
cluster={cluster}
showErrorCount={true}
className={cssNames({ active: isActive })}
onClick={() => this.selectCluster(cluster)}
onContextMenu={() => this.showContextMenu(cluster)}
/>
)
})}
<div className="add-cluster">
<div className="add-cluster" onClick={this.addCluster}>
<Icon
big material="add" className="add" onClick={this.addCluster}
big material="add" className="add"
tooltip={(
<div className="flex column gaps">
<p><Trans>This is the quick launch menu.</Trans></p>
@ -85,7 +86,7 @@ export class ClustersMenu extends React.Component<Props> {
)}
/>
{newContexts.length > 0 && (
<Badge className="new-contexts" label={newContexts.length}/>
<Badge className="counter" label={newContexts.length}/>
)}
</div>
</div>

View File

@ -65,8 +65,7 @@ export class Menu extends React.Component<MenuProps, State> {
const parent = this.elem.parentElement;
const position = window.getComputedStyle(parent).position;
if (position === 'static') parent.style.position = 'relative';
}
else if (this.isOpen) {
} else if (this.isOpen) {
this.refreshPosition();
}
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];
if (!nextItem) nextItem = items[activeIndex];
nextItem.elem.focus();
}
else {
} else {
items[0].elem.focus();
}
}
@ -224,7 +222,7 @@ export class Menu extends React.Component<MenuProps, State> {
}
render() {
const { position } = this.props;
const { position, id } = this.props;
let { className, usePortal } = this.props;
className = cssNames('Menu', className, this.state.position || position, {
portal: usePortal,
@ -246,7 +244,7 @@ export class Menu extends React.Component<MenuProps, State> {
const menu = (
<MenuContext.Provider value={this}>
<Animate enter={this.isOpen}>
<ul className={className} ref={this.bindRef}>
<ul id={id} className={className} ref={this.bindRef}>
{menuItems}
</ul>
</Animate>

View File

@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8">
<title>Lens - The Kubernetes IDE</title>
<!--<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />-->
</head>
<body>