mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add kubesync shortcut to catalog
- Add ability to register new app preference groupings - Switch all lens preferences to be retreived from the registry - Dynamically render preference's navigation based on the existance of items in each grouping - Add ability for settings to declare that they shouldn't be rendered Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
eef5dd5f9c
commit
207f0dfd62
@ -60,14 +60,13 @@ describe("Lens integration tests", () => {
|
|||||||
await app.client.waitUntilTextExists("[data-testid=application-header]", "Application");
|
await app.client.waitUntilTextExists("[data-testid=application-header]", "Application");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows all tabs and their contents", async () => {
|
it.each([
|
||||||
await app.client.click("[data-testid=application-tab]");
|
["application", "Application"],
|
||||||
await app.client.click("[data-testid=proxy-tab]");
|
["proxy", "Proxy"],
|
||||||
await app.client.waitUntilTextExists("[data-testid=proxy-header]", "Proxy");
|
["kubernetes", "Kubernetes"],
|
||||||
await app.client.click("[data-testid=kube-tab]");
|
])("Can click the %s tab and see the %s header", async (tab, header) => {
|
||||||
await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "Kubernetes");
|
await app.client.click(`[data-testid=${tab}-tab]`);
|
||||||
await app.client.click("[data-testid=telemetry-tab]");
|
await app.client.waitUntilTextExists(`[data-testid=${tab}-header]`, header);
|
||||||
await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "Telemetry");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ensures helm repos", async () => {
|
it("ensures helm repos", async () => {
|
||||||
@ -77,7 +76,7 @@ describe("Lens integration tests", () => {
|
|||||||
fail("Lens failed to add any repositories");
|
fail("Lens failed to add any repositories");
|
||||||
}
|
}
|
||||||
|
|
||||||
await app.client.click("[data-testid=kube-tab]");
|
await app.client.click("[data-testid=kubernetes-tab]");
|
||||||
await app.client.waitUntilTextExists("div.repos .repoName", repos[0].name); // wait for the helm-cli to fetch the repo(s)
|
await app.client.waitUntilTextExists("div.repos .repoName", repos[0].name); // wait for the helm-cli to fetch the repo(s)
|
||||||
await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down
|
await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down
|
||||||
await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text)
|
await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text)
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { clusterActivateHandler, clusterDeleteHandler, clusterDisconnectHandler
|
|||||||
import { ClusterStore } from "../cluster-store";
|
import { ClusterStore } from "../cluster-store";
|
||||||
import { requestMain } from "../ipc";
|
import { requestMain } from "../ipc";
|
||||||
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||||
import { addClusterURL } from "../routes";
|
import { addClusterURL, preferencesURL } from "../routes";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||||
import { HotbarStore } from "../hotbar-store";
|
import { HotbarStore } from "../hotbar-store";
|
||||||
@ -172,13 +172,18 @@ export class KubernetesClusterCategory extends CatalogCategory {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
|
this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
|
||||||
ctx.menuItems.push({
|
ctx.menuItems.push(
|
||||||
icon: "text_snippet",
|
{
|
||||||
title: "Add from kubeconfig",
|
icon: "text_snippet",
|
||||||
onClick: () => {
|
title: "Add from kubeconfig",
|
||||||
ctx.navigate(addClusterURL());
|
onClick: () => ctx.navigate(addClusterURL()),
|
||||||
}
|
},
|
||||||
});
|
{
|
||||||
|
icon: "settings",
|
||||||
|
title: "Sync kubeconfig file(s)",
|
||||||
|
onClick: () => ctx.navigate(preferencesURL({ fragment: "kube-sync" })),
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -209,6 +209,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
* Getting default directory to download kubectl binaries
|
* Getting default directory to download kubectl binaries
|
||||||
* @returns string
|
* @returns string
|
||||||
*/
|
*/
|
||||||
export function getDefaultKubectlPath(): string {
|
export function getDefaultKubectlDownloadPath(): string {
|
||||||
return path.join((app || remote.app).getPath("userData"), "binaries");
|
return path.join((app || remote.app).getPath("userData"), "binaries");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -273,6 +273,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||||
const removeItems = [
|
const removeItems = [
|
||||||
registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension),
|
registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension),
|
||||||
|
registries.AppPreferenceKindRegistry.getInstance().add(extension.appPreferenceKinds),
|
||||||
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
|
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
|
||||||
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
|
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
|
||||||
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
clusterPageMenus: registries.ClusterPageMenuRegistration[] = [];
|
clusterPageMenus: registries.ClusterPageMenuRegistration[] = [];
|
||||||
kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = [];
|
kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = [];
|
||||||
appPreferences: registries.AppPreferenceRegistration[] = [];
|
appPreferences: registries.AppPreferenceRegistration[] = [];
|
||||||
|
appPreferenceKinds: registries.AppPreferenceKindRegistration[] = [];
|
||||||
entitySettings: registries.EntitySettingRegistration[] = [];
|
entitySettings: registries.EntitySettingRegistration[] = [];
|
||||||
statusBarItems: registries.StatusBarRegistration[] = [];
|
statusBarItems: registries.StatusBarRegistration[] = [];
|
||||||
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
|
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
|
||||||
|
|||||||
@ -19,30 +19,87 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { IComputedValue } from "mobx/dist/internal";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface AppPreferenceComponents {
|
export interface AppPreferenceComponents {
|
||||||
Hint: React.ComponentType<any>;
|
/**
|
||||||
|
* This will be rendered below the `<Input>` with slightly smaller font size
|
||||||
|
*
|
||||||
|
* @optional
|
||||||
|
*/
|
||||||
|
Hint?: React.ComponentType<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component for rendering the interactive part of the setting
|
||||||
|
*/
|
||||||
Input: React.ComponentType<any>;
|
Input: React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppPreferenceRegistration {
|
export interface AppPreferenceRegistration {
|
||||||
|
/**
|
||||||
|
* The text that will be displayed as the title to the preference
|
||||||
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of your setting, used for several purposes including the navigation
|
||||||
|
* to specific settings.
|
||||||
|
*
|
||||||
|
* @optional If not provided then computed from `title`
|
||||||
|
*/
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which preferences tab to display this setting.
|
||||||
|
*
|
||||||
|
* @default "extensions"
|
||||||
|
*/
|
||||||
showInPreferencesTab?: string;
|
showInPreferencesTab?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function for hiding the setting. If the function returns true then this
|
||||||
|
* setting will not be rendered.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hide?: boolean | IComputedValue<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The components used for rendering the settings
|
||||||
|
*/
|
||||||
components: AppPreferenceComponents;
|
components: AppPreferenceComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisteredAppPreference extends AppPreferenceRegistration {
|
export type RegisteredAppPreference = Required<AppPreferenceRegistration>;
|
||||||
|
|
||||||
|
export interface AppPreferenceKindRegistration {
|
||||||
id: string;
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are the default preferences kinds provided by Lens
|
||||||
|
*/
|
||||||
|
export enum AppPreferenceKind {
|
||||||
|
Application = "application",
|
||||||
|
Proxy = "proxy",
|
||||||
|
Kubernetes = "kubernetes",
|
||||||
|
Telemetry = "telemetry",
|
||||||
|
Extensions = "extensions",
|
||||||
|
Other = "other"
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AppPreferenceRegistry extends BaseRegistry<AppPreferenceRegistration, RegisteredAppPreference> {
|
export class AppPreferenceRegistry extends BaseRegistry<AppPreferenceRegistration, RegisteredAppPreference> {
|
||||||
getRegisteredItem(item: AppPreferenceRegistration): RegisteredAppPreference {
|
getRegisteredItem({ id, showInPreferencesTab, hide = false, ...item}: AppPreferenceRegistration): RegisteredAppPreference {
|
||||||
return {
|
return {
|
||||||
id: item.id || item.title.toLowerCase().replace(/[^0-9a-zA-Z]+/g, "-"),
|
id: id || item.title.toLowerCase().replace(/[^0-9a-zA-Z]+/g, "-"),
|
||||||
|
showInPreferencesTab: showInPreferencesTab || AppPreferenceKind.Extensions,
|
||||||
|
hide,
|
||||||
...item,
|
...item,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AppPreferenceKindRegistry extends BaseRegistry<AppPreferenceKindRegistration> {}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { customRequest } from "../common/request";
|
|||||||
import { getBundledKubectlVersion } from "../common/utils/app-version";
|
import { getBundledKubectlVersion } from "../common/utils/app-version";
|
||||||
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
|
import type { SelectOption } from "../renderer/components/select";
|
||||||
|
|
||||||
const bundledVersion = getBundledKubectlVersion();
|
const bundledVersion = getBundledKubectlVersion();
|
||||||
const kubectlMap: Map<string, string> = new Map([
|
const kubectlMap: Map<string, string> = new Map([
|
||||||
@ -55,6 +56,12 @@ const packageMirrors: Map<string, string> = new Map([
|
|||||||
["default", "https://storage.googleapis.com/kubernetes-release/release"],
|
["default", "https://storage.googleapis.com/kubernetes-release/release"],
|
||||||
["china", "https://mirror.azure.cn/kubernetes/kubectl"]
|
["china", "https://mirror.azure.cn/kubernetes/kubectl"]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const downloadMirrorOptions: SelectOption<string>[] = [
|
||||||
|
{ value: "default", label: "Default (Google)" },
|
||||||
|
{ value: "china", label: "China (Azure)" },
|
||||||
|
];
|
||||||
|
|
||||||
let bundledPath: string;
|
let bundledPath: string;
|
||||||
const initScriptVersionString = "# lens-initscript v3\n";
|
const initScriptVersionString = "# lens-initscript v3\n";
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,8 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
rootElem.classList.toggle("is-mac", isMac);
|
rootElem.classList.toggle("is-mac", isMac);
|
||||||
|
|
||||||
initializers.initRegistries();
|
initializers.initRegistries();
|
||||||
|
initializers.initAppPreferenceKindRegistry();
|
||||||
|
initializers.initAppPreferenceRegistry();
|
||||||
initializers.initCommandRegistry();
|
initializers.initCommandRegistry();
|
||||||
initializers.initEntitySettingsRegistry();
|
initializers.initEntitySettingsRegistry();
|
||||||
initializers.initKubeObjectMenuRegistry();
|
initializers.initKubeObjectMenuRegistry();
|
||||||
|
|||||||
@ -20,19 +20,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { remote } from "electron";
|
|
||||||
import { Avatar, IconButton, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText, Paper } from "@material-ui/core";
|
import { Avatar, IconButton, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText, Paper } from "@material-ui/core";
|
||||||
import { Description, Folder, Delete, HelpOutline } from "@material-ui/icons";
|
import { Description, Folder, Delete, HelpOutline } from "@material-ui/icons";
|
||||||
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store";
|
import { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store";
|
||||||
import { Button } from "../button";
|
|
||||||
import { SubTitle } from "../layout/sub-title";
|
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import logger from "../../../main/logger";
|
import logger from "../../../main/logger";
|
||||||
import { iter } from "../../utils";
|
import { iter } from "../../utils";
|
||||||
import { isWindows } from "../../../common/vars";
|
import { isWindows } from "../../../common/vars";
|
||||||
|
import { PathPicker } from "../path-picker";
|
||||||
|
|
||||||
interface SyncInfo {
|
interface SyncInfo {
|
||||||
type: "file" | "folder" | "unknown";
|
type: "file" | "folder" | "unknown";
|
||||||
@ -70,8 +68,6 @@ async function getMapEntry({ filePath, ...data}: KubeconfigSyncEntry): Promise<[
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectPathOptions = ("openFile" | "openDirectory")[];
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class KubeconfigSyncs extends React.Component {
|
export class KubeconfigSyncs extends React.Component {
|
||||||
syncs = observable.map<string, Value>();
|
syncs = observable.map<string, Value>();
|
||||||
@ -109,24 +105,13 @@ export class KubeconfigSyncs extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async openDialog(message: string, actions: SelectPathOptions) {
|
onPick = async (filePaths: string[]) => {
|
||||||
const { dialog, BrowserWindow } = remote;
|
|
||||||
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
|
||||||
properties: ["showHiddenFiles", "multiSelections", ...actions],
|
|
||||||
message,
|
|
||||||
buttonLabel: "Sync",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (canceled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newEntries = await Promise.all(filePaths.map(filePath => getMapEntry({ filePath })));
|
const newEntries = await Promise.all(filePaths.map(filePath => getMapEntry({ filePath })));
|
||||||
|
|
||||||
for (const [filePath, info] of newEntries) {
|
for (const [filePath, info] of newEntries) {
|
||||||
this.syncs.set(filePath, info);
|
this.syncs.set(filePath, info);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
renderEntryIcon(entry: Entry) {
|
renderEntryIcon(entry: Entry) {
|
||||||
switch (entry.info.type) {
|
switch (entry.info.type) {
|
||||||
@ -188,27 +173,30 @@ export class KubeconfigSyncs extends React.Component {
|
|||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<Button
|
<PathPicker
|
||||||
primary
|
|
||||||
label="Sync file(s)"
|
label="Sync file(s)"
|
||||||
className="box grow"
|
className="box grow"
|
||||||
onClick={() => void this.openDialog("Sync file(s)", ["openFile"])}
|
onPick={this.onPick}
|
||||||
|
buttonLabel="Sync"
|
||||||
|
properties={["showHiddenFiles", "multiSelections", "openFile"]}
|
||||||
/>
|
/>
|
||||||
<Button
|
<PathPicker
|
||||||
primary
|
|
||||||
label="Sync folder(s)"
|
label="Sync folder(s)"
|
||||||
className="box grow"
|
className="box grow"
|
||||||
onClick={() => void this.openDialog("Sync folder(s)", ["openDirectory"])}
|
onPick={this.onPick}
|
||||||
|
buttonLabel="Sync"
|
||||||
|
properties={["showHiddenFiles", "multiSelections", "openDirectory"]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<PathPicker
|
||||||
primary
|
|
||||||
label="Sync file(s) and folder(s)"
|
label="Sync file(s) and folder(s)"
|
||||||
onClick={() => void this.openDialog("Sync file(s) and folder(s)", ["openFile", "openDirectory"])}
|
onPick={this.onPick}
|
||||||
|
buttonLabel="Sync"
|
||||||
|
properties={["showHiddenFiles", "multiSelections", "openFile", "openDirectory"]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -216,14 +204,8 @@ export class KubeconfigSyncs extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="small">
|
{this.renderSyncButtons()}
|
||||||
<SubTitle title="Files and Folders to sync" />
|
{this.renderEntries()}
|
||||||
{this.renderSyncButtons()}
|
|
||||||
<div className="hint">
|
|
||||||
Sync an individual file or all files in a folder (non-recursive).
|
|
||||||
</div>
|
|
||||||
{this.renderEntries()}
|
|
||||||
</section>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,111 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Input, InputValidators } from "../input";
|
|
||||||
import { SubTitle } from "../layout/sub-title";
|
|
||||||
import { getDefaultKubectlPath, UserStore } from "../../../common/user-store";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { bundledKubectlPath } from "../../../main/kubectl";
|
|
||||||
import { SelectOption, Select } from "../select";
|
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
|
||||||
|
|
||||||
export const KubectlBinaries = observer(() => {
|
|
||||||
const userStore = UserStore.getInstance();
|
|
||||||
const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || "");
|
|
||||||
const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || "");
|
|
||||||
const pathValidator = downloadPath ? InputValidators.isPath : undefined;
|
|
||||||
|
|
||||||
const downloadMirrorOptions: SelectOption<string>[] = [
|
|
||||||
{ value: "default", label: "Default (Google)" },
|
|
||||||
{ value: "china", label: "China (Azure)" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const save = () => {
|
|
||||||
userStore.downloadBinariesPath = downloadPath;
|
|
||||||
userStore.kubectlBinariesPath = binariesPath;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section className="small">
|
|
||||||
<SubTitle title="Kubectl binary download"/>
|
|
||||||
<FormSwitch
|
|
||||||
control={
|
|
||||||
<Switcher
|
|
||||||
checked={userStore.downloadKubectlBinaries}
|
|
||||||
onChange={v => userStore.downloadKubectlBinaries = v.target.checked}
|
|
||||||
name="kubectl-download"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Download kubectl binaries matching the Kubernetes cluster version"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr className="small"/>
|
|
||||||
|
|
||||||
<section className="small">
|
|
||||||
<SubTitle title="Download mirror" />
|
|
||||||
<Select
|
|
||||||
placeholder="Download mirror for kubectl"
|
|
||||||
options={downloadMirrorOptions}
|
|
||||||
value={userStore.downloadMirror}
|
|
||||||
onChange={({ value }: SelectOption) => userStore.downloadMirror = value}
|
|
||||||
disabled={!userStore.downloadKubectlBinaries}
|
|
||||||
themeName="lens"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr className="small"/>
|
|
||||||
|
|
||||||
<section className="small">
|
|
||||||
<SubTitle title="Directory for binaries" />
|
|
||||||
<Input
|
|
||||||
theme="round-black"
|
|
||||||
value={userStore.downloadBinariesPath}
|
|
||||||
placeholder={getDefaultKubectlPath()}
|
|
||||||
validators={pathValidator}
|
|
||||||
onChange={setDownloadPath}
|
|
||||||
onBlur={save}
|
|
||||||
disabled={!userStore.downloadKubectlBinaries}
|
|
||||||
/>
|
|
||||||
<div className="hint">
|
|
||||||
The directory to download binaries into.
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr className="small"/>
|
|
||||||
|
|
||||||
<section className="small">
|
|
||||||
<SubTitle title="Path to kubectl binary" />
|
|
||||||
<Input
|
|
||||||
theme="round-black"
|
|
||||||
placeholder={bundledKubectlPath()}
|
|
||||||
value={binariesPath}
|
|
||||||
validators={pathValidator}
|
|
||||||
onChange={setBinariesPath}
|
|
||||||
onBlur={save}
|
|
||||||
disabled={userStore.downloadKubectlBinaries}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@ -22,67 +22,39 @@
|
|||||||
import "./preferences.scss";
|
import "./preferences.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import moment from "moment-timezone";
|
import { observable, reaction, makeObservable } from "mobx";
|
||||||
import { computed, observable, reaction, makeObservable } from "mobx";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
|
||||||
import { isWindows } from "../../../common/vars";
|
import { AppPreferenceKind, AppPreferenceKindRegistry, AppPreferenceRegistry, RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry";
|
||||||
import { AppPreferenceRegistry, RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry";
|
|
||||||
import { UserStore } from "../../../common/user-store";
|
|
||||||
import { ThemeStore } from "../../theme.store";
|
|
||||||
import { Input } from "../input";
|
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { Select, SelectOption } from "../select";
|
|
||||||
import { HelmCharts } from "./helm-charts";
|
|
||||||
import { KubectlBinaries } from "./kubectl-binaries";
|
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { Tab, Tabs } from "../tabs";
|
import { Tab, Tabs } from "../tabs";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
|
||||||
import { KubeconfigSyncs } from "./kubeconfig-syncs";
|
|
||||||
import { SettingLayout } from "../layout/setting-layout";
|
import { SettingLayout } from "../layout/setting-layout";
|
||||||
import { Checkbox } from "../checkbox";
|
import { iter } from "../../utils";
|
||||||
import { sentryDsn } from "../../../common/vars";
|
|
||||||
|
|
||||||
enum Pages {
|
|
||||||
Application = "application",
|
|
||||||
Proxy = "proxy",
|
|
||||||
Kubernetes = "kubernetes",
|
|
||||||
Telemetry = "telemetry",
|
|
||||||
Extensions = "extensions",
|
|
||||||
Other = "other"
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Preferences extends React.Component {
|
export class Preferences extends React.Component {
|
||||||
@observable httpProxy = UserStore.getInstance().httpsProxy || "";
|
@observable activeTab: string = AppPreferenceKind.Application;
|
||||||
@observable shell = UserStore.getInstance().shell || "";
|
|
||||||
@observable activeTab = Pages.Application;
|
|
||||||
|
|
||||||
constructor(props: {}) {
|
constructor(props: {}) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get themeOptions(): SelectOption<string>[] {
|
|
||||||
return ThemeStore.getInstance().themes.map(theme => ({
|
|
||||||
label: theme.name,
|
|
||||||
value: theme.id,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
|
|
||||||
label: zone,
|
|
||||||
value: zone,
|
|
||||||
}));
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => navigation.location.hash, hash => {
|
reaction(() => navigation.location.hash, hash => {
|
||||||
const fragment = hash.slice(1); // hash is /^(#\w.)?$/
|
const fragment = hash.slice(1); // hash is /^(#\w.)?$/
|
||||||
|
|
||||||
if (fragment) {
|
if (fragment.length > 0) {
|
||||||
// ignore empty fragments
|
const settingsItem = AppPreferenceRegistry.getInstance().getItems().find(item => item.id === fragment);
|
||||||
document.getElementById(fragment)?.scrollIntoView();
|
|
||||||
|
if (settingsItem) {
|
||||||
|
this.activeTab = settingsItem.showInPreferencesTab;
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById(fragment)?.scrollIntoView();
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
fireImmediately: true
|
fireImmediately: true
|
||||||
@ -90,206 +62,76 @@ export class Preferences extends React.Component {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTabChange = (tabId: Pages) => {
|
onTabChange = (tabId: string) => {
|
||||||
this.activeTab = tabId;
|
this.activeTab = tabId;
|
||||||
};
|
};
|
||||||
|
|
||||||
renderNavigation() {
|
renderNavigation() {
|
||||||
const extensions = AppPreferenceRegistry.getInstance().getItems().filter(e => !e.showInPreferencesTab);
|
const activeTabs = new Set(
|
||||||
|
iter.map(
|
||||||
|
iter.filter(
|
||||||
|
AppPreferenceRegistry.getInstance().getItems(),
|
||||||
|
item => !item.hide,
|
||||||
|
),
|
||||||
|
item => item.showInPreferencesTab
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const tabs = AppPreferenceKindRegistry.getInstance().getItems();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
|
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
|
||||||
<div className="header">Preferences</div>
|
<div className="header">Preferences</div>
|
||||||
<Tab value={Pages.Application} label="Application" data-testid="application-tab"/>
|
{
|
||||||
<Tab value={Pages.Proxy} label="Proxy" data-testid="proxy-tab"/>
|
...tabs
|
||||||
<Tab value={Pages.Kubernetes} label="Kubernetes" data-testid="kube-tab"/>
|
.filter(tab => activeTabs.has(tab.id))
|
||||||
<Tab value={Pages.Telemetry} label="Telemetry" data-testid="telemetry-tab"/>
|
.map(tab => <Tab key={tab.id} value={tab.id} label={tab.title} data-testid={`${tab.id}-tab`} />)
|
||||||
{extensions.length > 0 &&
|
|
||||||
<Tab value={Pages.Extensions} label="Extensions" data-testid="extensions-tab"/>
|
|
||||||
}
|
}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderExtension({ title, id, components: { Hint, Input } }: RegisteredAppPreference) {
|
renderActiveTab() {
|
||||||
|
const entries = AppPreferenceRegistry.getInstance().getItems().filter(item => (
|
||||||
|
item.showInPreferencesTab === this.activeTab
|
||||||
|
&& !item.hide
|
||||||
|
));
|
||||||
|
const tab = AppPreferenceKindRegistry.getInstance().getItems().find(item => item.id === this.activeTab);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id={tab.id}>
|
||||||
|
<h2 data-testid={`${tab.id}-header`}>{tab.title}</h2>
|
||||||
|
{...entries.map(this.renderPreference)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPreference = ({ title, id, components: { Hint, Input } }: RegisteredAppPreference) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={id}>
|
<React.Fragment key={id}>
|
||||||
<section id={id} className="small">
|
<section id={id} className="small">
|
||||||
<SubTitle title={title}/>
|
<SubTitle title={title}/>
|
||||||
<Input/>
|
<Input/>
|
||||||
<div className="hint">
|
{
|
||||||
<Hint/>
|
Hint && (
|
||||||
</div>
|
<small className="hint">
|
||||||
|
<Hint/>
|
||||||
|
</small>
|
||||||
|
)
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
<hr className="small"/>
|
<hr className="small"/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const extensions = AppPreferenceRegistry.getInstance().getItems();
|
|
||||||
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry);
|
|
||||||
const defaultShell = process.env.SHELL
|
|
||||||
|| process.env.PTYSHELL
|
|
||||||
|| (
|
|
||||||
isWindows
|
|
||||||
? "powershell.exe"
|
|
||||||
: "System default shell"
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingLayout
|
<SettingLayout
|
||||||
navigation={this.renderNavigation()}
|
navigation={this.renderNavigation()}
|
||||||
className="Preferences"
|
className="Preferences"
|
||||||
contentGaps={false}
|
contentGaps={false}
|
||||||
>
|
>
|
||||||
{this.activeTab == Pages.Application && (
|
{this.renderActiveTab()}
|
||||||
<section id="application">
|
|
||||||
<h2 data-testid="application-header">Application</h2>
|
|
||||||
<section id="appearance">
|
|
||||||
<SubTitle title="Theme"/>
|
|
||||||
<Select
|
|
||||||
options={this.themeOptions}
|
|
||||||
value={UserStore.getInstance().colorTheme}
|
|
||||||
onChange={({ value }: SelectOption) => UserStore.getInstance().colorTheme = value}
|
|
||||||
themeName="lens"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr/>
|
|
||||||
|
|
||||||
<section id="shell">
|
|
||||||
<SubTitle title="Terminal Shell Path"/>
|
|
||||||
<Input
|
|
||||||
theme="round-black"
|
|
||||||
placeholder={defaultShell}
|
|
||||||
value={this.shell}
|
|
||||||
onChange={v => this.shell = v}
|
|
||||||
onBlur={() => UserStore.getInstance().shell = this.shell}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr/>
|
|
||||||
|
|
||||||
<section id="other">
|
|
||||||
<SubTitle title="Start-up"/>
|
|
||||||
<FormSwitch
|
|
||||||
control={
|
|
||||||
<Switcher
|
|
||||||
checked={UserStore.getInstance().openAtLogin}
|
|
||||||
onChange={v => UserStore.getInstance().openAtLogin = v.target.checked}
|
|
||||||
name="startup"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Automatically start Lens on login"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<section id="locale">
|
|
||||||
<SubTitle title="Locale Timezone" />
|
|
||||||
<Select
|
|
||||||
options={this.timezoneOptions}
|
|
||||||
value={UserStore.getInstance().localeTimezone}
|
|
||||||
onChange={({ value }: SelectOption) => UserStore.getInstance().setLocaleTimezone(value)}
|
|
||||||
themeName="lens"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
{this.activeTab == Pages.Proxy && (
|
|
||||||
<section id="proxy">
|
|
||||||
<section>
|
|
||||||
<h2 data-testid="proxy-header">Proxy</h2>
|
|
||||||
<SubTitle title="HTTP Proxy"/>
|
|
||||||
<Input
|
|
||||||
theme="round-black"
|
|
||||||
placeholder="Type HTTP proxy url (example: http://proxy.acme.org:8080)"
|
|
||||||
value={this.httpProxy}
|
|
||||||
onChange={v => this.httpProxy = v}
|
|
||||||
onBlur={() => UserStore.getInstance().httpsProxy = this.httpProxy}
|
|
||||||
/>
|
|
||||||
<small className="hint">
|
|
||||||
Proxy is used only for non-cluster communication.
|
|
||||||
</small>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr className="small"/>
|
|
||||||
|
|
||||||
<section className="small">
|
|
||||||
<SubTitle title="Certificate Trust"/>
|
|
||||||
<FormSwitch
|
|
||||||
control={
|
|
||||||
<Switcher
|
|
||||||
checked={UserStore.getInstance().allowUntrustedCAs}
|
|
||||||
onChange={v => UserStore.getInstance().allowUntrustedCAs = v.target.checked}
|
|
||||||
name="startup"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Allow untrusted Certificate Authorities"
|
|
||||||
/>
|
|
||||||
<small className="hint">
|
|
||||||
This will make Lens to trust ANY certificate authority without any validations.{" "}
|
|
||||||
Needed with some corporate proxies that do certificate re-writing.{" "}
|
|
||||||
Does not affect cluster communications!
|
|
||||||
</small>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
{this.activeTab == Pages.Kubernetes && (
|
|
||||||
<section id="kubernetes">
|
|
||||||
<section id="kubectl">
|
|
||||||
<h2 data-testid="kubernetes-header">Kubernetes</h2>
|
|
||||||
<KubectlBinaries />
|
|
||||||
</section>
|
|
||||||
<hr/>
|
|
||||||
<section id="kube-sync">
|
|
||||||
<h2 data-testid="kubernetes-sync-header">Kubeconfig Syncs</h2>
|
|
||||||
<KubeconfigSyncs />
|
|
||||||
</section>
|
|
||||||
<hr/>
|
|
||||||
<section id="helm">
|
|
||||||
<h2>Helm Charts</h2>
|
|
||||||
<HelmCharts/>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
{this.activeTab == Pages.Telemetry && (
|
|
||||||
<section id="telemetry">
|
|
||||||
<h2 data-testid="telemetry-header">Telemetry</h2>
|
|
||||||
{telemetryExtensions.map(this.renderExtension)}
|
|
||||||
{sentryDsn ? (
|
|
||||||
<React.Fragment key='sentry'>
|
|
||||||
<section id='sentry' className="small">
|
|
||||||
<SubTitle title='Automatic Error Reporting' />
|
|
||||||
<Checkbox
|
|
||||||
label="Allow automatic error reporting"
|
|
||||||
value={UserStore.getInstance().allowErrorReporting}
|
|
||||||
onChange={value => {
|
|
||||||
UserStore.getInstance().allowErrorReporting = value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="hint">
|
|
||||||
<span>
|
|
||||||
Automatic error reports provide vital information about issues and application crashes.
|
|
||||||
It is highly recommended to keep this feature enabled to ensure fast turnaround for issues you might encounter.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<hr className="small" />
|
|
||||||
</React.Fragment>) :
|
|
||||||
// we don't need to shows the checkbox at all if Sentry dsn is not a valid url
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
{this.activeTab == Pages.Extensions && (
|
|
||||||
<section id="extensions">
|
|
||||||
<h2>Extensions</h2>
|
|
||||||
{extensions.filter(e => !e.showInPreferencesTab).map(this.renderExtension)}
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</SettingLayout>
|
</SettingLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/renderer/components/path-picker/index.ts
Normal file
22
src/renderer/components/path-picker/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./path-picker";
|
||||||
71
src/renderer/components/path-picker/path-picker.tsx
Normal file
71
src/renderer/components/path-picker/path-picker.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FileFilter, OpenDialogOptions, remote } from "electron";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import React from "react";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
import { Button } from "../button";
|
||||||
|
|
||||||
|
export interface PathPickerProps {
|
||||||
|
className?: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onPick?: (paths: string[]) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
defaultPath?: string;
|
||||||
|
buttonLabel?: string;
|
||||||
|
filters?: FileFilter[];
|
||||||
|
properties?: OpenDialogOptions["properties"];
|
||||||
|
securityScopedBookmarks?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class PathPicker extends React.Component<PathPickerProps> {
|
||||||
|
async onClick() {
|
||||||
|
const { onPick, onCancel, label, className, disabled, ...dialogOptions } = this.props;
|
||||||
|
const { dialog, BrowserWindow } = remote;
|
||||||
|
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
||||||
|
message: label,
|
||||||
|
...dialogOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canceled) {
|
||||||
|
onCancel?.();
|
||||||
|
} else {
|
||||||
|
onPick?.(filePaths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, label, disabled } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
label={label}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cssNames("PathPicker", className)}
|
||||||
|
onClick={() => void this.onClick()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/renderer/initializers/app-preferences-kind-registry.ts
Normal file
52
src/renderer/initializers/app-preferences-kind-registry.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AppPreferenceKind, AppPreferenceKindRegistry } from "../../extensions/registries";
|
||||||
|
|
||||||
|
export function initAppPreferenceKindRegistry() {
|
||||||
|
AppPreferenceKindRegistry.getInstance()
|
||||||
|
.add([
|
||||||
|
{
|
||||||
|
id: AppPreferenceKind.Application,
|
||||||
|
title: "Application",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: AppPreferenceKind.Proxy,
|
||||||
|
title: "Proxy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: AppPreferenceKind.Kubernetes,
|
||||||
|
title: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: AppPreferenceKind.Telemetry,
|
||||||
|
title: "Telemetry",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: AppPreferenceKind.Other,
|
||||||
|
title: "Other",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: AppPreferenceKind.Extensions,
|
||||||
|
title: "Extensions",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
301
src/renderer/initializers/app-preferences-registry.tsx
Normal file
301
src/renderer/initializers/app-preferences-registry.tsx
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import moment from "moment";
|
||||||
|
import React from "react";
|
||||||
|
import { getDefaultKubectlDownloadPath, UserStore } from "../../common/user-store";
|
||||||
|
import { isWindows, sentryDsn } from "../../common/vars";
|
||||||
|
import { AppPreferenceKind, AppPreferenceRegistry } from "../../extensions/registries";
|
||||||
|
import { bundledKubectlPath, downloadMirrorOptions } from "../../main/kubectl";
|
||||||
|
import { HelmCharts } from "../components/+preferences/helm-charts";
|
||||||
|
import { KubeconfigSyncs } from "../components/+preferences/kubeconfig-syncs";
|
||||||
|
import { Button } from "../components/button";
|
||||||
|
import { Checkbox } from "../components/checkbox";
|
||||||
|
import { Input } from "../components/input";
|
||||||
|
import { PathPicker } from "../components/path-picker";
|
||||||
|
import { Select, SelectOption } from "../components/select";
|
||||||
|
import { FormSwitch, Switcher } from "../components/switch";
|
||||||
|
import { ThemeStore } from "../theme.store";
|
||||||
|
|
||||||
|
export function initAppPreferenceRegistry() {
|
||||||
|
AppPreferenceRegistry.getInstance()
|
||||||
|
.add([
|
||||||
|
{
|
||||||
|
id: "appearance",
|
||||||
|
title: "Theme",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<Select
|
||||||
|
options={ThemeStore.getInstance().themeOptions}
|
||||||
|
value={UserStore.getInstance().colorTheme}
|
||||||
|
onChange={({ value }: SelectOption<string>) => UserStore.getInstance().colorTheme = value}
|
||||||
|
themeName="lens"
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Application,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "shell",
|
||||||
|
title: "Terminal Shell Path",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<Input
|
||||||
|
theme="round-black"
|
||||||
|
placeholder={(
|
||||||
|
process.env.SHELL
|
||||||
|
|| process.env.PTYSHELL
|
||||||
|
|| (
|
||||||
|
isWindows
|
||||||
|
? "powershell.exe"
|
||||||
|
: "System default shell"
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
onBlur={(evt) => UserStore.getInstance().shell = evt.target.innerText}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Application,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "start-up",
|
||||||
|
title: "Start-up",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<FormSwitch
|
||||||
|
control={
|
||||||
|
<Switcher
|
||||||
|
checked={UserStore.getInstance().openAtLogin}
|
||||||
|
onChange={v => UserStore.getInstance().openAtLogin = v.target.checked}
|
||||||
|
name="startup"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Automatically start Lens on login"
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Application,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "locale",
|
||||||
|
title: "Locale Timezone",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<Select
|
||||||
|
options={moment.tz.names()}
|
||||||
|
value={UserStore.getInstance().localeTimezone}
|
||||||
|
onChange={({ value }: SelectOption) => UserStore.getInstance().setLocaleTimezone(value)}
|
||||||
|
themeName="lens"
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Application,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "http-proxy",
|
||||||
|
title: "HTTP Proxy",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<Input
|
||||||
|
theme="round-black"
|
||||||
|
placeholder="Type HTTP proxy url (example: http://proxy.acme.org:8080)"
|
||||||
|
onBlur={(evt) => UserStore.getInstance().httpsProxy = evt.target.innerText}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
Hint: () => <>Proxy is used only for non-cluster communication.</>,
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Proxy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "certificate-trust",
|
||||||
|
title: "Certificate Trust",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<FormSwitch
|
||||||
|
control={
|
||||||
|
<Switcher
|
||||||
|
checked={UserStore.getInstance().allowUntrustedCAs}
|
||||||
|
onChange={v => UserStore.getInstance().allowUntrustedCAs = v.target.checked}
|
||||||
|
name="startup"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Allow untrusted Certificate Authorities"
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
Hint: () => (
|
||||||
|
<>
|
||||||
|
This will make Lens to trust ANY certificate authority without any validations.{" "}
|
||||||
|
Needed with some corporate proxies that do certificate re-writing.{" "}
|
||||||
|
Does not affect cluster communications!
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Proxy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kubectl-binary-download",
|
||||||
|
title: "Kubectl binary download",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<FormSwitch
|
||||||
|
control={
|
||||||
|
<Switcher
|
||||||
|
checked={UserStore.getInstance().downloadKubectlBinaries}
|
||||||
|
onChange={v => UserStore.getInstance().downloadKubectlBinaries = v.target.checked}
|
||||||
|
name="kubectl-download"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Download kubectl binaries matching the Kubernetes cluster version"
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Kubernetes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kubectl-download-mirror",
|
||||||
|
title: "Kubectl Download mirror",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<Select
|
||||||
|
placeholder="Download mirror for kubectl"
|
||||||
|
options={downloadMirrorOptions}
|
||||||
|
value={UserStore.getInstance().downloadMirror}
|
||||||
|
onChange={({ value }: SelectOption) => UserStore.getInstance().downloadMirror = value}
|
||||||
|
disabled={!UserStore.getInstance().downloadKubectlBinaries}
|
||||||
|
themeName="lens"
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Kubernetes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kubectl-download-directory",
|
||||||
|
title: "Directory for Kubectl binaries",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<>
|
||||||
|
<p>Current Directory:</p>
|
||||||
|
<code className="overflow-x-scroll whitespace-nowrap">
|
||||||
|
{UserStore.getInstance().downloadBinariesPath || getDefaultKubectlDownloadPath()}
|
||||||
|
</code>
|
||||||
|
<div className="flex gaps align-center">
|
||||||
|
<PathPicker
|
||||||
|
className="box grow"
|
||||||
|
label="Select target directory"
|
||||||
|
onPick={([dirPath]) => UserStore.getInstance().downloadBinariesPath = dirPath}
|
||||||
|
buttonLabel="Select"
|
||||||
|
properties={["showHiddenFiles", "openDirectory"]}
|
||||||
|
disabled={!UserStore.getInstance().downloadKubectlBinaries}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="box"
|
||||||
|
accent
|
||||||
|
label="Clear"
|
||||||
|
onClick={() => UserStore.getInstance().downloadBinariesPath = undefined}
|
||||||
|
disabled={!UserStore.getInstance().downloadKubectlBinaries || !UserStore.getInstance().downloadBinariesPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)),
|
||||||
|
Hint: () => <>The directory to download binaries into.</>,
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Kubernetes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kubectl-path",
|
||||||
|
title: "Path to kubectl binary",
|
||||||
|
components: {
|
||||||
|
Input: observer(() => (
|
||||||
|
<>
|
||||||
|
<p>Current kubectl path:</p>
|
||||||
|
<code className="overflow-x-scroll whitespace-nowrap">
|
||||||
|
{UserStore.getInstance().kubectlBinariesPath || bundledKubectlPath()}
|
||||||
|
</code>
|
||||||
|
<label>{UserStore.getInstance().kubectlBinariesPath}</label>
|
||||||
|
<div className="flex gaps align-center">
|
||||||
|
<PathPicker
|
||||||
|
className="box grow"
|
||||||
|
label="Select kubectl binary"
|
||||||
|
onPick={([binPath]) => UserStore.getInstance().kubectlBinariesPath = binPath}
|
||||||
|
buttonLabel="Select"
|
||||||
|
properties={["showHiddenFiles", "openFile"]}
|
||||||
|
disabled={UserStore.getInstance().downloadKubectlBinaries}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="box"
|
||||||
|
accent
|
||||||
|
label="Clear"
|
||||||
|
onClick={() => UserStore.getInstance().kubectlBinariesPath = undefined}
|
||||||
|
disabled={UserStore.getInstance().downloadKubectlBinaries || !UserStore.getInstance().kubectlBinariesPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Kubernetes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kube-sync",
|
||||||
|
title: "Kubeconfig Syncs",
|
||||||
|
components: {
|
||||||
|
Input: KubeconfigSyncs,
|
||||||
|
Hint: () => (
|
||||||
|
<>
|
||||||
|
Sync an individual file or all files in a folder (non-recursive).
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Kubernetes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "helm",
|
||||||
|
title: "Helm Charts",
|
||||||
|
components: {
|
||||||
|
Input: HelmCharts,
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Kubernetes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sentry",
|
||||||
|
title: "Automatic Error Reporting",
|
||||||
|
components: {
|
||||||
|
Input: () => (
|
||||||
|
<Checkbox
|
||||||
|
label="Allow automatic error reporting"
|
||||||
|
value={UserStore.getInstance().allowErrorReporting}
|
||||||
|
onChange={value => {
|
||||||
|
UserStore.getInstance().allowErrorReporting = value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
Hint: () => (
|
||||||
|
<>
|
||||||
|
Automatic error reports provide vital information about issues and application crashes.{" "}
|
||||||
|
It is highly recommended to keep this feature enabled to ensure fast turnaround for issues you might encounter.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
showInPreferencesTab: AppPreferenceKind.Telemetry,
|
||||||
|
hide: Boolean(sentryDsn),
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
@ -19,6 +19,8 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./app-preferences-kind-registry";
|
||||||
|
export * from "./app-preferences-registry";
|
||||||
export * from "./catalog-entity-detail-registry";
|
export * from "./catalog-entity-detail-registry";
|
||||||
export * from "./catalog";
|
export * from "./catalog";
|
||||||
export * from "./command-registry";
|
export * from "./command-registry";
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import * as registries from "../../extensions/registries";
|
|||||||
|
|
||||||
export function initRegistries() {
|
export function initRegistries() {
|
||||||
registries.AppPreferenceRegistry.createInstance();
|
registries.AppPreferenceRegistry.createInstance();
|
||||||
|
registries.AppPreferenceKindRegistry.createInstance();
|
||||||
registries.CatalogEntityDetailRegistry.createInstance();
|
registries.CatalogEntityDetailRegistry.createInstance();
|
||||||
registries.ClusterPageMenuRegistry.createInstance();
|
registries.ClusterPageMenuRegistry.createInstance();
|
||||||
registries.ClusterPageRegistry.createInstance();
|
registries.ClusterPageRegistry.createInstance();
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { UserStore } from "../common/user-store";
|
|||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import darkTheme from "./themes/lens-dark.json";
|
import darkTheme from "./themes/lens-dark.json";
|
||||||
import lightTheme from "./themes/lens-light.json";
|
import lightTheme from "./themes/lens-light.json";
|
||||||
|
import type { SelectOption } from "./components/select";
|
||||||
|
|
||||||
export type ThemeId = string;
|
export type ThemeId = string;
|
||||||
|
|
||||||
@ -67,6 +68,13 @@ export class ThemeStore extends Singleton {
|
|||||||
return this.allThemes.get(this.activeThemeId) ?? this.allThemes.get("lens-dark");
|
return this.allThemes.get(this.activeThemeId) ?? this.allThemes.get("lens-dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get themeOptions(): SelectOption<string>[] {
|
||||||
|
return this.themes.map(theme => ({
|
||||||
|
label: theme.name,
|
||||||
|
value: theme.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user