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

Allow user to select Kubeconfig from filesystem, fix #738

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-08-24 19:24:24 +03:00
parent 0f11b03692
commit f067152d04
9 changed files with 252 additions and 91 deletions

View File

@ -83,7 +83,7 @@ msgstr "Account Name"
msgid "Active"
msgstr "Active"
#: src/renderer/components/+add-cluster/add-cluster.tsx:171
#: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/cluster-manager/clusters-menu.tsx:116
msgid "Add Cluster"
msgstr "Add Cluster"
@ -108,7 +108,7 @@ msgstr "Add bindings to {name}"
#~ msgid "Add cluster"
#~ msgstr "Add cluster"
#: src/renderer/components/+add-cluster/add-cluster.tsx:192
#: src/renderer/components/+add-cluster/add-cluster.tsx:204
msgid "Add cluster(s)"
msgstr "Add cluster(s)"
@ -451,7 +451,7 @@ msgstr "Claim Name"
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
#: src/renderer/components/dialog/logs-dialog.tsx:39
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:94
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
msgid "Close"
msgstr "Close"
@ -522,7 +522,7 @@ msgstr "Conditions"
msgid "Config Maps"
msgstr "Config Maps"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:55
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:54
msgid "Config copied to clipboard"
msgstr "Config copied to clipboard"
@ -594,7 +594,7 @@ msgid "Conversion"
msgstr "Conversion"
#: src/renderer/components/dialog/logs-dialog.tsx:36
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:88
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:87
msgid "Copy to clipboard"
msgstr "Copy to clipboard"
@ -710,7 +710,7 @@ msgstr "Currently applied filters:"
msgid "Custom Resources"
msgstr "Custom Resources"
#: src/renderer/components/+add-cluster/add-cluster.tsx:116
#: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Custom.."
msgstr "Custom.."
@ -811,7 +811,7 @@ msgstr "Domains"
msgid "Download Mirror"
msgstr "Download Mirror"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:91
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:90
msgid "Download file"
msgstr "Download file"
@ -1197,7 +1197,7 @@ msgstr "Kind"
msgid "Kubeconfig"
msgstr "Kubeconfig"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:85
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:84
msgid "Kubeconfig File"
msgstr "Kubeconfig File"
@ -1731,8 +1731,12 @@ msgid "Persistent Volumes"
msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Please select kubeconfig"
msgstr "Please select kubeconfig"
#~ msgid "Please select kubeconfig"
#~ msgstr "Please select kubeconfig"
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
msgid "Please select kubeconfig context"
msgstr "Please select kubeconfig context"
#: src/renderer/components/+workloads-pods/pod-menu.tsx:50
msgid "Pod"
@ -1828,7 +1832,7 @@ msgstr "Provisioner"
msgid "Proxy is used only for non-cluster communication."
msgstr "Proxy is used only for non-cluster communication."
#: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/+add-cluster/add-cluster.tsx:188
msgid "Proxy settings"
msgstr "Proxy settings"
@ -2212,13 +2216,21 @@ msgstr "Secret type"
msgid "Secrets"
msgstr "Secrets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:185
msgid "Select a context"
msgstr "Select a context"
#: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134
msgid "Select a quota.."
msgstr "Select a quota.."
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
msgid "Select kubeconfig"
msgstr "Select kubeconfig"
#~ msgid "Select context"
#~ msgstr "Select context"
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
#~ msgid "Select kubeconfig"
#~ msgstr "Select kubeconfig"
#: src/renderer/components/+preferences/preferences.tsx:88
#~ msgid "Select repository"
@ -2773,7 +2785,7 @@ msgstr "{0} total, {1} available"
msgid "{0} unavailable"
msgstr "{0} unavailable"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:129
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:128
msgid "{accountName} kubeconfig"
msgstr "{accountName} kubeconfig"

View File

