1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-15 11:07:32 +03:00
parent c06c322ca9
commit 547d7f8bbe
12 changed files with 70 additions and 58 deletions

View File

@ -36,8 +36,8 @@ export class BaseStore<T = any> extends Singleton {
return path.basename(this.storeConfig.path); return path.basename(this.storeConfig.path);
} }
get syncEvent() { get syncChannel() {
return `[STORE]:[SYNC]:${this.name}` return `store-sync:${this.name}`
} }
protected async init() { protected async init() {
@ -57,21 +57,22 @@ export class BaseStore<T = any> extends Singleton {
projectName: "lens", projectName: "lens",
projectVersion: getAppVersion(), projectVersion: getAppVersion(),
get cwd() { get cwd() {
return (app || remote.app).getPath("userData"); return (app || remote.app).getPath("userData"); // todo: remove usage of remote.app (deprecated)
}, },
}); });
const storeModel = Object.assign({}, this.storeConfig.store); const storedModel = Object.assign({}, this.storeConfig.store);
Reflect.deleteProperty(storeModel, "__internal__"); // fixme: avoid "external-internals" Reflect.deleteProperty(storedModel, "__internal__"); // fixme: avoid "external-internals"
logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`); logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`);
this.fromStore(storeModel); this.fromStore(storedModel);
this.isLoaded = true; this.isLoaded = true;
} }
protected async save(model: T) { protected async save(model: T) {
logger.info(`[STORE]: SAVING ${this.name}`); logger.info(`[STORE]: SAVING ${this.name}`);
// todo: avoid multiple file updates
// fixme: https://github.com/sindresorhus/conf/issues/114 // fixme: https://github.com/sindresorhus/conf/issues/114
Object.entries(model).forEach(([key, value]) => { Object.entries(model).forEach(([key, value]) => {
this.storeConfig.set(key, value); // save update to config file this.storeConfig.set(key, value);
}); });
} }
@ -80,18 +81,18 @@ export class BaseStore<T = any> extends Singleton {
reaction(() => this.toJSON(), model => this.onModelChange(model)), reaction(() => this.toJSON(), model => this.onModelChange(model)),
); );
if (ipcMain) { if (ipcMain) {
ipcMain.on(this.syncEvent, (event, model: T) => { ipcMain.on(this.syncChannel, (event, model: T) => {
logger.info(`[STORE]: SYNC ${this.name} from renderer`); logger.debug(`[STORE]: SYNC ${this.name} from renderer`, { model });
this.onSync(model); this.onSync(model);
}); });
this.syncDisposers.push(() => ipcMain.removeAllListeners(this.syncEvent)); this.syncDisposers.push(() => ipcMain.removeAllListeners(this.syncChannel));
} }
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.on(this.syncEvent, (event, model: T) => { ipcRenderer.on(this.syncChannel, (event, model: T) => {
logger.info(`[STORE]: SYNC ${this.name} from main`); logger.debug(`[STORE]: SYNC ${this.name} from main`, { model });
this.onSync(model); this.onSync(model);
}); });
this.syncDisposers.push(() => ipcRenderer.removeAllListeners(this.syncEvent)); this.syncDisposers.push(() => ipcRenderer.removeAllListeners(this.syncChannel));
} }
} }
@ -108,12 +109,12 @@ export class BaseStore<T = any> extends Singleton {
protected async onModelChange(model: T) { protected async onModelChange(model: T) {
if (ipcMain) { if (ipcMain) {
this.save(model); // save to config file this.save(model); // save config file
broadcastMessage({ channel: this.syncEvent }, model); // broadcast to renderer views broadcastMessage({ channel: this.syncChannel }, model); // broadcast to renderer views
} }
// send "update-request" to main-process // send "update-request" to main-process
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.send(this.syncEvent, model); ipcRenderer.send(this.syncChannel, model);
} }
} }

View File

