mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
fixes
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
c06c322ca9
commit
547d7f8bbe
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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`, {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}/>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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/>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user