@ -83,7 +83,7 @@ msgstr ""
msgid "Active"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:171
#: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/cluster-manager/clusters-menu.tsx:116
msgid "Add Cluster"
msgstr ""
@ -108,7 +108,7 @@ msgstr ""
#~ msgid "Add cluster"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:192
#: src/renderer/components/+add-cluster/add-cluster.tsx:204
msgid "Add cluster(s)"
msgstr ""
@ -447,7 +447,7 @@ msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
#: src/renderer/components/dialog/logs-dialog.tsx:39
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:94
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
msgid "Close"
msgstr ""
@ -518,7 +518,7 @@ msgstr ""
msgid "Config Maps"
msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:55
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:54
msgid "Config copied to clipboard"
msgstr ""
@ -590,7 +590,7 @@ msgid "Conversion"
msgstr ""
#: src/renderer/components/dialog/logs-dialog.tsx:36
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:88
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:87
msgid "Copy to clipboard"
msgstr ""
@ -706,7 +706,7 @@ msgstr ""
msgid "Custom Resources"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:116
#: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Custom.."
msgstr ""
@ -807,7 +807,7 @@ msgstr ""
msgid "Download Mirror"
msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:91
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:90
msgid "Download file"
msgstr ""
@ -1188,7 +1188,7 @@ msgstr ""
msgid "Kubeconfig"
msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:85
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:84
msgid "Kubeconfig File"
msgstr ""
@ -1714,7 +1714,11 @@ msgid "Persistent Volumes"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Please select kubeconfig"
#~ msgid "Please select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
msgid "Please select kubeconfig context"
msgstr ""
#: src/renderer/components/+workloads-pods/pod-menu.tsx:50
@ -1811,7 +1815,7 @@ msgstr ""
msgid "Proxy is used only for non-cluster communication."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/+add-cluster/add-cluster.tsx:188
msgid "Proxy settings"
msgstr ""
@ -2195,13 +2199,21 @@ msgstr ""
msgid "Secrets"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:185
msgid "Select a context"
msgstr ""
#: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134
msgid "Select a quota.."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
msgid "Select kubeconfig"
msgstr ""
#~ msgid "Select context"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
#~ msgid "Select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:88
#~ msgid "Select repository"
@ -2756,7 +2768,7 @@ msgstr ""
msgid "{0} unavailable"
msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:129
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:128
msgid "{accountName} kubeconfig"
msgstr ""

View File

@ -84,7 +84,7 @@ msgstr "Название аккаунта"
msgid "Active"
msgstr "Активный"
#: src/renderer/components/+add-cluster/add-cluster.tsx:171
#: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/cluster-manager/clusters-menu.tsx:116
msgid "Add Cluster"
msgstr ""
@ -109,7 +109,7 @@ msgstr "Добавить привязки к {name}"
#~ msgid "Add cluster"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:192
#: src/renderer/components/+add-cluster/add-cluster.tsx:204
msgid "Add cluster(s)"
msgstr ""
@ -452,7 +452,7 @@ msgstr ""
#: src/renderer/components/+workloads-pods/pod-logs-dialog.tsx:243
#: src/renderer/components/dialog/logs-dialog.tsx:39
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:94
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:93
msgid "Close"
msgstr "Закрыть"
@ -523,7 +523,7 @@ msgstr "Состояния"
msgid "Config Maps"
msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:55
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:54
msgid "Config copied to clipboard"
msgstr "Конфигурация скопирована в буфер"
@ -595,7 +595,7 @@ msgid "Conversion"
msgstr ""
#: src/renderer/components/dialog/logs-dialog.tsx:36
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:88
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:87
msgid "Copy to clipboard"
msgstr "Копировать"
@ -711,7 +711,7 @@ msgstr "Текущие фильтры:"
msgid "Custom Resources"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:116
#: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Custom.."
msgstr ""
@ -812,7 +812,7 @@ msgstr "Домены"
msgid "Download Mirror"
msgstr ""
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:91
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:90
msgid "Download file"
msgstr "Скачать файл"
@ -1198,7 +1198,7 @@ msgstr "Тип"
msgid "Kubeconfig"
msgstr "Файл конфигурации"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:85
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:84
msgid "Kubeconfig File"
msgstr "Файл конфигурации"
@ -1732,7 +1732,11 @@ msgid "Persistent Volumes"
msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:63
msgid "Please select kubeconfig"
#~ msgid "Please select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:64
msgid "Please select kubeconfig context"
msgstr ""
#: src/renderer/components/+workloads-pods/pod-menu.tsx:50
@ -1829,7 +1833,7 @@ msgstr "Комиссия"
msgid "Proxy is used only for non-cluster communication."
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:176
#: src/renderer/components/+add-cluster/add-cluster.tsx:188
msgid "Proxy settings"
msgstr ""
@ -2213,13 +2217,21 @@ msgstr "Тип секрета"
msgid "Secrets"
msgstr "Secrets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:185
msgid "Select a context"
msgstr ""
#: src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx:134
msgid "Select a quota.."
msgstr "Выберите квоту..."
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
msgid "Select kubeconfig"
msgstr ""
#~ msgid "Select context"
#~ msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:173
#~ msgid "Select kubeconfig"
#~ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:88
#~ msgid "Select repository"
@ -2774,7 +2786,7 @@ msgstr "{0} всего, {1} доступно"
msgid "{0} unavailable"
msgstr "{0} недоступно"
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:129
#: src/renderer/components/kubeconfig-dialog/kubeconfig-dialog.tsx:128
msgid "{accountName} kubeconfig"
msgstr "{accountName} конфигурация"