@ -21,15 +21,16 @@ export interface IpcBroadcastOpts {
} }
export function broadcastMessage({ channel, filter }: IpcBroadcastOpts, ...args: any[]) { export function broadcastMessage({ channel, filter }: IpcBroadcastOpts, ...args: any[]) {
let webContentsList = webContents.getAllWebContents(); if (!filter) {
if (filter) { filter = webContent => webContent.getType() === "window"
webContentsList = webContentsList.filter(filter);
} }
webContentsList.forEach(webContent => { webContents.getAllWebContents().filter(filter).forEach(webContent => {
logger.info(`[IPC]: broadcasting ${channel} to ${webContent.getType()}=${webContent.id}`);
webContent.send(channel, ...args); webContent.send(channel, ...args);
}) })
} }
// fixme: support timeout
export async function invokeMessage<T = any>(channel: IpcChannel, ...args: any[]): Promise<T> { export async function invokeMessage<T = any>(channel: IpcChannel, ...args: any[]): Promise<T> {
logger.info(`[IPC]: invoke channel "${channel}"`, { args }); logger.info(`[IPC]: invoke channel "${channel}"`, { args });
return ipcRenderer.invoke(channel, ...args); return ipcRenderer.invoke(channel, ...args);

View File

@ -84,7 +84,7 @@ export class KubeAuthProxy {
const channel = `kube-auth:${this.cluster.id}` const channel = `kube-auth:${this.cluster.id}`
const message = { data, stream }; const message = { data, stream };
logger.debug(channel, message); logger.debug(channel, message);
broadcastMessage({ channel }, message); broadcastMessage({ channel }, message); // todo: send message only to cluster's window
} }
public exit() { public exit() {

View File

@ -2,7 +2,7 @@ import Call from "@hapi/call"
import Subtext from "@hapi/subtext" import Subtext from "@hapi/subtext"
import http from "http" import http from "http"
import path from "path" import path from "path"
import { readFile, stat } from "fs-extra" import { readFile } from "fs-extra"
import { Cluster } from "./cluster" import { Cluster } from "./cluster"
import { apiPrefix, appName, outDir } from "../common/vars"; import { apiPrefix, appName, outDir } from "../common/vars";
import { configRoute, helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes"; import { configRoute, helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
@ -97,13 +97,12 @@ export class Router {
protected async handleStaticFile(filePath: string, response: http.ServerResponse) { protected async handleStaticFile(filePath: string, response: http.ServerResponse) {
const asset = path.resolve(outDir, filePath); const asset = path.resolve(outDir, filePath);
const info = await stat(asset); try {
if (info.isFile()) {
const data = await readFile(asset); const data = await readFile(asset);
response.setHeader("Content-Type", this.getMimeType(asset)); response.setHeader("Content-Type", this.getMimeType(asset));
response.write(data) response.write(data)
response.end() response.end()
} else { } catch (err) {
this.handleStaticFile(`${appName}.html`, response); this.handleStaticFile(`${appName}.html`, response);
} }
} }

View File

@ -1,3 +1,4 @@
import url from "url"
import { LensApiRequest } from "../router" import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" import { LensApi } from "../lens-api"
import requestPromise from "request-promise-native" import requestPromise from "request-promise-native"
@ -8,9 +9,19 @@ export type IMetricsQuery = string | string[] | {
} }
class MetricsRoute extends LensApi { class MetricsRoute extends LensApi {
public async routeMetrics(request: LensApiRequest<IMetricsQuery>) {
public async routeMetrics(request: LensApiRequest) {
const { response, cluster, payload } = request const { response, cluster, payload } = request
const { contextHandler, kubeProxyUrl } = cluster; const { contextHandler, kubeProxyUrl } = cluster;
const headers: Record<string, string> = {
"Host": url.parse(cluster.webContentUrl).host,
"Content-type": "application/json",
}
const queryParams: IMetricsQuery = {}
request.query.forEach((value: string, key: string) => {
queryParams[key] = value
})
let metricsUrl: string let metricsUrl: string
let prometheusProvider: PrometheusProvider let prometheusProvider: PrometheusProvider
try { try {
@ -22,20 +33,23 @@ class MetricsRoute extends LensApi {
return return
} }
// prometheus metrics loader // prometheus metrics loader
const attempts: Record<string, number> = {}; const attempts: { [query: string]: number } = {};
const maxAttempts = 5; const maxAttempts = 5;
const loadMetrics = (promQuery: string): Promise<any> => { const loadMetrics = (orgQuery: string): Promise<any> => {
const queryString = request.query.toString() + `&query=` + promQuery; const query = orgQuery.trim()
const attempt = attempts[queryString] = (attempts[queryString] || 0) + 1; const attempt = attempts[query] = (attempts[query] || 0) + 1;
return requestPromise(metricsUrl, { return requestPromise(metricsUrl, {
json: true,
qs: queryString,
useQuerystring: true,
resolveWithFullResponse: false, resolveWithFullResponse: false,
headers: headers,
json: true,
qs: {
query: query,
...queryParams
}
}).catch(async (error) => { }).catch(async (error) => {
if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) { if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) {
await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request
return loadMetrics(queryString); return loadMetrics(query);
} }
return { return {
status: error.toString(), status: error.toString(),

View File

@ -71,22 +71,24 @@ export class WindowManager {
const isLoadedBefore = !!this.getView(clusterId); const isLoadedBefore = !!this.getView(clusterId);
const view = this.initView(clusterId); const view = this.initView(clusterId);
logger.info(`[WINDOW-MANAGER]: activating cluster view`, { logger.info(`[WINDOW-MANAGER]: activating cluster view`, {
id: cluster.id, id: view.id,
clusterId: cluster.id,
contextName: cluster.contextName, contextName: cluster.contextName,
isLoadedBefore: isLoadedBefore, isLoadedBefore: isLoadedBefore,
}); });
if (activeView !== view) { if (activeView !== view) {
this.activeView = view;
if (!isLoadedBefore) { if (!isLoadedBefore) {
await cluster.whenReady; await cluster.whenReady;
await view.loadURL(cluster.webContentUrl); await view.loadURL(cluster.webContentUrl);
this.hideSplash();
} }
// refresh position and hide previous active window
if (activeView) { if (activeView) {
view.setBounds(activeView.getBounds()); // refresh position and swap windows view.setBounds(activeView.getBounds());
activeView.hide(); activeView.hide();
} }
view.show(); view.show();
this.hideSplash();
this.activeView = view;
} }
} catch (err) { } catch (err) {
logger.error(`[WINDOW-MANAGER]: can't activate cluster view`, { logger.error(`[WINDOW-MANAGER]: can't activate cluster view`, {

View File

@ -39,6 +39,16 @@ html, body {
min-height: 100%; min-height: 100%;
} }
// fixme: doesn't work
#draggable-top {
@include set-draggable;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 20px;
}
body { body {
font: $font-size $font-main; font: $font-size $font-main;
} }

View File

@ -1,7 +1,6 @@
import "./app.scss"; import "./app.scss";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { i18nStore } from "../i18n"; import { i18nStore } from "../i18n";
import { configStore } from "../config.store"; import { configStore } from "../config.store";
import { Terminal } from "./dock/terminal"; import { Terminal } from "./dock/terminal";
@ -30,9 +29,7 @@ import { DeploymentScaleDialog } from "./+workloads-deployments/deployment-scale
import { CustomResources } from "./+custom-resources/custom-resources"; import { CustomResources } from "./+custom-resources/custom-resources";
import { crdRoute } from "./+custom-resources"; import { crdRoute } from "./+custom-resources";
import { isAllowedResource } from "../api/rbac"; import { isAllowedResource } from "../api/rbac";
import { clusterStore } from "../../common/cluster-store";
@observer
export class App extends React.Component { export class App extends React.Component {
static rootElem = document.getElementById('app'); static rootElem = document.getElementById('app');
@ -48,13 +45,6 @@ export class App extends React.Component {
<Fragment> <Fragment>
<Switch> <Switch>
<Switch> <Switch>
<Route children={() => (
<div>
<p className="info">App is running!</p>
<p>Current cluster:</p>
<pre>{JSON.stringify(clusterStore.activeCluster.toJSON(), null, 2)}</pre>
</div>
)}/>
<Route component={Cluster} {...clusterRoute}/> <Route component={Cluster} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/> <Route component={Nodes} {...nodesRoute}/>
<Route component={Workloads} {...workloadsRoute}/> <Route component={Workloads} {...workloadsRoute}/>

View File

@ -1,22 +1,17 @@
.ClusterManager { .ClusterManager {
display: grid; display: grid;
grid-template-areas: "draggable draggable" "menu lens-view" "bottom-bar bottom-bar"; grid-template-areas: "menu lens-view" "menu lens-view" "bottom-bar bottom-bar";
grid-template-rows: auto 1fr min-content; grid-template-rows: auto 1fr min-content;
grid-template-columns: min-content 1fr; grid-template-columns: min-content 1fr;
height: 100%; height: 100%;
.draggable-top {
@include set-draggable;
grid-area: draggable;
height: 25px;
}
#lens-view { #lens-view {
position: relative; position: relative;
grid-area: lens-view; grid-area: lens-view;
} }
.ClustersMenu { .ClustersMenu {
margin-top: 25px;
grid-area: menu; grid-area: menu;
} }

View File

@ -8,7 +8,7 @@ export class ClusterManager extends React.Component {
const { children: lensView } = this.props; const { children: lensView } = this.props;
return ( return (
<div className="ClusterManager"> <div className="ClusterManager">
<div className="draggable-top"/> <div id="draggable-top"></div>
<div id="lens-view">{lensView}</div> <div id="lens-view">{lensView}</div>
<ClustersMenu/> <ClustersMenu/>
<BottomBar/> <BottomBar/>

View File

@ -4,7 +4,7 @@
--flex-gap: #{$padding * 2}; --flex-gap: #{$padding * 2};
--menu-bgc: #252729; --menu-bgc: #252729;
padding: $padding * 1.5; padding: $padding * 2;
background: var(--menu-bgc); background: var(--menu-bgc);
.add-cluster { .add-cluster {

View File

@ -7,7 +7,7 @@
grid-template-areas: "aside header" "aside tabs" "aside main" "aside footer"; grid-template-areas: "aside header" "aside tabs" "aside main" "aside footer";
grid-template-rows: [header] var(--main-layout-header) [tabs] min-content [main] 1fr [footer] auto; grid-template-rows: [header] var(--main-layout-header) [tabs] min-content [main] 1fr [footer] auto;
grid-template-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr; grid-template-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr;
height: 100vh; height: 100%;
&.light { &.light {
main { main {