1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

cluster-manager view

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-10 12:47:50 +03:00
parent b05ed9822d
commit 6df56b5471
24 changed files with 167 additions and 141 deletions

View File

@ -7,7 +7,6 @@ export const isWindows = process.platform === "win32"
export const isDebugging = process.env.DEBUG === "true"; export const isDebugging = process.env.DEBUG === "true";
export const isProduction = process.env.NODE_ENV === "production" export const isProduction = process.env.NODE_ENV === "production"
export const isDevelopment = isDebugging || !isProduction; export const isDevelopment = isDebugging || !isProduction;
export const buildVersion = process.env.BUILD_VERSION;
export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isTestEnv = !!process.env.JEST_WORKER_ID;
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}` export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`

View File

@ -78,8 +78,7 @@ export class Cluster implements ClusterModel {
this.contextHandler = new ContextHandler(this); this.contextHandler = new ContextHandler(this);
const proxyPort = await this.contextHandler.resolveProxyPort(); const proxyPort = await this.contextHandler.resolveProxyPort();
this.kubeconfigManager = new KubeconfigManager(this, proxyPort); this.kubeconfigManager = new KubeconfigManager(this, proxyPort);
this.url = this.contextHandler.url this.url = this.contextHandler.url;
// todo: verify api url
// this.apiUrl = kubeConfig.getCurrentCluster().server; // this.apiUrl = kubeConfig.getCurrentCluster().server;
this.initialized = true; this.initialized = true;
logger.debug(`[CLUSTER]: init done (id="${this.id}", context="${this.contextName}")`); logger.debug(`[CLUSTER]: init done (id="${this.id}", context="${this.contextName}")`);

View File

@ -111,12 +111,7 @@ export class ContextHandler {
async resolveProxyPort(): Promise<number> { async resolveProxyPort(): Promise<number> {
if (!this.proxyPort) { if (!this.proxyPort) {
try { this.proxyPort = await getFreePort()
this.proxyPort = await getFreePort()
} catch (error) {
logger.error(error)
throw(error)
}
} }
return this.proxyPort return this.proxyPort
} }
@ -133,7 +128,7 @@ export class ContextHandler {
if (!this.proxyServer) { if (!this.proxyServer) {
const proxyPort = await this.resolveProxyPort() const proxyPort = await this.resolveProxyPort()
const proxyEnv = Object.assign({}, process.env) const proxyEnv = Object.assign({}, process.env)
if (this.cluster?.preferences.httpsProxy) { if (this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
} }
this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv) this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv)

View File

@ -6,7 +6,7 @@ import { app, dialog } from "electron"
import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars"; import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars";
import path from "path" import path from "path"
import initMenu from "./menu" import initMenu from "./menu"
import { LensProxy, listen } from "./proxy" import { LensProxy, listen } from "./lens-proxy"
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
import AppUpdater from "./app-updater" import AppUpdater from "./app-updater"

View File

@ -4,27 +4,20 @@ import { Socket } from "net";
import * as url from "url"; import * as url from "url";
import * as WebSocket from "ws" import * as WebSocket from "ws"
import { ContextHandler } from "./context-handler"; import { ContextHandler } from "./context-handler";
import logger from "./logger"
import * as shell from "./node-shell-session" import * as shell from "./node-shell-session"
import { ClusterManager } from "./cluster-manager" import { ClusterManager } from "./cluster-manager"
import { Router } from "./router" import { Router } from "./router"
import { apiPrefix } from "../common/vars"; import { apiPrefix } from "../common/vars";
import logger from "./logger"
export class LensProxy { export class LensProxy {
public static readonly localShellSessions = true
public port: number;
protected clusterUrl: url.UrlWithStringQuery
protected clusterManager: ClusterManager
protected retryCounters: Map<string, number> = new Map()
protected router: Router
protected proxyServer: http.Server protected proxyServer: http.Server
protected router: Router
protected closed = false protected closed = false
protected retryCounters = new Map<string, number>()
constructor(port: number, clusterManager: ClusterManager) { constructor(public port: number, protected clusterManager: ClusterManager) {
this.port = port this.router = new Router();
this.clusterManager = clusterManager
this.router = new Router()
} }
public run() { public run() {
@ -34,7 +27,7 @@ export class LensProxy {
} }
public close() { public close() {
logger.info("Closing proxy server") logger.info(`Closing proxy server at port ${this.port}`);
this.proxyServer.close() this.proxyServer.close()
this.closed = true this.closed = true
} }
@ -77,9 +70,7 @@ export class LensProxy {
} }
}) })
proxy.on("error", (error, req, res, target) => { proxy.on("error", (error, req, res, target) => {
if(this.closed) { if (this.closed) return;
return
}
if (target) { if (target) {
logger.debug("Failed proxy to target: " + JSON.stringify(target)) logger.debug("Failed proxy to target: " + JSON.stringify(target))
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) { if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
@ -142,14 +133,13 @@ export class LensProxy {
return return
} }
const contextHandler = cluster.contextHandler const contextHandler = cluster.contextHandler
contextHandler.ensureServer().then(async () => { await contextHandler.ensureServer();
const proxyTarget = await this.getProxyTarget(req, contextHandler) const proxyTarget = await this.getProxyTarget(req, contextHandler)
if (proxyTarget) { if (proxyTarget) {
proxy.web(req, res, proxyTarget) proxy.web(req, res, proxyTarget)
} else { } else {
this.router.route(cluster, req, res) this.router.route(cluster, req, res)
} }
})
} }
protected async handleWsUpgrade(req: http.IncomingMessage, socket: Socket, head: Buffer) { protected async handleWsUpgrade(req: http.IncomingMessage, socket: Socket, head: Buffer) {

View File

@ -3,7 +3,7 @@ import { autobind, base64, EventEmitter, interval } from "../utils";
import { WebSocketApi } from "./websocket-api"; import { WebSocketApi } from "./websocket-api";
import { configStore } from "../config.store"; import { configStore } from "../config.store";
import isEqual from "lodash/isEqual" import isEqual from "lodash/isEqual"
import { apiPrefix, isDevelopment } from "../../common/vars"; import { isDevelopment } from "../../common/vars";
export enum TerminalChannels { export enum TerminalChannels {
STDIN = 0, STDIN = 0,

View File

@ -0,0 +1 @@
export * from "./workspaces"

View File

@ -1,33 +1,3 @@
.Workspaces { .Workspaces {
display: grid;
grid-template-areas: "draggable draggable" "menu lens-view" "bottom-bar bottom-bar";
grid-template-rows: auto 1fr min-content;
grid-template-columns: min-content 1fr;
height: 100%;
.draggable-top { }
@include set-draggable;
grid-area: draggable;
height: 25px;
}
#lens-view {
grid-area: lens-view;
}
.ClusterMenu {
grid-area: menu;
}
.WorkspacesBottomBar {
grid-area: bottom-bar;
}
}
.WorkspacesMenu {
border-radius: $radius;
.workspaces-title {
padding: $padding;
}
}

View File

@ -1,19 +1,12 @@
import "./workspaces.scss" import "./workspaces.scss"
import React from "react"; import React from "react";
import { ClustersMenu } from "./clusters-menu";
import { WorkspacesBottomBar } from "./bottom-bar";
// todo: support `workspaceId` in URL
export class Workspaces extends React.Component { export class Workspaces extends React.Component {
render() { render() {
return ( return (
<div className="Workspaces"> <div className="Workspaces">
<div className="draggable-top"/> Workspaces
<div id="lens-view"/>
<ClustersMenu/>
<WorkspacesBottomBar/>
</div> </div>
) );
} }
} }

View File

@ -1,15 +1,13 @@
import "./app.scss"; import "./app.scss";
import React from "react"; import React, { Fragment } from "react";
import { render } from "react-dom";
import { Redirect, Route, Router, Switch } from "react-router";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { I18nProvider } from '@lingui/react' import { i18nStore } from "../i18n";
import { _i18n, i18nStore } from "../i18n"; import { configStore } from "../config.store";
import { browserHistory } from "../navigation"; import { Terminal } from "./dock/terminal";
import { Redirect, Route, Switch } from "react-router";
import { Notifications } from "./notifications"; import { Notifications } from "./notifications";
import { NotFound } from "./+404"; import { NotFound } from "./+404";
import { configStore } from "../config.store";
import { UserManagement } from "./+user-management/user-management"; import { UserManagement } from "./+user-management/user-management";
import { ConfirmDialog } from "./confirm-dialog"; import { ConfirmDialog } from "./confirm-dialog";
import { usersManagementRoute } from "./+user-management/user-management.routes"; import { usersManagementRoute } from "./+user-management/user-management.routes";
@ -24,7 +22,6 @@ import { Cluster } from "./+cluster/cluster";
import { Config, configRoute } from "./+config"; import { Config, configRoute } from "./+config";
import { Events } from "./+events/events"; import { Events } from "./+events/events";
import { eventRoute } from "./+events"; import { eventRoute } from "./+events";
import { ErrorBoundary } from "./error-boundary";
import { Apps, appsRoute } from "./+apps"; import { Apps, appsRoute } from "./+apps";
import { KubeObjectDetails } from "./kube-object/kube-object-details"; import { KubeObjectDetails } from "./kube-object/kube-object-details";
import { AddRoleBindingDialog } from "./+user-management-roles-bindings"; import { AddRoleBindingDialog } from "./+user-management-roles-bindings";
@ -33,7 +30,6 @@ 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 { Terminal } from "./dock/terminal";
@observer @observer
export class App extends React.Component { export class App extends React.Component {
@ -43,42 +39,40 @@ export class App extends React.Component {
await i18nStore.init(); await i18nStore.init();
await configStore.init(); await configStore.init();
await Terminal.preloadFonts(); await Terminal.preloadFonts();
render(<App/>, App.rootElem);
} }
render() { render() {
const homeUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL(); const homeUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL();
return ( return (
<I18nProvider i18n={_i18n}> <Fragment>
<Router history={browserHistory}> <Switch>
<ErrorBoundary> <Switch>
<Switch> {/* todo: remove */}
<Switch> <Route children={() => <p className="info">App is running!</p>}/>
<Route component={Cluster} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/> <Route component={Cluster} {...clusterRoute}/>
<Route component={Workloads} {...workloadsRoute}/> <Route component={Nodes} {...nodesRoute}/>
<Route component={Config} {...configRoute}/> <Route component={Workloads} {...workloadsRoute}/>
<Route component={Network} {...networkRoute}/> <Route component={Config} {...configRoute}/>
<Route component={Storage} {...storageRoute}/> <Route component={Network} {...networkRoute}/>
<Route component={Namespaces} {...namespacesRoute}/> <Route component={Storage} {...storageRoute}/>
<Route component={Events} {...eventRoute}/> <Route component={Namespaces} {...namespacesRoute}/>
<Route component={CustomResources} {...crdRoute}/> <Route component={Events} {...eventRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/> <Route component={CustomResources} {...crdRoute}/>
<Route component={Apps} {...appsRoute}/> <Route component={UserManagement} {...usersManagementRoute}/>
<Redirect exact from="/" to={homeUrl}/> <Route component={Apps} {...appsRoute}/>
<Route path="*" component={NotFound}/> <Redirect exact from="/" to={homeUrl}/>
</Switch> <Route path="*" component={NotFound}/>
</Switch> </Switch>
<KubeObjectDetails/> </Switch>
<Notifications/> <KubeObjectDetails/>
<ConfirmDialog/> <Notifications/>
<KubeConfigDialog/> <ConfirmDialog/>
<AddRoleBindingDialog/> <KubeConfigDialog/>
<PodLogsDialog/> <AddRoleBindingDialog/>
<DeploymentScaleDialog/> <PodLogsDialog/>
</ErrorBoundary> <DeploymentScaleDialog/>
</Router> </Fragment>
</I18nProvider>
) )
} }
} }

View File

@ -1,4 +1,4 @@
.WorkspacesBottomBar { .BottomBar {
font-size: $font-size-small; font-size: $font-size-small;
background-color: #3d90ce; background-color: #3d90ce;
padding: $padding / 2 $padding; padding: $padding / 2 $padding;
@ -8,3 +8,11 @@
cursor: pointer; cursor: pointer;
} }
} }
#workspace-menu {
border-radius: $radius;
.workspaces-title {
padding: $padding;
}
}

View File

@ -13,26 +13,26 @@ import { workspaceStore } from "../../../common/workspace-store";
// todo: remove dummy actions + console.log // todo: remove dummy actions + console.log
@observer @observer
export class WorkspacesBottomBar extends React.Component { export class BottomBar extends React.Component {
@observable menuVisible = false; @observable menuVisible = false;
render() { render() {
const { currentWorkspace, workspacesList } = workspaceStore; const { currentWorkspace, workspacesList } = workspaceStore;
return ( return (
<div className="WorkspacesBottomBar flex gaps"> <div className="BottomBar flex gaps">
<div id="workspace" className="workspace flex align-center box right"> <div id="workspace" className="workspace flex align-center box right">
<Icon small material="layers"/> {currentWorkspace} <Icon small material="layers"/> {currentWorkspace}
</div> </div>
<Menu <Menu
usePortal usePortal
htmlFor="workspace" htmlFor="workspace"
className="WorkspacesMenu" id="workspace-menu"
isOpen={this.menuVisible} isOpen={this.menuVisible}
open={() => this.menuVisible = true} open={() => this.menuVisible = true}
close={() => this.menuVisible = false} close={() => this.menuVisible = false}
> >
<Link <Link
to="/workspaces" to="#"
className="workspaces-title" className="workspaces-title"
onClick={prevDefault(() => console.log('/navigate: workspaces page'))}> onClick={prevDefault(() => console.log('/navigate: workspaces page'))}>
<Trans>Workspaces</Trans> <Trans>Workspaces</Trans>

View File

@ -0,0 +1,30 @@
import React from "react";
import { observer } from "mobx-react";
import { ClusterId, clusterStore } from "../../../common/cluster-store";
import { WorkspaceId, workspaceStore } from "../../../common/workspace-store";
export const clusterContext = React.createContext(getClusterContext());
export interface ClusterContextValue {
workspaceId: WorkspaceId;
clusterId?: ClusterId;
}
export function getClusterContext(): ClusterContextValue {
return {
clusterId: clusterStore.activeCluster,
workspaceId: workspaceStore.currentWorkspace,
}
}
@observer
export class ClusterContext extends React.Component {
render() {
const { Provider } = clusterContext;
return (
<Provider value={getClusterContext()}>
{this.props.children}
</Provider>
)
}
}

View File

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

View File

@ -0,0 +1,18 @@
import "./cluster-manager.scss"
import React from "react";
import { ClustersMenu } from "./clusters-menu";
import { BottomBar } from "./bottom-bar";
export class ClusterManager extends React.Component {
render() {
const { children: lensView } = this.props;
return (
<div className="ClusterManager">
<div className="draggable-top"/>
<div id="lens-view">{lensView}</div>
<ClustersMenu/>
<BottomBar/>
</div>
)
}
}

View File

@ -43,7 +43,7 @@
color: white; color: white;
background: $colorSuccess; background: $colorSuccess;
font-weight: normal; font-weight: normal;
border-radius: 50%; border-radius: $radius;
padding: 0; padding: 0;
} }
} }

View File

@ -1,14 +1,13 @@
import type { Cluster } from "../../../main/cluster";
import "./clusters-menu.scss" import "./clusters-menu.scss"
import { remote } from "electron" import { remote } from "electron"
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import type { Cluster } from "../../../main/cluster";
import { userStore } from "../../../common/user-store"; import { userStore } from "../../../common/user-store";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { WorkspaceId } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
import { ClusterIcon } from "../+cluster-settings/cluster-icon"; import { ClusterIcon } from "../+cluster-settings/cluster-icon";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
@ -19,7 +18,6 @@ import { Badge } from "../badge";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
workspaceId?: WorkspaceId;
} }
@observer @observer
@ -54,9 +52,10 @@ export class ClustersMenu extends React.Component<Props> {
} }
render() { render() {
const { workspaceId, className } = this.props; const { className } = this.props;
const clusters = clusterStore.getByWorkspaceId(workspaceId);
const { newContexts } = userStore; const { newContexts } = userStore;
const { currentWorkspace } = workspaceStore;
const clusters = clusterStore.getByWorkspaceId(currentWorkspace);
return ( return (
<div className={cssNames("ClustersMenu flex gaps column", className)}> <div className={cssNames("ClustersMenu flex gaps column", className)}>
{clusters.map(cluster => { {clusters.map(cluster => {

View File

@ -0,0 +1 @@
export * from "./cluster-manager"

View File

@ -7,7 +7,7 @@ import { t, Trans } from "@lingui/macro";
import { Button } from "../button"; import { Button } from "../button";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { issuesTrackerUrl, slackUrl, buildVersion } from "../../../common/vars"; import { issuesTrackerUrl, slackUrl } from "../../../common/vars";
interface Props { interface Props {
} }
@ -45,7 +45,6 @@ export class ErrorBoundary extends React.Component<Props, State> {
<div className="ErrorBoundary flex column gaps"> <div className="ErrorBoundary flex column gaps">
<h5> <h5>
<Trans>App crash at <span className="contrast">{pageUrl}</span></Trans> <Trans>App crash at <span className="contrast">{pageUrl}</span></Trans>
{buildVersion && <p><Trans>Build version</Trans>: {buildVersion}</p>}
</h5> </h5>
<p> <p>
<Trans> <Trans>

View File

@ -21,6 +21,7 @@ export interface MenuProps {
isOpen?: boolean; isOpen?: boolean;
open(): void; open(): void;
close(): void; close(): void;
id?: string;
className?: string; className?: string;
htmlFor?: string; htmlFor?: string;
autoFocus?: boolean; autoFocus?: boolean;

View File

@ -1,38 +1,41 @@
// todo: remove when app.tsx re-used
import "./components/app.scss"
import "./theme.store";
import "../common/system-ca" import "../common/system-ca"
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import { render } from "react-dom";
import { Router } from "react-router"; import { Router } from "react-router";
import { userStore } from "../common/user-store"; import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store"; import { workspaceStore } from "../common/workspace-store";
import { clusterStore } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store";
import { Workspaces } from "./components/+workspaces/workspaces";
import { I18nProvider } from "@lingui/react"; import { I18nProvider } from "@lingui/react";
import { _i18n } from "./i18n";
import { browserHistory } from "./navigation"; import { browserHistory } from "./navigation";
import { _i18n } from "./i18n";
import { App } from "./components/app";
import { ClusterManager } from "./components/cluster-manager";
import { ErrorBoundary } from "./components/error-boundary";
class App extends React.Component { class LensApp extends React.Component {
static async init() { static async init() {
await Promise.all([ await Promise.all([
userStore.load(), userStore.load(),
workspaceStore.load(), workspaceStore.load(),
clusterStore.load(), clusterStore.load(),
]); ]);
ReactDOM.render(<App/>, document.getElementById("app"),) await App.init();
render(<LensApp/>, App.rootElem);
} }
render() { render() {
return ( return (
<I18nProvider i18n={_i18n}> <I18nProvider i18n={_i18n}>
<Router history={browserHistory}> <Router history={browserHistory}>
<Workspaces/> <ErrorBoundary>
<ClusterManager>
<App/>
</ClusterManager>
</ErrorBoundary>
</Router> </Router>
</I18nProvider> </I18nProvider>
) )
} }
} }
window.addEventListener("load", App.init); window.addEventListener("load", LensApp.init);