mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
apis / clusters-menu refactoring
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
4e88715b8d
commit
fbcb2fd281
@ -168,6 +168,7 @@
|
|||||||
"@types/node": "^12.12.45",
|
"@types/node": "^12.12.45",
|
||||||
"@types/proper-lockfile": "^4.1.1",
|
"@types/proper-lockfile": "^4.1.1",
|
||||||
"@types/tar": "^4.0.3",
|
"@types/tar": "^4.0.3",
|
||||||
|
"chalk": "^4.1.0",
|
||||||
"conf": "^7.0.1",
|
"conf": "^7.0.1",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
"electron-promise-ipc": "^2.1.0",
|
"electron-promise-ipc": "^2.1.0",
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||||
|
|
||||||
@computed get activeCluster(): Cluster {
|
@computed get activeCluster(): Cluster | null {
|
||||||
return this.getById(this.activeClusterId);
|
return this.getById(this.activeClusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,6 @@ export class Cluster implements ClusterModel {
|
|||||||
@observable kubeConfigPath: string;
|
@observable kubeConfigPath: string;
|
||||||
@observable apiUrl: string; // cluster server url
|
@observable apiUrl: string; // cluster server url
|
||||||
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
|
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
|
||||||
@observable kubeAuthProxyUrl: string; // auth-proxy to temp kube-config
|
|
||||||
@observable webContentUrl: string; // page content url for loading in renderer
|
@observable webContentUrl: string; // page content url for loading in renderer
|
||||||
@observable online: boolean;
|
@observable online: boolean;
|
||||||
@observable accessible: boolean;
|
@observable accessible: boolean;
|
||||||
@ -59,20 +58,21 @@ export class Cluster implements ClusterModel {
|
|||||||
async init(port: number) {
|
async init(port: number) {
|
||||||
try {
|
try {
|
||||||
this.contextHandler = new ContextHandler(this);
|
this.contextHandler = new ContextHandler(this);
|
||||||
const contextPort = await this.contextHandler.ensurePort();
|
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler);
|
||||||
this.kubeAuthProxyUrl = `http://127.0.0.1:${contextPort}`;
|
|
||||||
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
|
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
|
||||||
this.webContentUrl = `http://${this.id}.localhost:${port}`;
|
this.webContentUrl = `http://${this.id}.localhost:${port}`;
|
||||||
this.kubeconfigManager = new KubeconfigManager(this);
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
logger.info(`✅ ️Cluster(${this.id}) init success`, {
|
logger.info(`✅ ️Cluster init success`, {
|
||||||
|
id: this.id,
|
||||||
serverUrl: this.apiUrl,
|
serverUrl: this.apiUrl,
|
||||||
webContentUrl: this.webContentUrl,
|
webContentUrl: this.webContentUrl,
|
||||||
kubeProxyUrl: this.kubeProxyUrl,
|
kubeProxyUrl: this.kubeProxyUrl,
|
||||||
kubeAuthProxyUrl: this.kubeAuthProxyUrl,
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`💣 Cluster(${this.id}) init failed: ${err}`);
|
logger.error(`💣 Cluster init failed: ${err}`, {
|
||||||
|
id: this.id,
|
||||||
|
error: err,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,6 +68,11 @@ export class ContextHandler {
|
|||||||
return this.prometheusPath;
|
return this.prometheusPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async resolveAuthProxyUrl() {
|
||||||
|
const proxyPort = await this.ensurePort();
|
||||||
|
return `http://127.0.0.1:${proxyPort}`;
|
||||||
|
}
|
||||||
|
|
||||||
public async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
|
public async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
|
||||||
if (this.apiTarget && !isWatchRequest) {
|
if (this.apiTarget && !isWatchRequest) {
|
||||||
return this.apiTarget
|
return this.apiTarget
|
||||||
@ -81,19 +86,14 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
||||||
await this.ensurePort();
|
const proxyUrl = await this.resolveAuthProxyUrl();
|
||||||
return {
|
return {
|
||||||
|
target: proxyUrl + this.clusterUrl.path,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
headers: {
|
headers: {
|
||||||
"Host": this.clusterUrl.hostname,
|
"Host": this.clusterUrl.hostname,
|
||||||
},
|
},
|
||||||
target: {
|
|
||||||
protocol: "http://",
|
|
||||||
host: "127.0.0.1",
|
|
||||||
port: this.proxyPort,
|
|
||||||
path: this.clusterUrl.path
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { KubeConfig } from "@kubernetes/client-node";
|
import type { KubeConfig } from "@kubernetes/client-node";
|
||||||
import type { Cluster } from "./cluster"
|
import type { Cluster } from "./cluster"
|
||||||
|
import type { ContextHandler } from "./context-handler";
|
||||||
import { app } from "electron"
|
import { app } from "electron"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs-extra"
|
import fs from "fs-extra"
|
||||||
@ -10,18 +11,13 @@ export class KubeconfigManager {
|
|||||||
protected configDir = app.getPath("temp")
|
protected configDir = app.getPath("temp")
|
||||||
protected tempFile: string;
|
protected tempFile: string;
|
||||||
|
|
||||||
constructor(protected cluster: Cluster) {
|
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) {
|
||||||
if(!cluster.kubeAuthProxyUrl) {
|
|
||||||
throw new Error(`Cluster's auth proxy url must be initialized`)
|
|
||||||
}
|
|
||||||
if (!cluster.contextHandler.proxyPort) {
|
|
||||||
throw new Error("Context-handler proxy port must be resolved")
|
|
||||||
}
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init() {
|
protected async init() {
|
||||||
try {
|
try {
|
||||||
|
await this.contextHandler.ensurePort();
|
||||||
await this.createProxyKubeconfig();
|
await this.createProxyKubeconfig();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to created temp config for auth-proxy`, { err })
|
logger.error(`Failed to created temp config for auth-proxy`, { err })
|
||||||
@ -37,37 +33,38 @@ export class KubeconfigManager {
|
|||||||
* This way any user of the config does not need to know anything about the auth etc. details.
|
* This way any user of the config does not need to know anything about the auth etc. details.
|
||||||
*/
|
*/
|
||||||
protected async createProxyKubeconfig(): Promise<string> {
|
protected async createProxyKubeconfig(): Promise<string> {
|
||||||
const { configDir, cluster } = this;
|
const { configDir, cluster, contextHandler } = this;
|
||||||
const { contextName, kubeConfigPath, id } = cluster;
|
const { contextName, kubeConfigPath, id } = cluster;
|
||||||
const tempFile = path.join(configDir, `kubeconfig-${id}`);
|
const tempFile = path.join(configDir, `kubeconfig-${id}`);
|
||||||
const kubeConfig = loadConfig(kubeConfigPath);
|
const kubeConfig = loadConfig(kubeConfigPath);
|
||||||
const proxyUser = "proxy";
|
|
||||||
const proxyConfig: Partial<KubeConfig> = {
|
const proxyConfig: Partial<KubeConfig> = {
|
||||||
currentContext: contextName,
|
currentContext: contextName,
|
||||||
clusters: [
|
clusters: [
|
||||||
{
|
{
|
||||||
name: contextName,
|
name: contextName,
|
||||||
server: cluster.kubeAuthProxyUrl,
|
server: await contextHandler.resolveAuthProxyUrl(),
|
||||||
skipTLSVerify: undefined,
|
skipTLSVerify: undefined,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
users: [
|
users: [
|
||||||
{ name: proxyUser },
|
{ name: "proxy" },
|
||||||
],
|
],
|
||||||
contexts: [
|
contexts: [
|
||||||
{
|
{
|
||||||
user: proxyUser,
|
user: "proxy",
|
||||||
name: contextName,
|
name: contextName,
|
||||||
cluster: contextName,
|
cluster: contextName,
|
||||||
namespace: kubeConfig.getContextObject(contextName).namespace,
|
namespace: kubeConfig.getContextObject(contextName).namespace,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// write
|
||||||
const configYaml = dumpConfigYaml(proxyConfig);
|
const configYaml = dumpConfigYaml(proxyConfig);
|
||||||
fs.ensureDir(path.dirname(tempFile));
|
fs.ensureDir(path.dirname(tempFile));
|
||||||
fs.writeFileSync(tempFile, configYaml);
|
fs.writeFileSync(tempFile, configYaml);
|
||||||
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
|
|
||||||
this.tempFile = tempFile;
|
this.tempFile = tempFile;
|
||||||
|
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
|
||||||
return tempFile;
|
return tempFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,10 +42,13 @@ export class WindowManager {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
// auto-show active cluster view
|
// auto-show active cluster view
|
||||||
reaction(() => clusterStore.activeClusterId, clusterId => {
|
reaction(() => clusterStore.activeCluster, activeCluster => {
|
||||||
this.activateView(clusterId);
|
if (activeCluster) {
|
||||||
|
this.activateView(activeCluster.id);
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
|
delay: 250,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -65,10 +68,7 @@ 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) return;
|
||||||
logger.error(`Can't show a view for non-existing cluster(${clusterId})`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const activeView = this.activeView;
|
const activeView = this.activeView;
|
||||||
const isFresh = !this.getView(clusterId);
|
const isFresh = !this.getView(clusterId);
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export default migration({
|
|||||||
try {
|
try {
|
||||||
// take the embedded kubeconfig and dump it into a file
|
// take the embedded kubeconfig and dump it into a file
|
||||||
cluster.kubeConfigPath = writeEmbeddedKubeConfig(cluster.id, cluster.kubeConfig)
|
cluster.kubeConfigPath = writeEmbeddedKubeConfig(cluster.id, cluster.kubeConfig)
|
||||||
|
delete cluster.kubeConfig;
|
||||||
migratedClusters.push(cluster)
|
migratedClusters.push(cluster)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Failed to migrate Kubeconfig for cluster "${cluster.id}"`, error)
|
log(`Failed to migrate Kubeconfig for cluster "${cluster.id}"`, error)
|
||||||
|
|||||||
@ -1,7 +1,19 @@
|
|||||||
.ClusterIcon {
|
.ClusterIcon {
|
||||||
position: relative;
|
|
||||||
--size: 40px;
|
--size: 40px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
opacity: .75;
|
||||||
|
border-radius: $radius;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.interactive {
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 0 $radius #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> img {
|
> img {
|
||||||
width: var(--size);
|
width: var(--size);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import "./cluster-icon.scss"
|
|||||||
|
|
||||||
import React, { DOMAttributes } from "react";
|
import React, { DOMAttributes } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Hashicon, HashiconProps } from "@emeraldpay/hashicon-react";
|
import { Params as HashiconParams } from "@emeraldpay/hashicon";
|
||||||
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
@ -11,12 +12,13 @@ interface Props extends DOMAttributes<HTMLElement> {
|
|||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
errorClass?: IClassName;
|
errorClass?: IClassName;
|
||||||
showErrorCount?: boolean;
|
showErrors?: boolean;
|
||||||
options?: HashiconProps["options"]
|
interactive?: boolean;
|
||||||
|
options?: HashiconParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps: Partial<Props> = {
|
const defaultProps: Partial<Props> = {
|
||||||
showErrorCount: true,
|
showErrors: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -24,14 +26,17 @@ export class ClusterIcon extends React.Component<Props> {
|
|||||||
static defaultProps = defaultProps as object;
|
static defaultProps = defaultProps as object;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, cluster, showErrorCount, errorClass, options, children, ...elemProps } = this.props;
|
const { className: cName, cluster, showErrors, errorClass, options, interactive, children, ...elemProps } = this.props;
|
||||||
const { isAdmin, eventCount, preferences } = cluster;
|
const { isAdmin, eventCount, preferences } = cluster;
|
||||||
const { clusterName, icon } = preferences;
|
const { clusterName, icon } = preferences;
|
||||||
|
const className = cssNames("ClusterIcon flex inline", cName, {
|
||||||
|
interactive: interactive || !!this.props.onClick,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("ClusterIcon flex inline", className)} {...elemProps}>
|
<div {...elemProps} className={className}>
|
||||||
{icon && <img src={icon} alt={clusterName}/>}
|
{icon && <img src={icon} alt={clusterName}/>}
|
||||||
{!icon && <Hashicon value={clusterName} options={options}/>}
|
{!icon && <Hashicon value={clusterName} options={options}/>}
|
||||||
{showErrorCount && isAdmin && eventCount > 0 && (
|
{showErrors && isAdmin && eventCount > 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
className={cssNames("events-count", errorClass)}
|
className={cssNames("events-count", errorClass)}
|
||||||
label={eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount}
|
label={eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
.ClustersMenu {
|
.ClustersMenu {
|
||||||
@include hidden-scrollbar;
|
@include hidden-scrollbar;
|
||||||
$menuBgc: #252729;
|
|
||||||
|
--flex-gap: #{$padding * 2};
|
||||||
|
--menu-bgc: #252729;
|
||||||
|
|
||||||
padding: $padding * 1.5;
|
padding: $padding * 1.5;
|
||||||
background: $menuBgc;
|
background: var(--menu-bgc);
|
||||||
|
|
||||||
> * {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-cluster {
|
.add-cluster {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -15,7 +13,7 @@
|
|||||||
.Icon.add {
|
.Icon.add {
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
padding: $padding / 3;
|
padding: $padding / 3;
|
||||||
color: $menuBgc !important;
|
color: var(--menu-bgc) !important;
|
||||||
background: white !important;
|
background: white !important;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@ -63,8 +63,8 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<ClusterIcon
|
<ClusterIcon
|
||||||
key={cluster.id}
|
key={cluster.id}
|
||||||
|
showErrors={true}
|
||||||
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)}
|
||||||
|
|||||||
@ -3442,6 +3442,14 @@ chalk@^4.0.0:
|
|||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
chalk@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
|
||||||
|
integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.1.0"
|
||||||
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
char-regex@^1.0.2:
|
char-regex@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user