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

tweak add cluster page

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2021-05-04 08:57:01 +03:00
parent 3737c95719
commit 123b628ad7
2 changed files with 68 additions and 274 deletions

View File

@ -3,8 +3,8 @@
$spacing: $padding * 2; $spacing: $padding * 2;
.AceEditor { .AceEditor {
min-height: 200px; min-height: 600px;
max-height: 400px; max-height: 600px;
border: 1px solid var(--colorVague); border: 1px solid var(--colorVague);
border-radius: $radius; border-radius: $radius;
@ -17,24 +17,6 @@
} }
} }
.Select {
.kube-context {
--flex-gap: #{$padding};
}
// todo: extract to component, merge with namespace-select.scss
&__placeholder {
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&__control {
box-shadow: 0 0 0 1px $borderFaintColor;
}
}
code { code {
color: $pink-400; color: $pink-400;
} }

View File

@ -1,51 +1,35 @@
import "./add-cluster.scss"; import "./add-cluster.scss";
import os from "os";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { action, observable, runInAction } from "mobx"; import { action, observable, runInAction } from "mobx";
import { remote } from "electron";
import { KubeConfig } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
import { Select, SelectOption } from "../select";
import { DropFileInput, Input } from "../input";
import { AceEditor } from "../ace-editor"; import { AceEditor } from "../ace-editor";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { loadConfig, splitConfig, validateKubeConfig } from "../../../common/kube-helpers";
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
import { ClusterStore } from "../../../common/cluster-store"; import { ClusterStore } from "../../../common/cluster-store";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { UserStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { cssNames } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Tab, Tabs } from "../tabs";
import { ExecValidationNotFoundError } from "../../../common/custom-errors"; import { ExecValidationNotFoundError } from "../../../common/custom-errors";
import { appEventBus } from "../../../common/event-bus"; import { appEventBus } from "../../../common/event-bus";
import { PageLayout } from "../layout/page-layout"; import { PageLayout } from "../layout/page-layout";
import { docsUrl } from "../../../common/vars"; import { docsUrl } from "../../../common/vars";
import { catalogURL } from "../+catalog"; import { catalogURL } from "../+catalog";
import { preferencesURL } from "../+preferences";
enum KubeConfigSourceTab { import { Input } from "../input";
FILE = "file",
TEXT = "text"
}
@observer @observer
export class AddCluster extends React.Component { export class AddCluster extends React.Component {
@observable.ref kubeConfigLocal: KubeConfig; @observable.ref kubeConfigLocal: KubeConfig;
@observable.ref error: React.ReactNode; @observable.ref error: React.ReactNode;
@observable kubeContexts = observable.map<string, KubeConfig>(); // available contexts from kubeconfig-file or user-input
@observable selectedContexts = observable.array<string>();
@observable sourceTab = KubeConfigSourceTab.FILE;
@observable kubeConfigPath = "";
@observable customConfig = ""; @observable customConfig = "";
@observable proxyServer = ""; @observable proxyServer = "";
@observable isWaiting = false; @observable isWaiting = false;
@observable showSettings = false; @observable showSettings = false;
kubeContexts = observable.map<string, KubeConfig>();
componentDidMount() { componentDidMount() {
ClusterStore.getInstance().setActive(null);
this.setKubeConfig(UserStore.getInstance().kubeConfigPath);
appEventBus.emit({ name: "cluster-add", action: "start" }); appEventBus.emit({ name: "cluster-add", action: "start" });
} }
@ -53,52 +37,19 @@ export class AddCluster extends React.Component {
UserStore.getInstance().markNewContextsAsSeen(); UserStore.getInstance().markNewContextsAsSeen();
} }
@action
setKubeConfig(filePath: string, { throwError = false } = {}) {
try {
this.kubeConfigLocal = loadConfig(filePath);
validateConfig(this.kubeConfigLocal);
this.refreshContexts();
this.kubeConfigPath = filePath;
UserStore.getInstance().kubeConfigPath = filePath; // save to store
} catch (err) {
if (!UserStore.getInstance().isDefaultKubeConfigPath) {
Notifications.error(
<div>Can&apos;t setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
);
}
if (throwError) {
throw err;
}
}
}
@action @action
refreshContexts() { refreshContexts() {
this.selectedContexts.clear();
this.kubeContexts.clear(); this.kubeContexts.clear();
switch (this.sourceTab) { try {
case KubeConfigSourceTab.FILE: this.error = "";
const contexts = this.getContexts(this.kubeConfigLocal); const contexts = this.getContexts(loadConfig(this.customConfig || "{}"));
this.kubeContexts.replace(contexts); console.log(contexts);
break;
case KubeConfigSourceTab.TEXT:
try {
this.error = "";
const contexts = this.getContexts(loadConfig(this.customConfig || "{}"));
this.kubeContexts.replace(contexts); this.kubeContexts.replace(contexts);
} catch (err) { } catch (err) {
this.error = String(err); this.error = String(err);
}
break;
}
if (this.kubeContexts.size === 1) {
this.selectedContexts.push(this.kubeContexts.keys().next().value);
} }
} }
@ -112,36 +63,14 @@ export class AddCluster extends React.Component {
return contexts; return contexts;
} }
selectKubeConfigDialog = async () => {
const { dialog, BrowserWindow } = remote;
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
defaultPath: this.kubeConfigPath,
properties: ["openFile", "showHiddenFiles"],
message: `Select custom kubeconfig file`,
buttonLabel: `Use configuration`,
});
if (!canceled && filePaths.length) {
this.setKubeConfig(filePaths[0]);
}
};
onDropKubeConfig = (files: File[]) => {
this.sourceTab = KubeConfigSourceTab.FILE;
this.setKubeConfig(files[0].path);
};
@action @action
addClusters = (): void => { addClusters = (): void => {
try { try {
if (!this.selectedContexts.length) {
return void (this.error = "Please select at least one cluster context");
}
this.error = ""; this.error = "";
this.isWaiting = true; this.isWaiting = true;
appEventBus.emit({ name: "cluster-add", action: "click" }); appEventBus.emit({ name: "cluster-add", action: "click" });
const newClusters = this.selectedContexts.filter(context => { const newClusters = Array.from(this.kubeContexts.keys()).filter(context => {
const kubeConfig = this.kubeContexts.get(context); const kubeConfig = this.kubeContexts.get(context);
const error = validateKubeConfig(kubeConfig, context); const error = validateKubeConfig(kubeConfig, context);
@ -157,9 +86,7 @@ export class AddCluster extends React.Component {
}).map(context => { }).map(context => {
const clusterId = uuid(); const clusterId = uuid();
const kubeConfig = this.kubeContexts.get(context); const kubeConfig = this.kubeContexts.get(context);
const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE const kubeConfigPath = ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
? this.kubeConfigPath // save link to original kubeconfig in file-system
: ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
return { return {
id: clusterId, id: clusterId,
@ -193,9 +120,8 @@ export class AddCluster extends React.Component {
renderInfo() { renderInfo() {
return ( return (
<p> <p>
Add clusters by clicking the <span className="text-primary">Add Cluster</span> button. Paste kubeconfig as a text from the clipboard to the textarea below.
You&apos;ll need to obtain a working kubeconfig for the cluster you want to add. If you want to add clusters from kubeconfigs that exists on filesystem, please add those files (or folders) to kubeconfig sync via <a onClick={() => navigate(preferencesURL())}>Preferences</a>.
You can either browse it from the file system or paste it as a text from the clipboard.
Read more about adding clusters <a href={`${docsUrl}/clusters/adding-clusters/`} rel="noreferrer" target="_blank">here</a>. Read more about adding clusters <a href={`${docsUrl}/clusters/adding-clusters/`} rel="noreferrer" target="_blank">here</a>.
</p> </p>
); );
@ -204,180 +130,66 @@ export class AddCluster extends React.Component {
renderKubeConfigSource() { renderKubeConfigSource() {
return ( return (
<> <>
<Tabs onChange={this.onKubeConfigTabChange}> <div className="flex column">
<Tab <AceEditor
value={KubeConfigSourceTab.FILE} autoFocus
label="Select kubeconfig file" showGutter={false}
active={this.sourceTab == KubeConfigSourceTab.FILE}/> mode="yaml"
<Tab value={this.customConfig}
value={KubeConfigSourceTab.TEXT} wrap={true}
label="Paste as text" onChange={value => {
active={this.sourceTab == KubeConfigSourceTab.TEXT} this.customConfig = value;
this.refreshContexts();
}}
/> />
</Tabs> </div>
{this.sourceTab === KubeConfigSourceTab.FILE && (
<div>
<div className="kube-config-select flex gaps align-center">
<Input
theme="round-black"
className="kube-config-path box grow"
value={this.kubeConfigPath}
onChange={v => this.kubeConfigPath = v}
onBlur={this.onKubeConfigInputBlur}
/>
{this.kubeConfigPath !== kubeConfigDefaultPath && (
<Icon
material="settings_backup_restore"
onClick={() => this.setKubeConfig(kubeConfigDefaultPath)}
tooltip="Reset"
/>
)}
<Icon
material="folder"
onClick={this.selectKubeConfigDialog}
tooltip="Browse"
/>
</div>
<small className="hint">
Pro-Tip: you can also drag-n-drop kubeconfig file to this area
</small>
</div>
)}
{this.sourceTab === KubeConfigSourceTab.TEXT && (
<div className="flex column">
<AceEditor
autoFocus
showGutter={false}
mode="yaml"
value={this.customConfig}
onChange={value => {
this.customConfig = value;
this.refreshContexts();
}}
/>
<small className="hint">
Pro-Tip: paste kubeconfig to get available contexts
</small>
</div>
)}
</> </>
); );
} }
renderContextSelector() {
const allContexts = Array.from(this.kubeContexts.keys());
const placeholder = this.selectedContexts.length > 0
? <>Selected contexts: <b>{this.selectedContexts.length}</b></>
: "Select contexts";
return (
<div>
<Select
id="kubecontext-select" // todo: provide better mapping for integration tests (e.g. data-test-id="..")
placeholder={placeholder}
controlShouldRenderValue={false}
closeMenuOnSelect={false}
isOptionSelected={() => false}
options={allContexts}
formatOptionLabel={this.formatContextLabel}
noOptionsMessage={() => `No contexts available or they have been added already`}
onChange={({ value: ctx }: SelectOption<string>) => {
if (this.selectedContexts.includes(ctx)) {
this.selectedContexts.remove(ctx);
} else {
this.selectedContexts.push(ctx);
}
}}
/>
{this.selectedContexts.length > 0 && (
<small className="hint">
<span>Applying contexts:</span>{" "}
<code>{this.selectedContexts.join(", ")}</code>
</small>
)}
</div>
);
}
onKubeConfigInputBlur = () => {
const isChanged = this.kubeConfigPath !== UserStore.getInstance().kubeConfigPath;
if (isChanged) {
this.kubeConfigPath = this.kubeConfigPath.replace("~", os.homedir());
try {
this.setKubeConfig(this.kubeConfigPath, { throwError: true });
} catch (err) {
this.setKubeConfig(UserStore.getInstance().kubeConfigPath); // revert to previous valid path
}
}
};
onKubeConfigTabChange = (tabId: KubeConfigSourceTab) => {
this.sourceTab = tabId;
this.error = "";
this.refreshContexts();
};
protected formatContextLabel = ({ value: context }: SelectOption<string>) => {
const isNew = UserStore.getInstance().newContexts.has(context);
const isSelected = this.selectedContexts.includes(context);
return (
<div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span>
{isNew && <Icon small material="fiber_new"/>}
{isSelected && <Icon small material="check" className="box right"/>}
</div>
);
};
render() { render() {
const submitDisabled = this.selectedContexts.length === 0; const submitDisabled = this.kubeContexts.size === 0;
return ( return (
<DropFileInput onDropFiles={this.onDropKubeConfig}> <PageLayout className="AddClusters" showOnTop={true}>
<PageLayout className="AddClusters" showOnTop={true}> <h2>Add Clusters from Kubeconfig</h2>
<h2>Add Clusters from Kubeconfig</h2> {this.renderInfo()}
{this.renderInfo()} {this.renderKubeConfigSource()}
{this.renderKubeConfigSource()} <div className="cluster-settings">
{this.renderContextSelector()} <a href="#" onClick={() => this.showSettings = !this.showSettings}>
<div className="cluster-settings"> Proxy settings
<a href="#" onClick={() => this.showSettings = !this.showSettings}> </a>
Proxy settings </div>
</a> {this.showSettings && (
</div> <div className="proxy-settings">
{this.showSettings && ( <p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
<div className="proxy-settings"> <Input
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p> autoFocus
<Input value={this.proxyServer}
autoFocus onChange={value => this.proxyServer = value}
value={this.proxyServer} theme="round-black"
onChange={value => this.proxyServer = value}
theme="round-black"
/>
<small className="hint">
{"A HTTP proxy server URL (format: http://<address>:<port>)."}
</small>
</div>
)}
{this.error && (
<div className="error">{this.error}</div>
)}
<div className="actions-panel">
<Button
primary
disabled={submitDisabled}
label={this.selectedContexts.length < 2 ? "Add cluster" : "Add clusters"}
onClick={this.addClusters}
waiting={this.isWaiting}
tooltip={submitDisabled ? "Select at least one cluster to add." : undefined}
tooltipOverrideDisabled
/> />
<small className="hint">
{"A HTTP proxy server URL (format: http://<address>:<port>)."}
</small>
</div> </div>
</PageLayout> )}
</DropFileInput> {this.error && (
<div className="error">{this.error}</div>
)}
<div className="actions-panel">
<Button
primary
disabled={submitDisabled}
label={this.kubeContexts.keys.length < 2 ? "Add cluster" : "Add clusters"}
onClick={this.addClusters}
waiting={this.isWaiting}
tooltip={submitDisabled ? "Paste a valid kubeconfig." : undefined}
tooltipOverrideDisabled
/>
</div>
</PageLayout>
); );
} }
} }