View File

@ -1,11 +1,13 @@
import { app, remote } from "electron";
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"
import fse, { ensureDirSync, readFile, writeFileSync } from "fs-extra";
import fse, { ensureDirSync, writeFileSync } from "fs-extra";
import path from "path"
import os from "os"
import yaml from "js-yaml"
import logger from "../main/logger";
export const kubeConfigDefaultPath = path.join(os.homedir(), '.kube', 'config');
function resolveTilde(filePath: string) {
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
return filePath.replace("~", os.homedir());
@ -150,18 +152,3 @@ export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig |
writeFileSync(kubeConfigFile, kubeConfigContents);
return kubeConfigFile;
}
export async function getKubeConfigLocal(): Promise<string> {
try {
const configFile = path.join(os.homedir(), '.kube', 'config');
const file = await readFile(configFile, "utf8");
const obj = yaml.safeLoad(file);
if (obj.contexts) {
obj.contexts = obj.contexts.filter((ctx: any) => ctx?.context?.cluster && ctx?.name)
}
return yaml.safeDump(obj);
} catch (err) {
logger.debug(`Cannot read local kube-config: ${err}`)
return "";
}
}

View File

@ -1,13 +1,16 @@
import type { ThemeId } from "../renderer/theme.store";
import semver from "semver"
import { readFile } from "fs-extra"
import { action, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store"
import { getAppVersion } from "./utils/app-version";
import { getKubeConfigLocal, loadConfig } from "./kube-helpers";
import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
import { tracker } from "./tracker";
import logger from "../main/logger";
export interface UserStoreModel {
kubeConfigPath: string;
lastSeenAppVersion: string;
seenContexts: string[];
preferences: UserPreferences;
@ -37,10 +40,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
// refresh new contexts
this.whenLoaded.then(this.refreshNewContexts);
reaction(() => this.seenContexts.size, this.refreshNewContexts);
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
}
@observable lastSeenAppVersion = "0.0.0"
@observable kubeConfigPath = kubeConfigDefaultPath; // used in add-cluster page for providing context
@observable seenContexts = observable.set<string>();
@observable newContexts = observable.set<string>();
@ -55,6 +59,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
}
@action
resetKubeConfigPath() {
this.kubeConfigPath = kubeConfigDefaultPath;
}
@action
resetTheme() {
this.preferences.colorTheme = UserStore.defaultTheme;
@ -67,14 +76,18 @@ export class UserStore extends BaseStore<UserStoreModel> {
}
protected refreshNewContexts = async () => {
const kubeConfig = await getKubeConfigLocal();
if (kubeConfig) {
this.newContexts.clear();
const localContexts = loadConfig(kubeConfig).getContexts();
localContexts
.filter(ctx => ctx.cluster)
.filter(ctx => !this.seenContexts.has(ctx.name))
.forEach(ctx => this.newContexts.add(ctx.name));
try {
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
if (kubeConfig) {
this.newContexts.clear();
loadConfig(kubeConfig).getContexts()
.filter(ctx => ctx.cluster)
.filter(ctx => !this.seenContexts.has(ctx.name))
.forEach(ctx => this.newContexts.add(ctx.name));
}
} catch (err) {
logger.error(err);
this.resetKubeConfigPath();
}
}
@ -86,17 +99,21 @@ export class UserStore extends BaseStore<UserStoreModel> {
}
@action
protected fromStore(data: Partial<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences } = data
protected async fromStore(data: Partial<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data
if (lastSeenAppVersion) {
this.lastSeenAppVersion = lastSeenAppVersion;
}
if (kubeConfigPath) {
this.kubeConfigPath = kubeConfigPath;
}
this.seenContexts.replace(seenContexts);
Object.assign(this.preferences, preferences);
}
toJSON(): UserStoreModel {
const model: UserStoreModel = {
kubeConfigPath: this.kubeConfigPath,
lastSeenAppVersion: this.lastSeenAppVersion,
seenContexts: Array.from(this.seenContexts),
preferences: this.preferences,

View File

@ -1,10 +1,34 @@
.AddCluster {
.Select {
.kube-context {
--flex-gap: #{$padding};
}
&__control {
box-shadow: 0 0 0 1px $borderFaintColor;
}
}
.kube-config-select {
border: 1px solid $borderColor;
padding: $padding / 2 $padding;
border-radius: $radius;
transition: border 200ms ease-in-out;
> .title {
white-space: nowrap;
}
&:hover {
border-color: $primary;
cursor: pointer;
}
code {
word-break: break-all;
}
}
code {
color: $pink-400;
}

View File

@ -3,27 +3,28 @@ import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { computed, observable } from "mobx";
import { KubeConfig } from "@kubernetes/client-node";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { Trans } from "@lingui/macro";
import { Select, SelectOption } from "../select";
import { Input } from "../input";
import { FileInput, Input } from "../input";
import { AceEditor } from "../ace-editor";
import { Button } from "../button";
import { Icon } from "../icon";
import { WizardLayout } from "../layout/wizard-layout";
import { getKubeConfigLocal, loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers";
import { kubeConfigDefaultPath, loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers";
import { clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store";
import { v4 as uuid } from "uuid"
import { navigate } from "../../navigation";
import { userStore } from "../../../common/user-store";
import { clusterViewURL } from "../cluster-manager/cluster-view.route";
import { cssNames } from "../../utils";
import { Notifications } from "../notifications";
@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 localKubeConfig: KubeConfig;
@observable.ref newClusterConfig: KubeConfig;
@observable.ref error: React.ReactNode;
@observable isWaiting = false
@ -31,25 +32,36 @@ export class AddCluster extends React.Component {
@observable proxyServer = ""
@observable customConfig = ""
async componentDidMount() {
const kubeConfig: string = await getKubeConfigLocal()
if (kubeConfig) {
this.kubeConfig = loadConfig(kubeConfig)
}
componentDidMount() {
this.setLocalConfigPath(userStore.kubeConfigPath);
}
componentWillUnmount() {
userStore.markNewContextsAsSeen();
}
protected setLocalConfigPath(filePath: string) {
try {
const kubeConfig = loadConfig(filePath);
validateConfig(kubeConfig);
this.localKubeConfig = kubeConfig;
this.newClusterConfig = null; // reset previously selected
userStore.kubeConfigPath = filePath; // save to store
} catch (err) {
Notifications.error(
<p>Can't read config file in <em>{filePath}</em>: {String(err)}</p>
);
}
}
@computed get isCustom() {
return this.clusterConfig === this.custom;
return this.newClusterConfig === this.custom;
}
@computed get clusterOptions() {
const options: SelectOption<KubeConfig>[] = [];
if (this.kubeConfig) {
splitConfig(this.kubeConfig).forEach(kubeConfig => {
if (this.localKubeConfig) {
splitConfig(this.localKubeConfig).forEach(kubeConfig => {
const context = kubeConfig.currentContext;
const hasContext = clusterStore.hasContext(context);
if (!hasContext) {
@ -71,9 +83,8 @@ export class AddCluster extends React.Component {
if (value instanceof KubeConfig) {
const context = value.currentContext;
const isNew = userStore.newContexts.has(context);
const className = `${context} kube-context flex gaps align-center`
return (
<div className={className}>
<div className={cssNames("kube-context flex gaps align-center", context)}>
<span>{context}</span>
{isNew && <Icon material="fiber_new"/>}
</div>
@ -83,14 +94,14 @@ export class AddCluster extends React.Component {
};
addCluster = async () => {
const { clusterConfig, customConfig, proxyServer } = this;
const { newClusterConfig, customConfig, proxyServer } = this;
const clusterId = uuid();
this.isWaiting = true
this.error = ""
try {
const config = this.isCustom ? loadConfig(customConfig) : clusterConfig;
const config = this.isCustom ? loadConfig(customConfig) : newClusterConfig;
if (!config) {
this.error = <Trans>Please select kubeconfig</Trans>
this.error = <Trans>Please select kube-config's context</Trans>
return;
}
validateConfig(config);
@ -167,12 +178,31 @@ export class AddCluster extends React.Component {
return (
<WizardLayout className="AddCluster" infoPanel={this.renderInfo()}>
<h2><Trans>Add Cluster</Trans></h2>
<p>Choose config:</p>
<div className="flex gaps align-center">
<label
htmlFor="kube-config-select"
className="kube-config-select flex gaps align-center box grow"
>
<span className="title">Config file</span>
<code>{userStore.kubeConfigPath}</code>
</label>
{kubeConfigDefaultPath !== userStore.kubeConfigPath && (
<Icon
material="settings_backup_restore"
onClick={() => this.setLocalConfigPath(kubeConfigDefaultPath)}
tooltip="Reset to defaults"
/>
)}
</div>
<FileInput
id="kube-config-select"
onSelectFiles={({ file }) => this.setLocalConfigPath(file.path)}
/>
<Select
placeholder={<Trans>Select kubeconfig</Trans>}
value={this.clusterConfig}
placeholder={<Trans>Select a context</Trans>}
value={this.newClusterConfig}
options={this.clusterOptions}
onChange={({ value }: SelectOption) => this.clusterConfig = value}
onChange={({ value }: SelectOption) => this.newClusterConfig = value}
formatOptionLabel={this.formatClusterContextLabel}
id="kubecontext-select"
/>

View File

@ -0,0 +1,66 @@
import React, { InputHTMLAttributes } from "react";
export interface FileInputSelection<T = string> {
file: File;
data?: T | any; // not available when readAsTexts={false}
error?: string;
}
interface Props extends InputHTMLAttributes<any> {
id?: string; // could be used with <label htmlFor={id}/> to open filesystem dialog
accept?: string; // allowed file types to select, e.g. "application/json"
readAsText?: boolean; // provide files content as text in selection-callback
multiple?: boolean;
onSelectFiles(...selectedFiles: FileInputSelection[]): void;
}
export class FileInput extends React.Component<Props> {
protected input: HTMLInputElement;
protected style: React.CSSProperties = {
position: "absolute",
display: "none",
};
selectFiles = () => {
this.input.click(); // opens system dialog for selecting files
}
protected onChange = async (evt: React.ChangeEvent<HTMLInputElement>) => {
const fileList = Array.from(evt.target.files);
if (!fileList.length) {
return;
}
let selectedFiles: FileInputSelection[] = fileList.map(file => ({ file }));
if (this.props.readAsText) {
const readingFiles: Promise<FileInputSelection>[] = fileList.map(file => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve({
file: file,
data: reader.result,
error: reader.error ? String(reader.error) : null,
})
};
reader.readAsText(file);
})
});
selectedFiles = await Promise.all(readingFiles);
}
this.props.onSelectFiles(...selectedFiles);
}
render() {
const { onSelectFiles, readAsText, ...props } = this.props;
return (
<input
type="file"
style={this.style}
onChange={this.onChange}
ref={e => this.input = e}
{...props}
/>
)
}
}

View File

@ -1,2 +1,3 @@
export * from './input'
export * from './search-input'
export * from './file-input'