From 207f0dfd6211b58b4b31decdc59e8330748874bd Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 30 Jun 2021 23:12:06 -0400 Subject: [PATCH] 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 --- integration/__tests__/app.tests.ts | 17 +- .../catalog-entities/kubernetes-cluster.ts | 21 +- src/common/user-store/user-store.ts | 2 +- src/extensions/extension-loader.ts | 1 + src/extensions/lens-renderer-extension.ts | 1 + .../registries/app-preference-registry.ts | 65 +++- src/main/kubectl.ts | 7 + src/renderer/bootstrap.tsx | 2 + .../+preferences/kubeconfig-syncs.tsx | 52 +-- .../+preferences/kubectl-binaries.tsx | 111 ------- .../components/+preferences/preferences.tsx | 264 +++------------ src/renderer/components/path-picker/index.ts | 22 ++ .../components/path-picker/path-picker.tsx | 71 +++++ .../app-preferences-kind-registry.ts | 52 +++ .../initializers/app-preferences-registry.tsx | 301 ++++++++++++++++++ src/renderer/initializers/index.ts | 2 + src/renderer/initializers/registries.ts | 1 + src/renderer/theme.store.ts | 8 + 18 files changed, 621 insertions(+), 379 deletions(-) delete mode 100644 src/renderer/components/+preferences/kubectl-binaries.tsx create mode 100644 src/renderer/components/path-picker/index.ts create mode 100644 src/renderer/components/path-picker/path-picker.tsx create mode 100644 src/renderer/initializers/app-preferences-kind-registry.ts create mode 100644 src/renderer/initializers/app-preferences-registry.tsx diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 7b8ff11435..d8f54ba9da 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -60,14 +60,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 () => { @@ -77,7 +76,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) diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 83a94547bb..2db6893550 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -25,7 +25,7 @@ import { clusterActivateHandler, clusterDeleteHandler, clusterDisconnectHandler import { ClusterStore } from "../cluster-store"; import { requestMain } from "../ipc"; import { CatalogCategory, CatalogCategorySpec } from "../catalog"; -import { addClusterURL } from "../routes"; +import { addClusterURL, preferencesURL } from "../routes"; import { app } from "electron"; import type { CatalogEntitySpec } from "../catalog/catalog-entity"; import { HotbarStore } from "../hotbar-store"; @@ -172,13 +172,18 @@ export class KubernetesClusterCategory extends CatalogCategory { super(); this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => { - ctx.menuItems.push({ - icon: "text_snippet", - title: "Add from kubeconfig", - onClick: () => { - ctx.navigate(addClusterURL()); - } - }); + ctx.menuItems.push( + { + icon: "text_snippet", + title: "Add from kubeconfig", + onClick: () => ctx.navigate(addClusterURL()), + }, + { + icon: "settings", + title: "Sync kubeconfig file(s)", + onClick: () => ctx.navigate(preferencesURL({ fragment: "kube-sync" })), + }, + ); }); } } diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index beeb2078e1..2fccd59cbf 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -209,6 +209,6 @@ export class UserStore extends BaseStore /* 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"); } diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index af69a6bef8..217fc4060e 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -273,6 +273,7 @@ export class ExtensionLoader extends Singleton { this.autoInitExtensions(async (extension: LensRendererExtension) => { const removeItems = [ registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension), + registries.AppPreferenceKindRegistry.getInstance().add(extension.appPreferenceKinds), registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences), registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index dd5670fd67..3799978b90 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -31,6 +31,7 @@ export class LensRendererExtension extends LensExtension { clusterPageMenus: registries.ClusterPageMenuRegistration[] = []; kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = []; appPreferences: registries.AppPreferenceRegistration[] = []; + appPreferenceKinds: registries.AppPreferenceKindRegistration[] = []; entitySettings: registries.EntitySettingRegistration[] = []; statusBarItems: registries.StatusBarRegistration[] = []; kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = []; diff --git a/src/extensions/registries/app-preference-registry.ts b/src/extensions/registries/app-preference-registry.ts index fb20034d52..9651521618 100644 --- a/src/extensions/registries/app-preference-registry.ts +++ b/src/extensions/registries/app-preference-registry.ts @@ -19,30 +19,87 @@ * 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 { BaseRegistry } from "./base-registry"; export interface AppPreferenceComponents { - Hint: React.ComponentType; + /** + * This will be rendered below the `` with slightly smaller font size + * + * @optional + */ + Hint?: React.ComponentType; + + /** + * The component for rendering the interactive part of the setting + */ Input: React.ComponentType; } export interface AppPreferenceRegistration { + /** + * The text that will be displayed as the title to the preference + */ 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; + + /** + * Which preferences tab to display this setting. + * + * @default "extensions" + */ 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; + + /** + * The components used for rendering the settings + */ components: AppPreferenceComponents; } -export interface RegisteredAppPreference extends AppPreferenceRegistration { +export type RegisteredAppPreference = Required; + +export interface AppPreferenceKindRegistration { 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 { - getRegisteredItem(item: AppPreferenceRegistration): RegisteredAppPreference { + getRegisteredItem({ id, showInPreferencesTab, hide = false, ...item}: AppPreferenceRegistration): RegisteredAppPreference { 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, }; } } + +export class AppPreferenceKindRegistry extends BaseRegistry {} diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 01b38c67fe..6285c77dba 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -32,6 +32,7 @@ import { customRequest } from "../common/request"; import { getBundledKubectlVersion } from "../common/utils/app-version"; import { isDevelopment, isWindows, isTestEnv } from "../common/vars"; import { SemVer } from "semver"; +import type { SelectOption } from "../renderer/components/select"; const bundledVersion = getBundledKubectlVersion(); const kubectlMap: Map = new Map([ @@ -55,6 +56,12 @@ const packageMirrors: Map = new Map([ ["default", "https://storage.googleapis.com/kubernetes-release/release"], ["china", "https://mirror.azure.cn/kubernetes/kubectl"] ]); + +export const downloadMirrorOptions: SelectOption[] = [ + { value: "default", label: "Default (Google)" }, + { value: "china", label: "China (Azure)" }, +]; + let bundledPath: string; const initScriptVersionString = "# lens-initscript v3\n"; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 87c30dba59..1becdb37e8 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -74,6 +74,8 @@ export async function bootstrap(App: AppComponent) { rootElem.classList.toggle("is-mac", isMac); initializers.initRegistries(); + initializers.initAppPreferenceKindRegistry(); + initializers.initAppPreferenceRegistry(); initializers.initCommandRegistry(); initializers.initEntitySettingsRegistry(); initializers.initKubeObjectMenuRegistry(); diff --git a/src/renderer/components/+preferences/kubeconfig-syncs.tsx b/src/renderer/components/+preferences/kubeconfig-syncs.tsx index c1e049cbde..6471a2fdd5 100644 --- a/src/renderer/components/+preferences/kubeconfig-syncs.tsx +++ b/src/renderer/components/+preferences/kubeconfig-syncs.tsx @@ -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 { isWindows } from "../../../common/vars"; +import { PathPicker } from "../path-picker"; interface SyncInfo { type: "file" | "folder" | "unknown"; @@ -70,8 +68,6 @@ async function getMapEntry({ filePath, ...data}: KubeconfigSyncEntry): Promise<[ } } -type SelectPathOptions = ("openFile" | "openDirectory")[]; - @observer export class KubeconfigSyncs extends React.Component { syncs = observable.map(); @@ -109,24 +105,13 @@ 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; - } - + onPick = async (filePaths: string[]) => { const newEntries = await Promise.all(filePaths.map(filePath => getMapEntry({ filePath }))); for (const [filePath, info] of newEntries) { this.syncs.set(filePath, info); } - } + }; renderEntryIcon(entry: Entry) { switch (entry.info.type) { @@ -188,27 +173,30 @@ export class KubeconfigSyncs extends React.Component { if (isWindows) { return (
-
); } return ( -