mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
add-cluster -- part 2
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
a0831b3283
commit
33d3181113
@ -1,11 +1,21 @@
|
|||||||
import { ipcRenderer } from "electron";
|
|
||||||
import type { WorkspaceId } from "./workspace-store";
|
import type { WorkspaceId } from "./workspace-store";
|
||||||
|
import path from "path";
|
||||||
|
import filenamify from "filenamify";
|
||||||
|
import { app, ipcRenderer } from "electron";
|
||||||
|
import { copyFile, ensureDir, unlink } from "fs-extra";
|
||||||
import { action, computed, observable, toJS } from "mobx";
|
import { action, computed, observable, toJS } from "mobx";
|
||||||
import { v4 as uuid } from "uuid"
|
import { appProto } from "./vars";
|
||||||
import { BaseStore } from "./base-store";
|
import { BaseStore } from "./base-store";
|
||||||
import { Cluster, ClusterState } from "../main/cluster";
|
import { Cluster, ClusterState } from "../main/cluster";
|
||||||
import migrations from "../migrations/cluster-store"
|
import migrations from "../migrations/cluster-store"
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
|
import { tracker } from "./tracker";
|
||||||
|
|
||||||
|
export interface ClusterIconUpload {
|
||||||
|
clusterId: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClusterStoreModel {
|
export interface ClusterStoreModel {
|
||||||
activeCluster?: ClusterId; // last opened cluster
|
activeCluster?: ClusterId; // last opened cluster
|
||||||
@ -42,6 +52,10 @@ export interface ClusterPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||||
|
static get iconsDir() {
|
||||||
|
return path.join(app.getPath("userData"), "icons");
|
||||||
|
}
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super({
|
super({
|
||||||
configName: "lens-cluster-store",
|
configName: "lens-cluster-store",
|
||||||
@ -81,9 +95,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
addCluster(model: ClusterModel): Cluster {
|
addCluster(model: ClusterModel): Cluster {
|
||||||
const id = model.id || uuid();
|
const cluster = new Cluster(model);
|
||||||
const cluster = new Cluster({ ...model, id })
|
this.clusters.set(model.id, cluster);
|
||||||
this.clusters.set(id, cluster);
|
|
||||||
return cluster;
|
return cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +115,30 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
protected async uploadClusterIcon({ clusterId, ...upload }: ClusterIconUpload): Promise<string> {
|
||||||
|
const cluster = this.getById(clusterId);
|
||||||
|
if (cluster) {
|
||||||
|
tracker.event("cluster", "upload-icon");
|
||||||
|
const fileDest = path.join(ClusterStore.iconsDir, filenamify(cluster.contextName + "-" + upload.name))
|
||||||
|
await ensureDir(path.dirname(fileDest));
|
||||||
|
await copyFile(upload.path, fileDest)
|
||||||
|
cluster.preferences.icon = `${appProto}:///icons/${fileDest}`
|
||||||
|
return cluster.preferences.icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
protected resetClusterIcon(clusterId: ClusterId) {
|
||||||
|
const cluster = this.getById(clusterId);
|
||||||
|
if (cluster) {
|
||||||
|
tracker.event("cluster", "reset-icon")
|
||||||
|
const iconPath = path.join(ClusterStore.iconsDir, path.basename(cluster.preferences.icon));
|
||||||
|
unlink(iconPath).catch(() => null); // remove file
|
||||||
|
delete cluster.preferences.icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
|
protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) {
|
||||||
const currentClusters = this.clusters.toJS();
|
const currentClusters = this.clusters.toJS();
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
// IPC messages (all channels)
|
|
||||||
// All values must be unique
|
|
||||||
|
|
||||||
export enum ClusterIpcMessage {
|
|
||||||
ADD = "cluster-add",
|
|
||||||
STOP = "cluster-stop",
|
|
||||||
REMOVE = "cluster-remove",
|
|
||||||
REMOVE_WORKSPACE = "cluster-remove-all-from-workspace",
|
|
||||||
FEATURE_INSTALL = "cluster-feature-install",
|
|
||||||
FEATURE_UPGRADE = "cluster-feature-upgrade",
|
|
||||||
FEATURE_REMOVE = "cluster-feature-remove",
|
|
||||||
ICON_SAVE = "cluster-icon-save",
|
|
||||||
ICON_RESET = "cluster-icon-reset",
|
|
||||||
}
|
|
||||||
@ -141,11 +141,12 @@ export function getNodeWarningConditions(node: V1Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write kubeconfigs to "embedded" store, i.e. "/Users/ixrock/Library/Application Support/Lens/kubeconfigs"
|
// Write kubeconfigs to "embedded" store, i.e. "/Users/ixrock/Library/Application Support/Lens/kubeconfigs"
|
||||||
export function saveConfigToAppFiles(clusterId: string, kubeConfig: string): string {
|
export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig | string): string {
|
||||||
const userData = (app || remote.app).getPath("userData");
|
const userData = (app || remote.app).getPath("userData");
|
||||||
const kubeConfigFile = path.join(userData, `kubeconfigs/${clusterId}`)
|
const kubeConfigFile = path.join(userData, `kubeconfigs/${clusterId}`)
|
||||||
|
const kubeConfigContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
|
||||||
|
|
||||||
ensureDirSync(path.dirname(kubeConfigFile));
|
ensureDirSync(path.dirname(kubeConfigFile));
|
||||||
writeFileSync(kubeConfigFile, kubeConfig);
|
writeFileSync(kubeConfigFile, kubeConfigContents);
|
||||||
return kubeConfigFile;
|
return kubeConfigFile;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +1,12 @@
|
|||||||
import { app } from "electron"
|
import type http from "http"
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import path from "path"
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import http from "http"
|
import { ClusterId, clusterStore } from "../common/cluster-store"
|
||||||
import { copyFile, ensureDir } from "fs-extra"
|
import { handleMessage } from "../common/ipc";
|
||||||
import filenamify from "filenamify"
|
|
||||||
import { apiKubePrefix, appProto } from "../common/vars";
|
|
||||||
import { ClusterId, ClusterModel, clusterStore } from "../common/cluster-store"
|
|
||||||
import { handleMessages } from "../common/ipc";
|
|
||||||
import { ClusterIpcMessage } from "../common/ipc-messages";
|
|
||||||
import { tracker } from "../common/tracker";
|
import { tracker } from "../common/tracker";
|
||||||
import { validateConfig } from "../common/kube-helpers";
|
import { Cluster, ClusterIpcEvent } from "./cluster"
|
||||||
import { Cluster } from "./cluster"
|
|
||||||
import { FeatureInstallRequest } from "./feature";
|
|
||||||
import logger from "./logger"
|
|
||||||
|
|
||||||
export interface ClusterIconUpload {
|
|
||||||
clusterId: string;
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClusterManager {
|
export class ClusterManager {
|
||||||
static get clusterIconDir() {
|
|
||||||
return path.join(app.getPath("userData"), "icons");
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(public readonly port: number) {
|
constructor(public readonly port: number) {
|
||||||
// auto-init clusters
|
// auto-init clusters
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
@ -32,13 +14,15 @@ export class ClusterManager {
|
|||||||
.filter(cluster => !cluster.initialized)
|
.filter(cluster => !cluster.initialized)
|
||||||
.forEach(cluster => cluster.init(port));
|
.forEach(cluster => cluster.init(port));
|
||||||
});
|
});
|
||||||
|
|
||||||
// auto-stop removed clusters
|
// auto-stop removed clusters
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
clusterStore.removedClusters.forEach(cluster => cluster.stop());
|
clusterStore.removedClusters.forEach(cluster => cluster.stop());
|
||||||
clusterStore.removedClusters.clear();
|
clusterStore.removedClusters.clear();
|
||||||
});
|
});
|
||||||
// listen ipc-events
|
|
||||||
ClusterManager.ipcListen(this);
|
// listen ipc-events which could be handled *only* in main-process (nodeIntegration=true)
|
||||||
|
handleMessage(ClusterIpcEvent.STOP, this.stopCluster.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
@ -51,40 +35,11 @@ export class ClusterManager {
|
|||||||
return clusterStore.getById(id);
|
return clusterStore.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async addCluster(clusterModel: ClusterModel): Promise<Cluster> {
|
|
||||||
tracker.event("cluster", "add");
|
|
||||||
try {
|
|
||||||
await validateConfig(clusterModel.kubeConfigPath);
|
|
||||||
return clusterStore.addCluster(clusterModel);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[CLUSTER-MANAGER]: add cluster error ${JSON.stringify(error)}`)
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected stopCluster(clusterId: ClusterId) {
|
protected stopCluster(clusterId: ClusterId) {
|
||||||
tracker.event("cluster", "stop");
|
tracker.event("cluster", "stop");
|
||||||
this.getCluster(clusterId)?.stop();
|
this.getCluster(clusterId)?.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected removeAllByWorkspace(workspaceId: string) {
|
|
||||||
tracker.event("cluster", "remove-workspace");
|
|
||||||
const clusters = clusterStore.getByWorkspaceId(workspaceId);
|
|
||||||
clusters.forEach(cluster => {
|
|
||||||
this.removeCluster(cluster.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected removeCluster(clusterId: string): Cluster {
|
|
||||||
tracker.event("cluster", "remove");
|
|
||||||
const cluster = this.getCluster(clusterId);
|
|
||||||
if (cluster) {
|
|
||||||
cluster.stop()
|
|
||||||
clusterStore.removeById(cluster.id);
|
|
||||||
return cluster;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
||||||
let cluster: Cluster = null
|
let cluster: Cluster = null
|
||||||
|
|
||||||
@ -105,61 +60,4 @@ export class ClusterManager {
|
|||||||
|
|
||||||
return cluster;
|
return cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async uploadClusterIcon({ clusterId, name: fileName, path: src }: ClusterIconUpload): Promise<string> {
|
|
||||||
const cluster = this.getCluster(clusterId);
|
|
||||||
if (cluster) {
|
|
||||||
tracker.event("cluster", "upload-icon");
|
|
||||||
await ensureDir(ClusterManager.clusterIconDir)
|
|
||||||
fileName = filenamify(cluster.contextName + "-" + fileName)
|
|
||||||
const dest = path.join(ClusterManager.clusterIconDir, fileName)
|
|
||||||
await copyFile(src, dest)
|
|
||||||
cluster.preferences.icon = `${appProto}:///icons/${fileName}`
|
|
||||||
return cluster.preferences.icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: remove current icon file ?
|
|
||||||
protected resetClusterIcon(clusterId: ClusterId) {
|
|
||||||
const cluster = this.getCluster(clusterId);
|
|
||||||
if (cluster) {
|
|
||||||
tracker.event("cluster", "reset-icon")
|
|
||||||
cluster.preferences.icon = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async installFeature({ clusterId, name, config }: FeatureInstallRequest) {
|
|
||||||
tracker.event("cluster", "install-feature")
|
|
||||||
return this.getCluster(clusterId)?.installFeature(name, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async upgradeFeature({ clusterId, name, config }: FeatureInstallRequest) {
|
|
||||||
tracker.event("cluster", "upgrade-feature")
|
|
||||||
return this.getCluster(clusterId)?.upgradeFeature(name, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async uninstallFeature({ clusterId, name }: FeatureInstallRequest) {
|
|
||||||
tracker.event("cluster", "uninstall-feature")
|
|
||||||
return this.getCluster(clusterId)?.uninstallFeature(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ipcListen(clusterManager: ClusterManager) {
|
|
||||||
const handlers = {
|
|
||||||
[ClusterIpcMessage.ADD]: clusterManager.addCluster,
|
|
||||||
[ClusterIpcMessage.STOP]: clusterManager.stopCluster,
|
|
||||||
[ClusterIpcMessage.REMOVE]: clusterManager.removeCluster,
|
|
||||||
[ClusterIpcMessage.REMOVE_WORKSPACE]: clusterManager.removeAllByWorkspace,
|
|
||||||
[ClusterIpcMessage.FEATURE_INSTALL]: clusterManager.installFeature,
|
|
||||||
[ClusterIpcMessage.FEATURE_UPGRADE]: clusterManager.upgradeFeature,
|
|
||||||
[ClusterIpcMessage.FEATURE_REMOVE]: clusterManager.uninstallFeature,
|
|
||||||
[ClusterIpcMessage.ICON_SAVE]: clusterManager.uploadClusterIcon,
|
|
||||||
[ClusterIpcMessage.ICON_RESET]: clusterManager.removeCluster,
|
|
||||||
};
|
|
||||||
Object.entries(handlers).forEach(([key, handler]) => {
|
|
||||||
handlers[key as keyof typeof handlers] = handler.bind(clusterManager);
|
|
||||||
})
|
|
||||||
handleMessages(handlers, {
|
|
||||||
timeout: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,11 @@ import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from ".
|
|||||||
import request, { RequestPromiseOptions } from "request-promise-native"
|
import request, { RequestPromiseOptions } from "request-promise-native"
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
|
|
||||||
enum ClusterStatus {
|
export enum ClusterIpcEvent {
|
||||||
|
STOP = "cluster:stop",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ClusterStatus {
|
||||||
AccessGranted = 2,
|
AccessGranted = 2,
|
||||||
AccessDenied = 1,
|
AccessDenied = 1,
|
||||||
Offline = 0
|
Offline = 0
|
||||||
@ -310,7 +314,7 @@ export class Cluster implements ClusterModel {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// serializable full-featured state of the cluster
|
// serializable cluster-info for push-notifications
|
||||||
getState(): ClusterState {
|
getState(): ClusterState {
|
||||||
const state: ClusterState = {
|
const state: ClusterState = {
|
||||||
...this.toJSON(),
|
...this.toJSON(),
|
||||||
|
|||||||
@ -20,6 +20,12 @@
|
|||||||
border-left: 1px solid #353a3e;
|
border-left: 1px solid #353a3e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
border-radius: $radius;
|
||||||
|
padding: $padding;
|
||||||
|
background-color: pink;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $colorInfo;
|
color: $colorInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,39 +1,101 @@
|
|||||||
import "./add-cluster.scss"
|
import "./add-cluster.scss"
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs-extra";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { computed, observable } from "mobx";
|
import { computed, observable } from "mobx";
|
||||||
import path from "path";
|
|
||||||
import { Select, SelectOption } from "../select";
|
import { Select, SelectOption } from "../select";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { AceEditor } from "../ace-editor";
|
import { AceEditor } from "../ace-editor";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
|
import { loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers";
|
||||||
|
import { tracker } from "../../../common/tracker";
|
||||||
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
|
import { workspaceStore } from "../../../common/workspace-store";
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class AddCluster extends React.Component {
|
export class AddCluster extends React.Component {
|
||||||
readonly customContext = "custom"
|
readonly custom: any = "custom"
|
||||||
readonly kubeConfigFile = path.join(process.env.HOME, '.kube', 'config');
|
@observable.ref clusterConfig: KubeConfig;
|
||||||
|
@observable.ref kubeConfig: KubeConfig; // local ~/.kube/config (if available)
|
||||||
|
|
||||||
|
@observable isWaiting = false
|
||||||
@observable showSettings = false
|
@observable showSettings = false
|
||||||
@observable clusterContext = ""
|
|
||||||
@observable error = ""
|
@observable error = ""
|
||||||
@observable proxyServerUrl = ""
|
@observable proxyServer = ""
|
||||||
@observable customConfig = ""
|
@observable customConfig = ""
|
||||||
|
|
||||||
// todo: mark new contexts with badge
|
async componentDidMount() {
|
||||||
@computed get clusterOptions(): SelectOption[] {
|
const kubeConfig = await this.readLocalKubeConfig();
|
||||||
return [
|
if (kubeConfig) {
|
||||||
{
|
this.kubeConfig = loadConfig(kubeConfig)
|
||||||
label: <Trans>Custom..</Trans>,
|
this.customConfig = kubeConfig
|
||||||
value: this.customContext,
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo
|
async readLocalKubeConfig(): Promise<string> {
|
||||||
addCluster = () => {
|
const localPath = path.join(process.env.HOME, '.kube', 'config');
|
||||||
console.log('add new cluster')
|
return fs.readFile(localPath, "utf8").catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get isCustom() {
|
||||||
|
return this.clusterConfig === this.custom;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get clusterOptions() {
|
||||||
|
const options: SelectOption<KubeConfig>[] = [];
|
||||||
|
if (this.kubeConfig) {
|
||||||
|
splitConfig(this.kubeConfig).forEach(kubeConfig => {
|
||||||
|
const contextName = kubeConfig.getCurrentContext();
|
||||||
|
const isNew = false; // fixme: detect new context since last visit
|
||||||
|
options.push({
|
||||||
|
value: kubeConfig,
|
||||||
|
label: <>
|
||||||
|
{contextName}
|
||||||
|
{isNew && <span className="new"> <Trans>(new)</Trans></span>}
|
||||||
|
</>,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
options.push({
|
||||||
|
label: <Trans>Custom..</Trans>,
|
||||||
|
value: this.custom,
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
addCluster = async () => {
|
||||||
|
tracker.event("cluster", "add");
|
||||||
|
const { clusterConfig, customConfig, proxyServer } = this;
|
||||||
|
const clusterId = uuid();
|
||||||
|
try {
|
||||||
|
const config = this.isCustom ? loadConfig(customConfig) : clusterConfig;
|
||||||
|
if (!config) {
|
||||||
|
this.error = "Please select kubeconfig"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.error = ""
|
||||||
|
this.isWaiting = true
|
||||||
|
validateConfig(config);
|
||||||
|
clusterStore.addCluster({
|
||||||
|
id: clusterId,
|
||||||
|
kubeConfigPath: saveConfigToAppFiles(clusterId, config),
|
||||||
|
workspace: workspaceStore.currentWorkspaceId,
|
||||||
|
contextName: config.currentContext,
|
||||||
|
preferences: {
|
||||||
|
clusterName: config.currentContext,
|
||||||
|
httpsProxy: proxyServer || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
this.error = String(err);
|
||||||
|
} finally {
|
||||||
|
this.isWaiting = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -43,9 +105,9 @@ export class AddCluster extends React.Component {
|
|||||||
<h2><Trans>Add Cluster</Trans></h2>
|
<h2><Trans>Add Cluster</Trans></h2>
|
||||||
<Select
|
<Select
|
||||||
placeholder={<Trans>Select kubeconfig</Trans>}
|
placeholder={<Trans>Select kubeconfig</Trans>}
|
||||||
value={this.clusterContext}
|
value={this.clusterConfig}
|
||||||
options={this.clusterOptions}
|
options={this.clusterOptions}
|
||||||
onChange={({ value }: SelectOption) => this.clusterContext = value}
|
onChange={({ value }: SelectOption) => this.clusterConfig = value}
|
||||||
/>
|
/>
|
||||||
<div className="cluster-settings">
|
<div className="cluster-settings">
|
||||||
<a href="#" onClick={() => this.showSettings = !this.showSettings}>
|
<a href="#" onClick={() => this.showSettings = !this.showSettings}>
|
||||||
@ -57,15 +119,15 @@ export class AddCluster extends React.Component {
|
|||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder={_i18n._(t`A HTTP proxy server URL (format: http://<address>:<port>)`)}
|
placeholder={_i18n._(t`A HTTP proxy server URL (format: http://<address>:<port>)`)}
|
||||||
value={this.proxyServerUrl}
|
value={this.proxyServer}
|
||||||
onChange={value => this.proxyServerUrl = value}
|
onChange={value => this.proxyServer = value}
|
||||||
/>
|
/>
|
||||||
<small className="hint">
|
<small className="hint">
|
||||||
<Trans>HTTP Proxy server. Used for communicating with Kubernetes API.</Trans>
|
<Trans>HTTP Proxy server. Used for communicating with Kubernetes API.</Trans>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{this.clusterContext === this.customContext && (
|
{this.isCustom && (
|
||||||
<div className="custom-kubeconfig flex column gaps box grow">
|
<div className="custom-kubeconfig flex column gaps box grow">
|
||||||
<p>Kubeconfig:</p>
|
<p>Kubeconfig:</p>
|
||||||
<AceEditor
|
<AceEditor
|
||||||
@ -76,11 +138,15 @@ export class AddCluster extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{this.error && (
|
||||||
|
<div className="error">{this.error}</div>
|
||||||
|
)}
|
||||||
<div className="actions-panel">
|
<div className="actions-panel">
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
label={<Trans>Add cluster</Trans>}
|
label={<Trans>Add cluster</Trans>}
|
||||||
onClick={this.addCluster}
|
onClick={this.addCluster}
|
||||||
|
waiting={this.isWaiting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.ClusterIcon {
|
.ClusterIcon {
|
||||||
--size: 40px;
|
--size: 37px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
opacity: .75;
|
opacity: .75;
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
.ClustersMenu {
|
.ClustersMenu {
|
||||||
--flex-gap: #{$padding * 2};
|
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: var(--flex-gap);
|
text-align: center;
|
||||||
|
padding: $padding * 2 $padding;
|
||||||
background: var(--clusters-menu-bgc);
|
background: var(--clusters-menu-bgc);
|
||||||
|
|
||||||
> .startup-tooltip {
|
> .startup-tooltip {
|
||||||
@ -35,14 +34,17 @@
|
|||||||
|
|
||||||
> .clusters {
|
> .clusters {
|
||||||
@include hidden-scrollbar;
|
@include hidden-scrollbar;
|
||||||
|
--flex-gap: #{$padding * 2};
|
||||||
|
padding: $padding;
|
||||||
|
|
||||||
.is-mac & {
|
.is-mac & {
|
||||||
margin-top: $padding;
|
margin-top: $padding * 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .add-cluster {
|
> .add-cluster {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-top: $padding;
|
||||||
|
|
||||||
.Icon {
|
.Icon {
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
const showStartupHint = this.showHint && isLanding && noClusters;
|
const showStartupHint = this.showHint && isLanding && noClusters;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cssNames("ClustersMenu flex gaps column", className)}
|
className={cssNames("ClustersMenu flex column", className)}
|
||||||
onMouseEnter={() => this.showHint = false}
|
onMouseEnter={() => this.showHint = false}
|
||||||
>
|
>
|
||||||
{showStartupHint && (
|
{showStartupHint && (
|
||||||
@ -87,7 +87,7 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="clusters">
|
<div className="clusters flex column gaps">
|
||||||
{clusters.map(cluster => {
|
{clusters.map(cluster => {
|
||||||
return (
|
return (
|
||||||
<ClusterIcon
|
<ClusterIcon
|
||||||
|
|||||||
@ -92,11 +92,9 @@
|
|||||||
|
|
||||||
@mixin set-draggable($is-draggable: true) {
|
@mixin set-draggable($is-draggable: true) {
|
||||||
@if ($is-draggable) {
|
@if ($is-draggable) {
|
||||||
cursor: move;
|
-webkit-user-select: none;
|
||||||
-webkit-user-drag: auto;
|
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
} @else {
|
} @else {
|
||||||
-webkit-user-drag: none;
|
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user