diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 2db6893550..14a584ee8c 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -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, preferencesURL } from "../routes"; import { app } from "electron"; import type { CatalogEntitySpec } from "../catalog/catalog-entity"; import { HotbarStore } from "../hotbar-store"; @@ -148,7 +147,7 @@ export class KubernetesCluster extends CatalogEntity { - 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" })), - }, - ); - }); - } } -catalogCategoryRegistry.add(new KubernetesClusterCategory()); +export const kubernetesClusterCategory = new KubernetesClusterCategory(); + +catalogCategoryRegistry.add(kubernetesClusterCategory); diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts index bc3e360a7b..e74fd238e3 100644 --- a/src/common/catalog/catalog-entity.ts +++ b/src/common/catalog/catalog-entity.ts @@ -22,6 +22,7 @@ import EventEmitter from "events"; import type TypedEmitter from "typed-emitter"; import { observable, makeObservable } from "mobx"; +import type React from "react"; type ExtractEntityMetadataType = Entity extends CatalogEntity ? Metadata : never; type ExtractEntityStatusType = Entity extends CatalogEntity ? Status : never; @@ -108,7 +109,7 @@ export interface CatalogEntityContextMenu { /** * Menu icon */ - icon?: string; + icon?: string | React.ComponentType; /** * OnClick handler */ @@ -122,7 +123,7 @@ export interface CatalogEntityContextMenu { } export interface CatalogEntityAddMenu extends CatalogEntityContextMenu { - icon: string; + icon: string | React.ComponentType; } export interface CatalogEntitySettingsMenu { diff --git a/src/common/utils/extended-map.ts b/src/common/utils/extended-map.ts index bd29cbbfe0..46621194e2 100644 --- a/src/common/utils/extended-map.ts +++ b/src/common/utils/extended-map.ts @@ -21,6 +21,12 @@ import { action, ObservableMap } from "mobx"; +export function multiSet(map: Map, newEntries: [T, V][]): void { + for (const [key, val] of newEntries) { + map.set(key, val); + } +} + export class ExtendedMap extends Map { static new(entries?: readonly (readonly [K, V])[] | null): ExtendedMap { return new ExtendedMap(entries); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 1becdb37e8..9ad274020b 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -74,6 +74,7 @@ export async function bootstrap(App: AppComponent) { rootElem.classList.toggle("is-mac", isMac); initializers.initRegistries(); + initializers.initCatalogCategoryRegistryEntries(); initializers.initAppPreferenceKindRegistry(); initializers.initAppPreferenceRegistry(); initializers.initCommandRegistry(); diff --git a/src/renderer/components/+catalog/catalog-add-button.tsx b/src/renderer/components/+catalog/catalog-add-button.tsx index 25cd43239f..636b03b2f7 100644 --- a/src/renderer/components/+catalog/catalog-add-button.tsx +++ b/src/renderer/components/+catalog/catalog-add-button.tsx @@ -97,7 +97,7 @@ export class CatalogAddButton extends React.Component { { this.menuItems.map((menuItem, index) => { return } + icon={typeof menuItem.icon === "string" ? : } tooltipTitle={menuItem.title} onClick={() => menuItem.onClick()} TooltipClasses={{ diff --git a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx index af3104afe2..b0cca3a97f 100644 --- a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx +++ b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx @@ -31,11 +31,24 @@ import { ConfirmDialog } from "../confirm-dialog"; import { HotbarStore } from "../../../common/hotbar-store"; import { Icon } from "../icon"; import type { CatalogEntityItem } from "./catalog-entity.store"; +import { Tooltip } from "@material-ui/core"; export interface CatalogEntityDrawerMenuProps extends MenuActionsProps { item: CatalogEntityItem | null | undefined; } +function resolveIcon(item: CatalogEntityContextMenu) { + if (typeof item.icon === "string") { + if (item.icon.includes("; + } + + return ; + } + + return ; +} + @observer export class CatalogEntityDrawerMenu extends React.Component> { @observable private contextMenu: CatalogEntityContextMenuContext; @@ -86,14 +99,9 @@ export class CatalogEntityDrawerMenu extends React.Comp continue; } - const key = menuItem.icon.includes(" this.onMenuItemClick(menuItem)}> - + {resolveIcon(menuItem)} ); } @@ -109,7 +117,7 @@ export class CatalogEntityDrawerMenu extends React.Comp render() { const { className, item: entity, ...menuProps } = this.props; - + if (!this.contextMenu || !entity.enabled) { return null; } diff --git a/src/renderer/components/+preferences/kubeconfig-syncs.tsx b/src/renderer/components/+preferences/kubeconfig-syncs.tsx index 6471a2fdd5..89150b518e 100644 --- a/src/renderer/components/+preferences/kubeconfig-syncs.tsx +++ b/src/renderer/components/+preferences/kubeconfig-syncs.tsx @@ -28,7 +28,7 @@ import fse from "fs-extra"; import { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store"; 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"; @@ -68,6 +68,10 @@ async function getMapEntry({ filePath, ...data}: KubeconfigSyncEntry): Promise<[ } } +export async function getAllEntries(filePaths: string[]): Promise<[string, Value][]> { + return Promise.all(filePaths.map(filePath => getMapEntry({ filePath }))); +} + @observer export class KubeconfigSyncs extends React.Component { syncs = observable.map(); @@ -105,13 +109,7 @@ export class KubeconfigSyncs extends React.Component { } @action - 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); - } - }; + onPick = async (filePaths: string[]) => multiSet(this.syncs, await getAllEntries(filePaths)); renderEntryIcon(entry: Entry) { switch (entry.info.type) { diff --git a/src/renderer/components/path-picker/path-picker.tsx b/src/renderer/components/path-picker/path-picker.tsx index 7bd73db8f3..782b0e67d5 100644 --- a/src/renderer/components/path-picker/path-picker.tsx +++ b/src/renderer/components/path-picker/path-picker.tsx @@ -25,12 +25,10 @@ import React from "react"; import { cssNames } from "../../utils"; import { Button } from "../button"; -export interface PathPickerProps { - className?: string; +export interface PathPickOpts { label: string; - disabled?: boolean; - onPick?: (paths: string[]) => void; - onCancel?: () => void; + onPick?: (paths: string[]) => any; + onCancel?: () => any; defaultPath?: string; buttonLabel?: string; filters?: FileFilter[]; @@ -38,10 +36,15 @@ export interface PathPickerProps { securityScopedBookmarks?: boolean; } +export interface PathPickerProps extends PathPickOpts { + className?: string; + disabled?: boolean; +} + @observer export class PathPicker extends React.Component { - async onClick() { - const { onPick, onCancel, label, className, disabled, ...dialogOptions } = this.props; + 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, @@ -49,12 +52,18 @@ export class PathPicker extends React.Component { }); if (canceled) { - onCancel?.(); + await onCancel?.(); } else { - onPick?.(filePaths); + await onPick?.(filePaths); } } + async onClick() { + const { className, disabled, ...pickOpts } = this.props; + + return PathPicker.pick(pickOpts); + } + render() { const { className, label, disabled } = this.props; diff --git a/src/renderer/initializers/catalog-category-registry.tsx b/src/renderer/initializers/catalog-category-registry.tsx new file mode 100644 index 0000000000..f222845677 --- /dev/null +++ b/src/renderer/initializers/catalog-category-registry.tsx @@ -0,0 +1,98 @@ +/** + * 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 { CreateNewFolderOutlined, NoteAdd } from "@material-ui/icons"; +import { kubernetesClusterCategory } from "../../common/catalog-entities"; +import { addClusterURL, preferencesURL } from "../../common/routes"; +import { PathPicker } from "../components/path-picker"; +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"; + +async function addSyncEntries(filePaths: string[]) { + const entries = await getAllEntries(filePaths); + + runInAction(() => { + multiSet(UserStore.getInstance().syncKubeconfigEntries, entries); + }); +} + +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: () => , + title: "Sync kubeconfig folders(s)", + onClick: async () => { + await PathPicker.pick({ + label: "Sync folders(s)", + buttonLabel: "Sync", + properties: ["showHiddenFiles", "multiSelections", "openDirectory"], + onPick: addSyncEntries, + }); + ctx.navigate(preferencesURL({ fragment: "kube-sync" })); + }, + }, + { + icon: () => , + title: "Sync kubeconfig file(s)", + onClick: async () => { + await PathPicker.pick({ + label: "Sync file(s)", + buttonLabel: "Sync", + properties: ["showHiddenFiles", "multiSelections", "openFile"], + onPick: addSyncEntries, + }); + ctx.navigate(preferencesURL({ fragment: "kube-sync" })); + }, + }, + ); + } else { + ctx.menuItems.push( + { + icon: "settings", + title: "Sync kubeconfig(s)", + onClick: async () => { + await PathPicker.pick({ + label: "Sync file(s)", + buttonLabel: "Sync", + properties: ["showHiddenFiles", "multiSelections", "openFile"], + onPick: addSyncEntries, + }); + ctx.navigate(preferencesURL({ fragment: "kube-sync" })); + }, + }, + ); + } + }); +} diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts index 02e7a12f83..97431ca3ca 100644 --- a/src/renderer/initializers/index.ts +++ b/src/renderer/initializers/index.ts @@ -21,6 +21,7 @@ export * from "./app-preferences-kind-registry"; export * from "./app-preferences-registry"; +export * from "./catalog-category-registry"; export * from "./catalog-entity-detail-registry"; export * from "./catalog"; export * from "./command-registry";