mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
allow to remove cluster from icon's context-menu, random fixes
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
33d3181113
commit
e11d8582f1
@ -65,7 +65,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
if (ipcRenderer) {
|
||||
ipcRenderer.on("cluster:state", (event, clusterState: ClusterState) => {
|
||||
this.applyWithoutSync(() => {
|
||||
logger.info(`[CLUSTER-STORE]: received cluster(${clusterState.id}) update`, clusterState);
|
||||
logger.debug(`[CLUSTER-STORE]: received state update for cluster=${clusterState.id}`, clusterState);
|
||||
const cluster = this.getById(clusterState.id);
|
||||
if (cluster) cluster.updateModel(clusterState)
|
||||
})
|
||||
@ -85,6 +85,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return Array.from(this.clusters.values());
|
||||
}
|
||||
|
||||
hasContext(name: string) {
|
||||
return this.clustersList.some(cluster => cluster.contextName === name);
|
||||
}
|
||||
|
||||
getById(id: ClusterId): Cluster {
|
||||
return this.clusters.get(id);
|
||||
}
|
||||
@ -94,18 +98,23 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
addCluster(model: ClusterModel): Cluster {
|
||||
async addCluster(model: ClusterModel, activate = true): Promise<Cluster> {
|
||||
const cluster = new Cluster(model);
|
||||
this.clusters.set(model.id, cluster);
|
||||
if (activate) this.activeClusterId = model.id;
|
||||
return cluster;
|
||||
}
|
||||
|
||||
@action
|
||||
removeById(clusterId: ClusterId): void {
|
||||
if (this.activeClusterId === clusterId) {
|
||||
this.activeClusterId = null;
|
||||
async removeById(clusterId: ClusterId) {
|
||||
const cluster = this.getById(clusterId);
|
||||
if (cluster) {
|
||||
this.clusters.delete(clusterId);
|
||||
if (this.activeClusterId === clusterId) {
|
||||
this.activeClusterId = null;
|
||||
}
|
||||
unlink(cluster.kubeConfigPath).catch(() => null);
|
||||
}
|
||||
this.clusters.delete(clusterId);
|
||||
}
|
||||
|
||||
@action
|
||||
@ -116,7 +125,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
protected async uploadClusterIcon({ clusterId, ...upload }: ClusterIconUpload): Promise<string> {
|
||||
protected async uploadIcon({ clusterId, ...upload }: ClusterIconUpload): Promise<string> {
|
||||
const cluster = this.getById(clusterId);
|
||||
if (cluster) {
|
||||
tracker.event("cluster", "upload-icon");
|
||||
@ -129,7 +138,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
protected resetClusterIcon(clusterId: ClusterId) {
|
||||
protected resetIcon(clusterId: ClusterId) {
|
||||
const cluster = this.getById(clusterId);
|
||||
if (cluster) {
|
||||
tracker.event("cluster", "reset-icon")
|
||||
@ -167,7 +176,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
this.clusters.replace(newClusters);
|
||||
this.removedClusters.replace(removedClusters);
|
||||
|
||||
// "auto-select" first cluster if not available or invalid from config file
|
||||
// "auto-select" first cluster if available
|
||||
if (!this.activeClusterId && newClusters.size) {
|
||||
this.activeClusterId = Array.from(newClusters.values())[0].id;
|
||||
}
|
||||
|
||||
@ -31,14 +31,14 @@ export function sendMessage({ channel, webContentId, filter, args = [] }: IpcMes
|
||||
}
|
||||
views.forEach(webContent => {
|
||||
const type = webContent.getType();
|
||||
logger.info(`[IPC]: sending message "${channel}" to ${type}=${webContent.id}`);
|
||||
logger.debug(`[IPC]: sending message "${channel}" to ${type}=${webContent.id}`, { args });
|
||||
webContent.send(channel, ...[args].flat());
|
||||
})
|
||||
}
|
||||
|
||||
// todo: support timeout + merge with sendMessage?
|
||||
export async function invokeMessage<T extends any[], R = any>(channel: IpcChannel, ...args: T): Promise<R> {
|
||||
logger.debug(`[IPC]: invoke channel "${channel}"`, args);
|
||||
logger.debug(`[IPC]: invoke channel "${channel}"`, { args });
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ export async function invokeMessage<T extends any[], R = any>(channel: IpcChanne
|
||||
export function handleMessage<T extends any[]>(channel: IpcChannel, handler: IpcMessageHandler<T>, options: IpcHandleOpts = {}) {
|
||||
const { timeout = 0 } = options;
|
||||
ipcMain.handle(channel, async (event, ...args: T) => {
|
||||
logger.info(`[IPC]: handle "${channel}"`, { event, args });
|
||||
logger.debug(`[IPC]: handle "${channel}"`, { args });
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let timerId;
|
||||
if (timeout) {
|
||||
@ -60,7 +60,7 @@ export function handleMessage<T extends any[]>(channel: IpcChannel, handler: Ipc
|
||||
clearTimeout(timerId);
|
||||
return result;
|
||||
} catch (err) {
|
||||
logger.debug(`[IPC]: handling "${channel}" error`, err);
|
||||
logger.debug(`[IPC]: handling "${channel}" error`, { err });
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,23 +5,31 @@ import { ClusterId, clusterStore } from "../common/cluster-store"
|
||||
import { handleMessage } from "../common/ipc";
|
||||
import { tracker } from "../common/tracker";
|
||||
import { Cluster, ClusterIpcEvent } from "./cluster"
|
||||
import logger from "./logger";
|
||||
|
||||
export class ClusterManager {
|
||||
constructor(public readonly port: number) {
|
||||
// auto-init clusters
|
||||
autorun(() => {
|
||||
clusterStore.clustersList
|
||||
.filter(cluster => !cluster.initialized)
|
||||
.forEach(cluster => cluster.init(port));
|
||||
clusterStore.clusters.forEach(cluster => {
|
||||
if (cluster.initialized) return;
|
||||
cluster.init(port);
|
||||
logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta());
|
||||
});
|
||||
});
|
||||
|
||||
// auto-stop removed clusters
|
||||
autorun(() => {
|
||||
clusterStore.removedClusters.forEach(cluster => cluster.stop());
|
||||
clusterStore.removedClusters.clear();
|
||||
const { removedClusters } = clusterStore;
|
||||
const meta = Array.from(removedClusters.values()).map(cluster => cluster.getMeta());
|
||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
||||
removedClusters.forEach(cluster => cluster.destroy());
|
||||
removedClusters.clear();
|
||||
}, {
|
||||
delay: 250
|
||||
});
|
||||
|
||||
// listen ipc-events which could be handled *only* in main-process (nodeIntegration=true)
|
||||
// listen for ipc-events that must be handled *only* in main-process (nodeIntegration=true)
|
||||
handleMessage(ClusterIpcEvent.STOP, this.stopCluster.bind(this));
|
||||
}
|
||||
|
||||
@ -37,7 +45,7 @@ export class ClusterManager {
|
||||
|
||||
protected stopCluster(clusterId: ClusterId) {
|
||||
tracker.event("cluster", "stop");
|
||||
this.getCluster(clusterId)?.stop();
|
||||
this.getCluster(clusterId)?.destroy();
|
||||
}
|
||||
|
||||
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
||||
|
||||
@ -24,6 +24,7 @@ export enum ClusterStatus {
|
||||
}
|
||||
|
||||
export interface ClusterState extends ClusterModel {
|
||||
initialized?: boolean;
|
||||
apiUrl: string;
|
||||
online?: boolean;
|
||||
accessible?: boolean;
|
||||
@ -98,12 +99,13 @@ export class Cluster implements ClusterModel {
|
||||
|
||||
bindEvents(viewId: number) {
|
||||
if (!this.initialized) return;
|
||||
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||
const refreshStatusTimer = setInterval(() => this.refreshStatus(), 30000); // every 30s
|
||||
const refreshEventsTimer = setInterval(() => this.refreshEvents(), 3000); // every 3s
|
||||
|
||||
this.disposers.push(
|
||||
() => clearTimeout(refreshStatusTimer),
|
||||
() => clearTimeout(refreshEventsTimer),
|
||||
() => clearInterval(refreshStatusTimer),
|
||||
() => clearInterval(refreshEventsTimer),
|
||||
|
||||
reaction(() => this.getState(), clusterState => {
|
||||
sendMessage({
|
||||
@ -118,15 +120,24 @@ export class Cluster implements ClusterModel {
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
if (!this.initialized) return;
|
||||
logger.info(`[CLUSTER]: unbind events`, this.getMeta());
|
||||
this.disposers.forEach(dispose => dispose());
|
||||
this.disposers.length = 0;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.initialized) return;
|
||||
this.contextHandler.stopServer();
|
||||
this.kubeconfigManager.unlink();
|
||||
this.unbindEvents();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
try {
|
||||
this.stop();
|
||||
this.unbindEvents();
|
||||
this.kubeconfigManager.unlink();
|
||||
} catch (err) {
|
||||
logger.error(`[CLUSTER]: destroy() throws: ${err}`, this.getMeta());
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@ -318,6 +329,7 @@ export class Cluster implements ClusterModel {
|
||||
getState(): ClusterState {
|
||||
const state: ClusterState = {
|
||||
...this.toJSON(),
|
||||
initialized: this.initialized,
|
||||
apiUrl: this.apiUrl,
|
||||
online: this.online,
|
||||
accessible: this.accessible,
|
||||
@ -333,4 +345,12 @@ export class Cluster implements ClusterModel {
|
||||
recurseEverything: true
|
||||
})
|
||||
}
|
||||
|
||||
// get cluster system meta, e.g. use in "logger"
|
||||
getMeta() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.contextName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ export class KubeconfigManager {
|
||||
}
|
||||
|
||||
unlink() {
|
||||
logger.debug('Deleting temporary kubeconfig: ' + this.tempFile)
|
||||
logger.info('Deleting temporary kubeconfig: ' + this.tempFile)
|
||||
fs.unlinkSync(this.tempFile)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,6 @@ export class WindowManager {
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
delay: 250,
|
||||
}),
|
||||
|
||||
// auto-destroy views for removed clusters
|
||||
|
||||
@ -1,261 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<b-container fluid class="h-100">
|
||||
<b-row align-h="around">
|
||||
<b-col lg="7">
|
||||
<div class="card">
|
||||
<h2>Add Cluster</h2>
|
||||
<div class="add-cluster">
|
||||
<b-form @submit.prevent="doAddCluster">
|
||||
<b-form-group
|
||||
label="Choose config:"
|
||||
>
|
||||
<b-form-file
|
||||
v-model="file"
|
||||
:state="Boolean(file)"
|
||||
placeholder="Choose a file or drop it here..."
|
||||
drop-placeholder="Drop file here..."
|
||||
@input="reloadKubeContexts()"
|
||||
/>
|
||||
|
||||
<div class="mt-3">
|
||||
Selected file: {{ file ? file.name : '' }}
|
||||
</div>
|
||||
|
||||
<b-form-select
|
||||
id="kubecontext-select"
|
||||
v-model="kubecontext"
|
||||
:options="contextNames"
|
||||
@change="onSelect($event)"
|
||||
/>
|
||||
<b-button v-b-toggle.collapse-advanced variant="link">
|
||||
Proxy settings
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
<b-collapse id="collapse-advanced">
|
||||
<b-form-group
|
||||
label="HTTP Proxy server. Used for communicating with Kubernetes API."
|
||||
description="A HTTP proxy server URL (format: http://<address>:<port>)."
|
||||
>
|
||||
<b-form-input
|
||||
v-model="httpsProxy"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-collapse>
|
||||
<b-form-group
|
||||
label="Kubeconfig:"
|
||||
v-if="status === 'ERROR' || kubecontext === 'custom'"
|
||||
>
|
||||
<div class="editor">
|
||||
<prism-editor v-model="clusterconfig" language="yaml" />
|
||||
</div>
|
||||
</b-form-group>
|
||||
<b-alert variant="danger" show v-if="status === 'ERROR'">
|
||||
{{ errorMsg }}
|
||||
<div v-if="errorDetails !== ''">
|
||||
<b-button v-b-toggle.collapse-error variant="link" size="sm">
|
||||
Show details
|
||||
</b-button>
|
||||
<b-collapse id="collapse-error">
|
||||
<code>
|
||||
{{ errorDetails }}
|
||||
</code>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</b-alert>
|
||||
<b-form-row>
|
||||
<b-col>
|
||||
<b-button variant="primary" type="submit" :disabled="clusterconfig === ''">
|
||||
<b-spinner small v-if="isProcessing" label="Small Spinner" />
|
||||
{{ addButtonText }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-form-row>
|
||||
</b-form>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<!--info-panel-->
|
||||
</b-row>
|
||||
</b-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as PrismEditor from 'vue-prism-editor'
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
import { dumpConfigYaml } from "../../../common/kube-helpers"
|
||||
import ClustersMixin from "@/_vue/mixins/ClustersMixin";
|
||||
import * as path from "path"
|
||||
import fs from 'fs'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class ClusterAccessError extends Error {}
|
||||
|
||||
export default {
|
||||
name: 'AddClusterPage',
|
||||
mixins: [ClustersMixin],
|
||||
props: { },
|
||||
components: {
|
||||
PrismEditor,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
file: null,
|
||||
filepath: null,
|
||||
clusterconfig: "",
|
||||
httpsProxy: "",
|
||||
kubecontext: "",
|
||||
status: "",
|
||||
errorMsg: "",
|
||||
errorCluster: "",
|
||||
errorDetails: "",
|
||||
seenContexts: []
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
this.filepath = path.join(process.env.HOME, '.kube', 'config')
|
||||
this.file = new File(fs.readFileSync(this.filepath), this.filepath)
|
||||
this.$store.dispatch("reloadAvailableKubeContexts", this.filepath);
|
||||
this.seenContexts = JSON.parse(JSON.stringify(this.$store.getters.seenContexts)) // clone seenContexts from store
|
||||
this.storeSeenContexts()
|
||||
},
|
||||
computed: {
|
||||
isProcessing: function() {
|
||||
return this.status === "PROCESSING";
|
||||
},
|
||||
addButtonText: function() {
|
||||
if (this.kubecontext === "custom") {
|
||||
return "Add Cluster(s)"
|
||||
} else {
|
||||
return "Add Cluster"
|
||||
}
|
||||
},
|
||||
contextNames: function() {
|
||||
const configs = this.availableContexts
|
||||
const names = configs.map((kc) => {
|
||||
return { text: kc.currentContext + (this.isNewContext(kc.currentContext) ? " (new)": ""), value: dumpConfigYaml(kc) }
|
||||
})
|
||||
names.unshift({text: "Select kubeconfig", value: ""})
|
||||
names.push({text: "Custom ...", value: "custom"})
|
||||
|
||||
return names;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reloadKubeContexts() {
|
||||
this.filepath = this.file.path
|
||||
this.$store.dispatch("reloadAvailableKubeContexts", this.file.path);
|
||||
},
|
||||
isNewContext(context) {
|
||||
return this.newContexts.indexOf(context) > -1
|
||||
},
|
||||
storeSeenContexts() {
|
||||
const configs = this.$store.getters.availableKubeContexts
|
||||
const contexts = configs.map((kc) => {
|
||||
return kc.currentContext
|
||||
})
|
||||
this.$store.dispatch("addSeenContexts", contexts)
|
||||
},
|
||||
onSelect: function() {
|
||||
this.status = "";
|
||||
if (this.kubecontext === "custom") {
|
||||
this.clusterconfig = "";
|
||||
} else {
|
||||
this.clusterconfig = this.kubecontext;
|
||||
}
|
||||
},
|
||||
doAddCluster: async function() {
|
||||
// Clear previous error details
|
||||
this.errorMsg = ""
|
||||
this.errorCluster = ""
|
||||
this.errorDetails = ""
|
||||
this.status = "PROCESSING"
|
||||
try {
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromString(this.clusterconfig); // throws TypeError if we cannot parse kubeconfig
|
||||
const clusterId = uuidv4();
|
||||
// We need to store the kubeconfig to "app-home"/
|
||||
if (this.kubecontext === "custom") {
|
||||
this.filepath = saveConfigToAppFiles(clusterId, this.clusterconfig)
|
||||
}
|
||||
const clusterInfo = {
|
||||
id: clusterId,
|
||||
kubeConfigPath: this.filepath,
|
||||
contextName: kc.currentContext,
|
||||
preferences: {
|
||||
clusterName: kc.currentContext
|
||||
},
|
||||
workspace: this.$store.getters.currentWorkspace.id
|
||||
}
|
||||
if (this.httpsProxy) {
|
||||
clusterInfo.preferences.httpsProxy = this.httpsProxy
|
||||
}
|
||||
console.log("sending clusterInfo:", clusterInfo)
|
||||
let res = await this.$store.dispatch('addCluster', clusterInfo)
|
||||
console.log("addCluster result:", res)
|
||||
if(!res){
|
||||
this.status = "ERROR";
|
||||
return false;
|
||||
}
|
||||
this.status = "SUCCESS"
|
||||
this.$router.push({
|
||||
name: "cluster-page",
|
||||
params: {
|
||||
id: res.id
|
||||
},
|
||||
}).catch((err) => {})
|
||||
} catch (error) {
|
||||
console.log("addCluster raised:", error)
|
||||
if(typeof error === 'string') {
|
||||
this.errorMsg = error;
|
||||
} else if(error instanceof TypeError) {
|
||||
this.errorMsg = "cannot parse kubeconfig";
|
||||
} else if(error.response && error.response.statusCode === 401) {
|
||||
this.errorMsg = "invalid kubeconfig (access denied)"
|
||||
} else if(error.message) {
|
||||
this.errorMsg = error.message
|
||||
} else if(error instanceof ClusterAccessError) {
|
||||
this.errorMsg = `Invalid kubeconfig context ${error.context}`
|
||||
this.errorCluster = error.cluster
|
||||
this.errorDetails = error.details
|
||||
}
|
||||
this.status = "ERROR";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.help{
|
||||
border-left: 1px solid #353a3e;
|
||||
padding-top: 20px;
|
||||
&:first-child{
|
||||
padding-top: 0;
|
||||
}
|
||||
h3{
|
||||
padding: 0.75rem 0 0.75rem 0;
|
||||
}
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
h2{
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.add-cluster {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
padding-left: 0;
|
||||
}
|
||||
</style>
|
||||
@ -16,16 +16,17 @@ import { tracker } from "../../../common/tracker";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { workspaceStore } from "../../../common/workspace-store";
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { navigate } from "../../navigation";
|
||||
|
||||
@observer
|
||||
export class AddCluster extends React.Component {
|
||||
readonly custom: any = "custom"
|
||||
@observable.ref clusterConfig: KubeConfig;
|
||||
@observable.ref kubeConfig: KubeConfig; // local ~/.kube/config (if available)
|
||||
@observable.ref error: React.ReactNode;
|
||||
|
||||
@observable isWaiting = false
|
||||
@observable showSettings = false
|
||||
@observable error = ""
|
||||
@observable proxyServer = ""
|
||||
@observable customConfig = ""
|
||||
|
||||
@ -49,13 +50,15 @@ export class AddCluster extends React.Component {
|
||||
@computed get clusterOptions() {
|
||||
const options: SelectOption<KubeConfig>[] = [];
|
||||
if (this.kubeConfig) {
|
||||
splitConfig(this.kubeConfig).forEach(kubeConfig => {
|
||||
const contextName = kubeConfig.getCurrentContext();
|
||||
const contexts = splitConfig(this.kubeConfig)
|
||||
.filter(kc => !clusterStore.hasContext(kc.currentContext));
|
||||
|
||||
contexts.forEach(kubeConfig => {
|
||||
const isNew = false; // fixme: detect new context since last visit
|
||||
options.push({
|
||||
value: kubeConfig,
|
||||
label: <>
|
||||
{contextName}
|
||||
{kubeConfig.currentContext}
|
||||
{isNew && <span className="new"> <Trans>(new)</Trans></span>}
|
||||
</>,
|
||||
})
|
||||
@ -72,16 +75,16 @@ export class AddCluster extends React.Component {
|
||||
tracker.event("cluster", "add");
|
||||
const { clusterConfig, customConfig, proxyServer } = this;
|
||||
const clusterId = uuid();
|
||||
this.isWaiting = true
|
||||
this.error = ""
|
||||
try {
|
||||
const config = this.isCustom ? loadConfig(customConfig) : clusterConfig;
|
||||
if (!config) {
|
||||
this.error = "Please select kubeconfig"
|
||||
this.error = <Trans>Please select kubeconfig</Trans>
|
||||
return;
|
||||
}
|
||||
this.error = ""
|
||||
this.isWaiting = true
|
||||
validateConfig(config);
|
||||
clusterStore.addCluster({
|
||||
await clusterStore.addCluster({
|
||||
id: clusterId,
|
||||
kubeConfigPath: saveConfigToAppFiles(clusterId, config),
|
||||
workspace: workspaceStore.currentWorkspaceId,
|
||||
@ -90,7 +93,8 @@ export class AddCluster extends React.Component {
|
||||
clusterName: config.currentContext,
|
||||
httpsProxy: proxyServer || undefined,
|
||||
},
|
||||
})
|
||||
});
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
this.error = String(err);
|
||||
} finally {
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
> .clusters {
|
||||
@include hidden-scrollbar;
|
||||
//@include hidden-scrollbar; // fixme: uncomment after refactoring tooltip.tsx
|
||||
--flex-gap: #{$padding * 2};
|
||||
padding: $padding;
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import { addClusterURL } from "../+add-cluster";
|
||||
import { clusterSettingsURL } from "../+cluster-settings";
|
||||
import { landingURL } from "../+landing-page";
|
||||
import { Tooltip, TooltipContent } from "../tooltip";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
|
||||
// fixme: allow to rearrange clusters with drag&drop
|
||||
// fixme: disconnect cluster from context-menu
|
||||
@ -50,7 +51,6 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
label: _i18n._(t`Settings`),
|
||||
click: () => navigate(clusterSettingsURL())
|
||||
}));
|
||||
|
||||
if (cluster.initialized) {
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Disconnect`),
|
||||
@ -59,6 +59,16 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
}
|
||||
}))
|
||||
}
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Remove`),
|
||||
click: () => {
|
||||
ConfirmDialog.open({
|
||||
ok: () => clusterStore.removeById(cluster.id),
|
||||
labelOk: _i18n._(t`Remove`),
|
||||
message: <p>Are you sure want to remove cluster <b title={cluster.id}>{cluster.contextName}</b>?</p>,
|
||||
})
|
||||
}
|
||||
}));
|
||||
menu.popup({
|
||||
window: remote.getCurrentWindow()
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user