mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Sync kubeconfigs from catalog (#3573)
* Kubeconfig syncs from Catalog Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Allow to select directory to sync Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Convert Preferences to Route-based component Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Jump from notification to Preferences Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Adding navigateWithoutHistoryChange() method Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Cleaning up Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fixing integration tests (no check for Telemetry tab) Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fixing tests harder Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
parent
2760178196
commit
b7be3ae21a
@ -56,14 +56,13 @@ describe("Lens integration tests", () => {
|
||||
await app.client.waitUntilTextExists("[data-testid=application-header]", "Application");
|
||||
});
|
||||
|
||||
it("shows all tabs and their contents", async () => {
|
||||
await app.client.click("[data-testid=application-tab]");
|
||||
await app.client.click("[data-testid=proxy-tab]");
|
||||
await app.client.waitUntilTextExists("[data-testid=proxy-header]", "Proxy");
|
||||
await app.client.click("[data-testid=kube-tab]");
|
||||
await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "Kubernetes");
|
||||
await app.client.click("[data-testid=telemetry-tab]");
|
||||
await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "Telemetry");
|
||||
it.each([
|
||||
["application", "Application"],
|
||||
["proxy", "Proxy"],
|
||||
["kubernetes", "Kubernetes"],
|
||||
])("Can click the %s tab and see the %s header", async (tab, header) => {
|
||||
await app.client.click(`[data-testid=${tab}-tab]`);
|
||||
await app.client.waitUntilTextExists(`[data-testid=${tab}-header]`, header);
|
||||
});
|
||||
|
||||
it("ensures helm repos", async () => {
|
||||
@ -73,7 +72,7 @@ describe("Lens integration tests", () => {
|
||||
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.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)
|
||||
|
||||
@ -20,12 +20,11 @@
|
||||
*/
|
||||
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||
import { clusterActivateHandler, clusterDeleteHandler, clusterDisconnectHandler } from "../cluster-ipc";
|
||||
import { ClusterStore } from "../cluster-store";
|
||||
import { requestMain } from "../ipc";
|
||||
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||
import { addClusterURL } from "../routes";
|
||||
import { app } from "electron";
|
||||
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
import { HotbarStore } from "../hotbar-store";
|
||||
@ -43,7 +42,6 @@ export interface KubernetesClusterPrometheusMetrics {
|
||||
export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
||||
kubeconfigPath: string;
|
||||
kubeconfigContext: string;
|
||||
accessibleNamespaces?: string[];
|
||||
metrics?: {
|
||||
source: string;
|
||||
prometheus?: KubernetesClusterPrometheusMetrics;
|
||||
@ -149,7 +147,7 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
export class KubernetesClusterCategory extends CatalogCategory {
|
||||
class KubernetesClusterCategory extends CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
@ -168,20 +166,8 @@ export class KubernetesClusterCategory extends CatalogCategory {
|
||||
kind: "KubernetesCluster"
|
||||
}
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
|
||||
ctx.menuItems.push({
|
||||
icon: "text_snippet",
|
||||
title: "Add from kubeconfig",
|
||||
onClick: () => {
|
||||
ctx.navigate(addClusterURL());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
catalogCategoryRegistry.add(new KubernetesClusterCategory());
|
||||
export const kubernetesClusterCategory = new KubernetesClusterCategory();
|
||||
|
||||
catalogCategoryRegistry.add(kubernetesClusterCategory);
|
||||
|
||||
@ -123,6 +123,7 @@ export interface CatalogEntityContextMenu {
|
||||
|
||||
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
|
||||
icon: string;
|
||||
defaultAction?: boolean;
|
||||
}
|
||||
|
||||
export interface CatalogEntitySettingsMenu {
|
||||
|
||||
@ -26,4 +26,29 @@ export const preferencesRoute: RouteProps = {
|
||||
path: "/preferences"
|
||||
};
|
||||
|
||||
export const appRoute: RouteProps = {
|
||||
path: `${preferencesRoute.path}/app`
|
||||
};
|
||||
|
||||
export const proxyRoute: RouteProps = {
|
||||
path: `${preferencesRoute.path}/proxy`
|
||||
};
|
||||
|
||||
export const kubernetesRoute: RouteProps = {
|
||||
path: `${preferencesRoute.path}/kubernetes`
|
||||
};
|
||||
|
||||
export const telemetryRoute: RouteProps = {
|
||||
path: `${preferencesRoute.path}/telemetry`
|
||||
};
|
||||
|
||||
export const extensionRoute: RouteProps = {
|
||||
path: `${preferencesRoute.path}/extensions`
|
||||
};
|
||||
|
||||
export const preferencesURL = buildURL(preferencesRoute.path);
|
||||
export const appURL = buildURL(appRoute.path);
|
||||
export const proxyURL = buildURL(proxyRoute.path);
|
||||
export const kubernetesURL = buildURL(kubernetesRoute.path);
|
||||
export const telemetryURL = buildURL(telemetryRoute.path);
|
||||
export const extensionURL = buildURL(extensionRoute.path);
|
||||
|
||||
@ -213,6 +213,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
* Getting default directory to download kubectl binaries
|
||||
* @returns string
|
||||
*/
|
||||
export function getDefaultKubectlPath(): string {
|
||||
export function getDefaultKubectlDownloadPath(): string {
|
||||
return path.join((app || remote.app).getPath("userData"), "binaries");
|
||||
}
|
||||
|
||||
@ -21,6 +21,12 @@
|
||||
|
||||
import { action, ObservableMap } from "mobx";
|
||||
|
||||
export function multiSet<T, V>(map: Map<T, V>, newEntries: [T, V][]): void {
|
||||
for (const [key, val] of newEntries) {
|
||||
map.set(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtendedMap<K, V> extends Map<K, V> {
|
||||
static new<K, V>(entries?: readonly (readonly [K, V])[] | null): ExtendedMap<K, V> {
|
||||
return new ExtendedMap<K, V>(entries);
|
||||
|
||||
@ -81,6 +81,7 @@ export async function bootstrap(App: AppComponent) {
|
||||
initializers.initWelcomeMenuRegistry();
|
||||
initializers.initWorkloadsOverviewDetailRegistry();
|
||||
initializers.initCatalogEntityDetailRegistry();
|
||||
initializers.initCatalogCategoryRegistryEntries();
|
||||
initializers.initCatalog();
|
||||
initializers.initIpcRendererListeners();
|
||||
|
||||
|
||||
@ -73,9 +73,10 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
|
||||
|
||||
@boundMethod
|
||||
onButtonClick() {
|
||||
if (this.menuItems.length == 1) {
|
||||
this.menuItems[0].onClick();
|
||||
}
|
||||
const defaultAction = this.menuItems.find(item => item.defaultAction)?.onClick;
|
||||
const clickAction = defaultAction || (this.menuItems.length === 1 ? this.menuItems[0].onClick : null);
|
||||
|
||||
clickAction?.();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -97,9 +98,12 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
|
||||
{ this.menuItems.map((menuItem, index) => {
|
||||
return <SpeedDialAction
|
||||
key={index}
|
||||
icon={<Icon material={menuItem.icon} />}
|
||||
icon={<Icon material={menuItem.icon}/>}
|
||||
tooltipTitle={menuItem.title}
|
||||
onClick={() => menuItem.onClick()}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
menuItem.onClick();
|
||||
}}
|
||||
TooltipClasses={{
|
||||
popper: "catalogSpeedDialPopper"
|
||||
}}
|
||||
|
||||
104
src/renderer/components/+preferences/application.tsx
Normal file
104
src/renderer/components/+preferences/application.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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 from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { Input } from "../input";
|
||||
import { isWindows } from "../../../common/vars";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
import moment from "moment-timezone";
|
||||
|
||||
const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
|
||||
label: zone,
|
||||
value: zone,
|
||||
}));
|
||||
|
||||
export const Application = observer(() => {
|
||||
const defaultShell = process.env.SHELL
|
||||
|| process.env.PTYSHELL
|
||||
|| (
|
||||
isWindows
|
||||
? "powershell.exe"
|
||||
: "System default shell"
|
||||
);
|
||||
|
||||
const [shell, setShell] = React.useState(UserStore.getInstance().shell || "");
|
||||
|
||||
return (
|
||||
<section id="application">
|
||||
<h2 data-testid="application-header">Application</h2>
|
||||
<section id="appearance">
|
||||
<SubTitle title="Theme"/>
|
||||
<Select
|
||||
options={ThemeStore.getInstance().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={shell}
|
||||
onChange={v => setShell(v)}
|
||||
onBlur={() => UserStore.getInstance().shell = 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={timezoneOptions}
|
||||
value={UserStore.getInstance().localeTimezone}
|
||||
onChange={({ value }: SelectOption) => UserStore.getInstance().setLocaleTimezone(value)}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
38
src/renderer/components/+preferences/extensions.tsx
Normal file
38
src/renderer/components/+preferences/extensions.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
import { AppPreferenceRegistry } from "../../../extensions/registries";
|
||||
import { ExtensionSettings } from "./preferences";
|
||||
|
||||
export const Extensions = observer(() => {
|
||||
const extensions = AppPreferenceRegistry.getInstance().getItems();
|
||||
|
||||
return (
|
||||
<section id="extensions">
|
||||
<h2>Extensions</h2>
|
||||
{extensions.filter(e => !e.showInPreferencesTab).map((extension) =>
|
||||
<ExtensionSettings key={extension.id} {...extension}/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
});
|
||||
@ -20,19 +20,17 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { remote } from "electron";
|
||||
import { Avatar, IconButton, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText, Paper } from "@material-ui/core";
|
||||
import { Description, Folder, Delete, HelpOutline } from "@material-ui/icons";
|
||||
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import fse from "fs-extra";
|
||||
import { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store";
|
||||
import { Button } from "../button";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Spinner } from "../spinner";
|
||||
import logger from "../../../main/logger";
|
||||
import { iter } from "../../utils";
|
||||
import { iter, multiSet } from "../../utils";
|
||||
import { isWindows } from "../../../common/vars";
|
||||
import { PathPicker } from "../path-picker/path-picker";
|
||||
|
||||
interface SyncInfo {
|
||||
type: "file" | "folder" | "unknown";
|
||||
@ -70,7 +68,9 @@ async function getMapEntry({ filePath, ...data}: KubeconfigSyncEntry): Promise<[
|
||||
}
|
||||
}
|
||||
|
||||
type SelectPathOptions = ("openFile" | "openDirectory")[];
|
||||
export async function getAllEntries(filePaths: string[]): Promise<[string, Value][]> {
|
||||
return Promise.all(filePaths.map(filePath => getMapEntry({ filePath })));
|
||||
}
|
||||
|
||||
@observer
|
||||
export class KubeconfigSyncs extends React.Component {
|
||||
@ -109,24 +109,7 @@ export class KubeconfigSyncs extends React.Component {
|
||||
}
|
||||
|
||||
@action
|
||||
async openDialog(message: string, actions: SelectPathOptions) {
|
||||
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 })));
|
||||
|
||||
for (const [filePath, info] of newEntries) {
|
||||
this.syncs.set(filePath, info);
|
||||
}
|
||||
}
|
||||
onPick = async (filePaths: string[]) => multiSet(this.syncs, await getAllEntries(filePaths));
|
||||
|
||||
renderEntryIcon(entry: Entry) {
|
||||
switch (entry.info.type) {
|
||||
@ -188,27 +171,30 @@ export class KubeconfigSyncs extends React.Component {
|
||||
if (isWindows) {
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<Button
|
||||
primary
|
||||
<PathPicker
|
||||
label="Sync file(s)"
|
||||
className="box grow"
|
||||
onClick={() => void this.openDialog("Sync file(s)", ["openFile"])}
|
||||
onPick={this.onPick}
|
||||
buttonLabel="Sync"
|
||||
properties={["showHiddenFiles", "multiSelections", "openFile"]}
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
<PathPicker
|
||||
label="Sync folder(s)"
|
||||
className="box grow"
|
||||
onClick={() => void this.openDialog("Sync folder(s)", ["openDirectory"])}
|
||||
onPick={this.onPick}
|
||||
buttonLabel="Sync"
|
||||
properties={["showHiddenFiles", "multiSelections", "openDirectory"]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
primary
|
||||
<PathPicker
|
||||
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 +202,8 @@ export class KubeconfigSyncs extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<section className="small">
|
||||
<SubTitle title="Files and Folders to sync" />
|
||||
{this.renderSyncButtons()}
|
||||
<div className="hint">
|
||||
Sync an individual file or all files in a folder (non-recursive).
|
||||
</div>
|
||||
{this.renderEntries()}
|
||||
</section>
|
||||
{this.renderSyncButtons()}
|
||||
{this.renderEntries()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { Input, InputValidators } from "../input";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { getDefaultKubectlPath, UserStore } from "../../../common/user-store";
|
||||
import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-store";
|
||||
import { observer } from "mobx-react";
|
||||
import { bundledKubectlPath } from "../../../main/kubectl";
|
||||
import { SelectOption, Select } from "../select";
|
||||
@ -81,7 +81,7 @@ export const KubectlBinaries = observer(() => {
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={userStore.downloadBinariesPath}
|
||||
placeholder={getDefaultKubectlPath()}
|
||||
placeholder={getDefaultKubectlDownloadPath()}
|
||||
validators={pathValidator}
|
||||
onChange={setDownloadPath}
|
||||
onBlur={save}
|
||||
|
||||
47
src/renderer/components/+preferences/kubernetes.tsx
Normal file
47
src/renderer/components/+preferences/kubernetes.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
|
||||
import { HelmCharts } from "./helm-charts";
|
||||
import { KubeconfigSyncs } from "./kubeconfig-syncs";
|
||||
import { KubectlBinaries } from "./kubectl-binaries";
|
||||
|
||||
export const Kubernetes = observer(() => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
});
|
||||
@ -18,279 +18,100 @@
|
||||
* 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 "./preferences.scss";
|
||||
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import moment from "moment-timezone";
|
||||
import { computed, observable, reaction, makeObservable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { matchPath, Redirect, Route, RouteProps, Switch } from "react-router";
|
||||
|
||||
import { isWindows } from "../../../common/vars";
|
||||
import {
|
||||
appRoute,
|
||||
appURL,
|
||||
extensionRoute,
|
||||
extensionURL,
|
||||
kubernetesRoute,
|
||||
kubernetesURL,
|
||||
preferencesURL,
|
||||
proxyRoute,
|
||||
proxyURL,
|
||||
telemetryRoute,
|
||||
telemetryURL,
|
||||
} from "../../../common/routes";
|
||||
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 { Select, SelectOption } from "../select";
|
||||
import { HelmCharts } from "./helm-charts";
|
||||
import { KubectlBinaries } from "./kubectl-binaries";
|
||||
import { navigation } from "../../navigation";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
import { KubeconfigSyncs } from "./kubeconfig-syncs";
|
||||
import { navigateWithoutHistoryChange, navigation } from "../../navigation";
|
||||
import { SettingLayout } from "../layout/setting-layout";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { Application } from "./application";
|
||||
import { Kubernetes } from "./kubernetes";
|
||||
import { LensProxy } from "./proxy";
|
||||
import { Telemetry } from "./telemetry";
|
||||
import { Extensions } from "./extensions";
|
||||
import { sentryDsn } from "../../../common/vars";
|
||||
|
||||
enum Pages {
|
||||
Application = "application",
|
||||
Proxy = "proxy",
|
||||
Kubernetes = "kubernetes",
|
||||
Telemetry = "telemetry",
|
||||
Extensions = "extensions",
|
||||
Other = "other"
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Preferences extends React.Component {
|
||||
@observable httpProxy = UserStore.getInstance().httpsProxy || "";
|
||||
@observable shell = UserStore.getInstance().shell || "";
|
||||
@observable activeTab = Pages.Application;
|
||||
@observable historyLength: number | undefined;
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
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() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => navigation.location.hash, hash => {
|
||||
const fragment = hash.slice(1); // hash is /^(#\w.)?$/
|
||||
|
||||
if (fragment) {
|
||||
// ignore empty fragments
|
||||
document.getElementById(fragment)?.scrollIntoView();
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
onTabChange = (tabId: Pages) => {
|
||||
this.activeTab = tabId;
|
||||
};
|
||||
|
||||
renderNavigation() {
|
||||
const extensions = AppPreferenceRegistry.getInstance().getItems().filter(e => !e.showInPreferencesTab);
|
||||
const extensions = AppPreferenceRegistry.getInstance().getItems();
|
||||
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry");
|
||||
const currentLocation = navigation.location.pathname;
|
||||
const isActive = (route: RouteProps) => !!matchPath(currentLocation, { path: route.path, exact: route.exact });
|
||||
|
||||
return (
|
||||
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
|
||||
<Tabs className="flex column" scrollable={false} onChange={(url) => navigateWithoutHistoryChange({ pathname: url })}>
|
||||
<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"/>
|
||||
<Tab value={Pages.Kubernetes} label="Kubernetes" data-testid="kube-tab"/>
|
||||
<Tab value={Pages.Telemetry} label="Telemetry" data-testid="telemetry-tab"/>
|
||||
{extensions.length > 0 &&
|
||||
<Tab value={Pages.Extensions} label="Extensions" data-testid="extensions-tab"/>
|
||||
<Tab value={appURL()} label="Application" data-testid="application-tab" active={isActive(appRoute)}/>
|
||||
<Tab value={proxyURL()} label="Proxy" data-testid="proxy-tab" active={isActive(proxyRoute)}/>
|
||||
<Tab value={kubernetesURL()} label="Kubernetes" data-testid="kubernetes-tab" active={isActive(kubernetesRoute)}/>
|
||||
{telemetryExtensions.length > 0 || !!sentryDsn &&
|
||||
<Tab value={telemetryURL()} label="Telemetry" data-testid="telemetry-tab" active={isActive(telemetryRoute)}/>
|
||||
}
|
||||
{extensions.filter(e => !e.showInPreferencesTab).length > 0 &&
|
||||
<Tab value={extensionURL()} label="Extensions" data-testid="extensions-tab" active={isActive(extensionRoute)}/>
|
||||
}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
renderExtension({ title, id, components: { Hint, Input } }: RegisteredAppPreference) {
|
||||
return (
|
||||
<React.Fragment key={id}>
|
||||
<section id={id} className="small">
|
||||
<SubTitle title={title}/>
|
||||
<Input/>
|
||||
<div className="hint">
|
||||
<Hint/>
|
||||
</div>
|
||||
</section>
|
||||
<hr className="small"/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<SettingLayout
|
||||
navigation={this.renderNavigation()}
|
||||
className="Preferences"
|
||||
contentGaps={false}
|
||||
>
|
||||
{this.activeTab == Pages.Application && (
|
||||
<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>
|
||||
)}
|
||||
<Switch>
|
||||
<Route path={appURL()} component={Application}/>
|
||||
<Route path={proxyURL()} component={LensProxy}/>
|
||||
<Route path={kubernetesURL()} component={Kubernetes}/>
|
||||
<Route path={telemetryURL()} component={Telemetry}/>
|
||||
<Route path={extensionURL()} component={Extensions}/>
|
||||
<Redirect exact from={`${preferencesURL()}/`} to={appURL()}/>
|
||||
</Switch>
|
||||
</SettingLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function ExtensionSettings({ title, id, components: { Hint, Input } }: RegisteredAppPreference) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<section id={id} className="small">
|
||||
<SubTitle title={title}/>
|
||||
<Input/>
|
||||
<div className="hint">
|
||||
<Hint/>
|
||||
</div>
|
||||
</section>
|
||||
<hr className="small"/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
71
src/renderer/components/+preferences/proxy.tsx
Normal file
71
src/renderer/components/+preferences/proxy.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 { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { Input } from "../input";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
|
||||
export const LensProxy = observer(() => {
|
||||
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
|
||||
|
||||
return (
|
||||
<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={proxy}
|
||||
onChange={v => setProxy(v)}
|
||||
onBlur={() => UserStore.getInstance().httpsProxy = proxy}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
});
|
||||
63
src/renderer/components/+preferences/telemetry.tsx
Normal file
63
src/renderer/components/+preferences/telemetry.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { sentryDsn } from "../../../common/vars";
|
||||
import { AppPreferenceRegistry } from "../../../extensions/registries";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { ExtensionSettings } from "./preferences";
|
||||
|
||||
export const Telemetry = observer(() => {
|
||||
const extensions = AppPreferenceRegistry.getInstance().getItems();
|
||||
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry");
|
||||
|
||||
return (
|
||||
<section id="telemetry">
|
||||
<h2 data-testid="telemetry-header">Telemetry</h2>
|
||||
{telemetryExtensions.map((extension) => <ExtensionSettings key={extension.id} {...extension}/>)}
|
||||
{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>
|
||||
);
|
||||
});
|
||||
@ -36,6 +36,14 @@ export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
||||
back?: (evt: React.MouseEvent | KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
function scrollToAnchor() {
|
||||
const { hash } = window.location;
|
||||
|
||||
if (hash) {
|
||||
document.querySelector(`${hash}`).scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
const defaultProps: Partial<SettingLayoutProps> = {
|
||||
provideBackButtonNavigation: true,
|
||||
contentGaps: true,
|
||||
@ -59,6 +67,8 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
||||
|
||||
async componentDidMount() {
|
||||
window.addEventListener("keydown", this.onEscapeKey);
|
||||
|
||||
scrollToAnchor();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -79,7 +89,7 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
||||
render() {
|
||||
const {
|
||||
contentClass, provideBackButtonNavigation,
|
||||
contentGaps, navigation, children, ...elemProps
|
||||
contentGaps, navigation, children, back, ...elemProps
|
||||
} = this.props;
|
||||
const className = cssNames("SettingLayout", { showNavigation: navigation }, this.props.className);
|
||||
|
||||
|
||||
80
src/renderer/components/path-picker/path-picker.tsx
Normal file
80
src/renderer/components/path-picker/path-picker.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 PathPickOpts {
|
||||
label: string;
|
||||
onPick?: (paths: string[]) => any;
|
||||
onCancel?: () => any;
|
||||
defaultPath?: string;
|
||||
buttonLabel?: string;
|
||||
filters?: FileFilter[];
|
||||
properties?: OpenDialogOptions["properties"];
|
||||
securityScopedBookmarks?: boolean;
|
||||
}
|
||||
|
||||
export interface PathPickerProps extends PathPickOpts {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PathPicker extends React.Component<PathPickerProps> {
|
||||
static async pick(opts: PathPickOpts) {
|
||||
const { onPick, onCancel, label, ...dialogOptions } = opts;
|
||||
const { dialog, BrowserWindow } = remote;
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
||||
message: label,
|
||||
...dialogOptions,
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
await onCancel?.();
|
||||
} else {
|
||||
await onPick?.(filePaths);
|
||||
}
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
const { className, disabled, ...pickOpts } = this.props;
|
||||
|
||||
return PathPicker.pick(pickOpts);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, label, disabled } = this.props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
primary
|
||||
label={label}
|
||||
disabled={disabled}
|
||||
className={cssNames("PathPicker", className)}
|
||||
onClick={() => void this.onClick()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
106
src/renderer/initializers/catalog-category-registry.tsx
Normal file
106
src/renderer/initializers/catalog-category-registry.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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 from "react";
|
||||
import { kubernetesClusterCategory } from "../../common/catalog-entities";
|
||||
import { addClusterURL, kubernetesURL } from "../../common/routes";
|
||||
import { multiSet } from "../utils";
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import { getAllEntries } from "../components/+preferences/kubeconfig-syncs";
|
||||
import { runInAction } from "mobx";
|
||||
import { isWindows } from "../../common/vars";
|
||||
import { PathPicker } from "../components/path-picker/path-picker";
|
||||
import { Notifications } from "../components/notifications";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
async function addSyncEntries(filePaths: string[]) {
|
||||
const entries = await getAllEntries(filePaths);
|
||||
|
||||
runInAction(() => {
|
||||
multiSet(UserStore.getInstance().syncKubeconfigEntries, entries);
|
||||
});
|
||||
|
||||
Notifications.ok(
|
||||
<div>
|
||||
<p>Selected items has been added to Kubeconfig Sync.</p><br/>
|
||||
<p>Check the <Link style={{ textDecoration: "underline" }} to={`${kubernetesURL()}#kube-sync`}>Preferences</Link>{" "}
|
||||
to see full list.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function initCatalogCategoryRegistryEntries() {
|
||||
kubernetesClusterCategory.on("catalogAddMenu", ctx => {
|
||||
ctx.menuItems.push(
|
||||
{
|
||||
icon: "text_snippet",
|
||||
title: "Add from kubeconfig",
|
||||
onClick: () => ctx.navigate(addClusterURL()),
|
||||
},
|
||||
);
|
||||
|
||||
if (isWindows) {
|
||||
ctx.menuItems.push(
|
||||
{
|
||||
icon: "create_new_folder",
|
||||
title: "Sync kubeconfig folders(s)",
|
||||
defaultAction: true,
|
||||
onClick: async () => {
|
||||
await PathPicker.pick({
|
||||
label: "Sync folders(s)",
|
||||
buttonLabel: "Sync",
|
||||
properties: ["showHiddenFiles", "multiSelections", "openDirectory"],
|
||||
onPick: addSyncEntries,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "note_add",
|
||||
title: "Sync kubeconfig file(s)",
|
||||
onClick: async () => {
|
||||
await PathPicker.pick({
|
||||
label: "Sync file(s)",
|
||||
buttonLabel: "Sync",
|
||||
properties: ["showHiddenFiles", "multiSelections", "openFile"],
|
||||
onPick: addSyncEntries,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ctx.menuItems.push(
|
||||
{
|
||||
icon: "create_new_folder",
|
||||
title: "Sync kubeconfig(s)",
|
||||
defaultAction: true,
|
||||
onClick: async () => {
|
||||
await PathPicker.pick({
|
||||
label: "Sync file(s)",
|
||||
buttonLabel: "Sync",
|
||||
properties: ["showHiddenFiles", "multiSelections", "openFile", "openDirectory"],
|
||||
onPick: addSyncEntries,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -29,3 +29,4 @@ export * from "./kube-object-menu-registry";
|
||||
export * from "./registries";
|
||||
export * from "./welcome-menu-registry";
|
||||
export * from "./workloads-overview-detail-registry";
|
||||
export * from "./catalog-category-registry";
|
||||
|
||||
@ -38,6 +38,10 @@ export function navigate(location: LocationDescriptor) {
|
||||
}
|
||||
}
|
||||
|
||||
export function navigateWithoutHistoryChange(location: Partial<Location>) {
|
||||
navigation.merge(location, true);
|
||||
}
|
||||
|
||||
export function createPageParam<V = string>(init: PageParamInit<V>) {
|
||||
return new PageParam<V>(init, navigation);
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import { UserStore } from "../common/user-store";
|
||||
import logger from "../main/logger";
|
||||
import darkTheme from "./themes/lens-dark.json";
|
||||
import lightTheme from "./themes/lens-light.json";
|
||||
import type { SelectOption } from "./components/select";
|
||||
|
||||
export type ThemeId = string;
|
||||
|
||||
@ -67,6 +68,13 @@ export class ThemeStore extends Singleton {
|
||||
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() {
|
||||
super();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user