diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts index 85feaa21ce..7effe60f2f 100644 --- a/src/common/catalog/catalog-entity.ts +++ b/src/common/catalog/catalog-entity.ts @@ -293,11 +293,22 @@ export interface CatalogEntitySettingsMenu { }; } +export interface CatalogEntityContextMenuNavigate { + /** + * @param pathname The location to navigate to in the main iframe + */ + (pathname: string, forceMainFrame?: boolean): void; + /** + * @param pathname The location to navigate to in the current iframe. Useful for when called within the cluster frame + */ + (pathname: string, forceMainFrame: false): void; +} + export interface CatalogEntityContextMenuContext { /** * Navigate to the specified pathname */ - navigate: (pathname: string) => void; + navigate: CatalogEntityContextMenuNavigate; menuItems: CatalogEntityContextMenu[]; } diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 3f9d750b4d..e36ccd317f 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -30,6 +30,7 @@ export * from "./n-fircate"; export * from "./openBrowser"; export * from "./paths"; export * from "./promise-exec"; +export * from "./readonly"; export * from "./reject-promise"; export * from "./singleton"; export * from "./sort-compare"; diff --git a/src/common/utils/type-narrowing.ts b/src/common/utils/type-narrowing.ts index bd5a404e14..a138b2a0ec 100644 --- a/src/common/utils/type-narrowing.ts +++ b/src/common/utils/type-narrowing.ts @@ -133,6 +133,10 @@ export function bindPredicate(fn: (arg1: unknown, ...ar return (arg1: unknown): arg1 is T => fn(arg1, ...boundArgs); } +export function hasDefiniteField(field: Field): (val: T) => val is T & { [f in Field]-?: NonNullable } { + return (val): val is T & { [f in Field]-?: NonNullable } => val[field] != null; +} + export function isErrnoException(error: unknown): error is NodeJS.ErrnoException { return isObject(error) && hasOptionalTypedProperty(error, "code", isString) diff --git a/src/extensions/common-api/registrations.ts b/src/extensions/common-api/registrations.ts index 01b4407a6b..a35e23ef11 100644 --- a/src/extensions/common-api/registrations.ts +++ b/src/extensions/common-api/registrations.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ export type { StatusBarRegistration } from "../../renderer/components/status-bar/status-bar-registration"; -export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration"; +export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../../renderer/components/kube-object-menu/kube-object-menu-registration"; export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../renderer/components/+preferences/app-preferences/app-preference-registration"; export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry"; export type { KubeObjectStatusRegistration } from "../../renderer/components/kube-object-status-icon/kube-object-status-registration"; @@ -12,3 +12,4 @@ export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../ export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler"; export type { CustomCategoryViewProps, CustomCategoryViewComponents, CustomCategoryViewRegistration } from "../../renderer/components/+catalog/custom-views"; export type { ShellEnvModifier, ShellEnvContext } from "../../main/shell-session/shell-env-modifier/shell-env-modifier-registration"; +export type { KubeObjectContextMenuItem, KubeObjectOnContextMenuOpenContext, KubeObjectOnContextMenuOpen, KubeObjectHandlers, KubeObjectHandlerRegistration } from "../../renderer/kube-object/handler"; diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index de6ab67187..1ed9164cb0 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -18,13 +18,14 @@ import type { AppPreferenceRegistration } from "../renderer/components/+preferen import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns"; import type { CustomCategoryViewRegistration } from "../renderer/components/+catalog/custom-views"; import type { StatusBarRegistration } from "../renderer/components/status-bar/status-bar-registration"; -import type { KubeObjectMenuRegistration } from "../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration"; +import type { KubeObjectMenuRegistration } from "../renderer/components/kube-object-menu/kube-object-menu-registration"; import type { WorkloadsOverviewDetailRegistration } from "../renderer/components/+workloads-overview/workloads-overview-detail-registration"; import type { KubeObjectStatusRegistration } from "../renderer/components/kube-object-status-icon/kube-object-status-registration"; import { fromPairs, map, matches, toPairs } from "lodash/fp"; import { pipeline } from "@ogre-tools/fp"; import { getExtensionRoutePath } from "../renderer/routes/for-extension"; import type { LensRendererExtensionDependencies } from "./lens-extension-set-dependencies"; +import type { KubeObjectHandlerRegistration } from "../renderer/kube-object/handler"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; @@ -44,6 +45,7 @@ export class LensRendererExtension extends LensExtension void; +} + +export type NormalizeCatalogEntityContextMenu = (menuItem: CatalogEntityContextMenu) => NormalizedCatalogEntityContextMenu; + +const normalizeCatalogEntityContextMenuInjectable = getInjectable({ + id: "normalize-catalog-entity-context-menu", + instantiate: (di): NormalizeCatalogEntityContextMenu => { + const withConfirmation = di.inject(withConfirmationInjectable); + + return (menuItem) => { + if (menuItem.confirm) { + return { + title: menuItem.title, + icon: menuItem.icon, + onClick: withConfirmation({ + message: menuItem.confirm.message, + ok: menuItem.onClick, + okButtonProps: { + primary: false, + accent: true, + }, + }), + }; + } + + return menuItem; + }; + }, +}); + +export default normalizeCatalogEntityContextMenuInjectable; diff --git a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx index 6b2ca4c89f..198b467a04 100644 --- a/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx +++ b/src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx @@ -4,81 +4,61 @@ */ import React from "react"; -import { cssNames } from "../../utils"; +import { cssNames, hasDefiniteField } from "../../utils"; import type { MenuActionsProps } from "../menu/menu-actions"; import { MenuActions } from "../menu/menu-actions"; import type { CatalogEntity, CatalogEntityContextMenu } from "../../api/catalog-entity"; import { observer } from "mobx-react"; import { observable } from "mobx"; -import { navigate } from "../../navigation"; import { MenuItem } from "../menu"; -import { ConfirmDialog } from "../confirm-dialog"; import { Icon } from "../icon"; import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item"; import type { OnContextMenuOpen } from "../../../common/catalog/on-context-menu-open.injectable"; -import { withInjectables } from "@ogre-tools/injectable-react"; import onContextMenuOpenInjectable from "../../../common/catalog/on-context-menu-open.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { Navigate } from "../../navigation/navigate.injectable"; +import navigateInjectable from "../../navigation/navigate.injectable"; +import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable"; +import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable"; export interface CatalogEntityDrawerMenuProps extends MenuActionsProps { entity: Entity; } interface Dependencies { + normalizeMenuItem: NormalizeCatalogEntityContextMenu; + navigate: Navigate; onContextMenuOpen: OnContextMenuOpen; } @observer -class NonInjectedCatalogEntityDrawerMenu extends React.Component> { +class NonInjectedCatalogEntityDrawerMenu extends React.Component & Dependencies> { private readonly menuItems = observable.array(); componentDidMount() { this.props.onContextMenuOpen(this.props.entity, { menuItems: this.menuItems, - navigate: (url) => navigate(url), + navigate: this.props.navigate, }); } - onMenuItemClick(menuItem: CatalogEntityContextMenu) { - if (menuItem.confirm) { - ConfirmDialog.open({ - okButtonProps: { - primary: false, - accent: true, - }, - ok: () => { - menuItem.onClick(); - }, - message: menuItem.confirm.message, - }); - } else { - menuItem.onClick(); - } - } - - getMenuItems(entity: Entity): React.ReactChild[] { + getMenuItems(entity: T): React.ReactChild[] { if (!entity) { return []; } - const items: React.ReactChild[] = []; - - for (const menuItem of this.menuItems) { - if (!menuItem.icon) { - continue; - } - - const key = Icon.isSvg(menuItem.icon) ? "svg" : "material"; - - items.push( - this.onMenuItemClick(menuItem)}> + const items = this.menuItems + .map(this.props.normalizeMenuItem) + .filter(hasDefiniteField("icon")) + .map(menuItem => ( + - , - ); - } + + )); items.push( ({ ...props, onContextMenuOpen: di.inject(onContextMenuOpenInjectable), + normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable), + navigate: di.inject(navigateInjectable), }), }) as (props: CatalogEntityDrawerMenuProps) => JSX.Element; diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 1441455d56..0cc5f8be4c 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -11,10 +11,8 @@ import { ItemListLayout } from "../item-object-list"; import type { IComputedValue } from "mobx"; import { action, computed, makeObservable, observable, reaction, runInAction, when } from "mobx"; import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store"; -import { navigate } from "../../navigation"; import { MenuItem, MenuActions } from "../menu"; import type { CatalogEntityContextMenu } from "../../api/catalog-entity"; -import { ConfirmDialog } from "../confirm-dialog"; import type { CatalogCategory, CatalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; import { CatalogAddButton } from "./catalog-add-button"; import { Notifications } from "../notifications"; @@ -46,6 +44,10 @@ import type { HotbarStore } from "../../../common/hotbars/store"; import type { OnContextMenuOpen } from "../../../common/catalog/on-context-menu-open.injectable"; import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable"; import onContextMenuOpenInjectable from "../../../common/catalog/on-context-menu-open.injectable"; +import type { Navigate } from "../../navigation/navigate.injectable"; +import navigateInjectable from "../../navigation/navigate.injectable"; +import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable"; +import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable"; interface Dependencies { catalogPreviousActiveTabStorage: StorageLayer; @@ -61,6 +63,8 @@ interface Dependencies { hotbarStore: HotbarStore; catalogCategoryRegistry: CatalogCategoryRegistry; onContextMenuOpen: OnContextMenuOpen; + navigate: Navigate; + normalizeMenuItem: NormalizeCatalogEntityContextMenu; } @observer @@ -160,23 +164,6 @@ class NonInjectedCatalog extends React.Component { } }; - onMenuItemClick(menuItem: CatalogEntityContextMenu) { - if (menuItem.confirm) { - ConfirmDialog.open({ - okButtonProps: { - primary: false, - accent: true, - }, - ok: () => { - menuItem.onClick(); - }, - message: menuItem.confirm.message, - }); - } else { - menuItem.onClick(); - } - } - get categories() { return this.props.catalogCategoryRegistry.items; } @@ -206,7 +193,7 @@ class NonInjectedCatalog extends React.Component { this.menuItems.clear(); this.props.onContextMenuOpen(entity, { menuItems: this.menuItems, - navigate: (url) => navigate(url), + navigate: this.props.navigate, }); }; @@ -219,11 +206,13 @@ class NonInjectedCatalog extends React.Component { View Details { - this.menuItems.map((menuItem, index) => ( - this.onMenuItemClick(menuItem)}> - {menuItem.title} - - )) + this.menuItems + .map(this.props.normalizeMenuItem) + .map((menuItem, index) => ( + + {menuItem.title} + + )) } { } } -export const Catalog = withInjectables( NonInjectedCatalog, { - getProps: (di) => ({ +export const Catalog = withInjectables(NonInjectedCatalog, { + getProps: (di, props) => ({ + ...props, catalogEntityStore: di.inject(catalogEntityStoreInjectable), catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable), getCategoryColumns: di.inject(getCategoryColumnsInjectable), @@ -370,5 +360,7 @@ export const Catalog = withInjectables( NonInjectedCatalog, { hotbarStore: di.inject(hotbarStoreInjectable), catalogCategoryRegistry: di.inject(catalogCategoryRegistryInjectable), onContextMenuOpen: di.inject(onContextMenuOpenInjectable), + navigate: di.inject(navigateInjectable), + normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable), }), }); diff --git a/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx b/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx new file mode 100644 index 0000000000..0af3149e1b --- /dev/null +++ b/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx @@ -0,0 +1,152 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ExtendableDisposer } from "../../../common/utils"; +import { downloadFile, downloadJson } from "../../../common/utils"; +import { Notifications } from "../notifications"; +import React from "react"; +import path from "path"; +import { SemVer } from "semver"; +import URLParse from "url-parse"; +import type { InstallRequest } from "./attempt-install/install-request"; +import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; +import type { Confirm } from "../confirm-dialog/confirm.injectable"; +import { getInjectable } from "@ogre-tools/injectable"; +import attemptInstallInjectable from "./attempt-install/attempt-install.injectable"; +import getBaseRegistryUrlInjectable from "./get-base-registry-url/get-base-registry-url.injectable"; +import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import confirmInjectable from "../confirm-dialog/confirm.injectable"; +import { reduce } from "lodash"; + +export interface ExtensionInfo { + name: string; + version?: string; + requireConfirmation?: boolean; +} + +export type AttemptInstallByInfo = (info: ExtensionInfo) => Promise; + +interface Dependencies { + attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise; + getBaseRegistryUrl: () => Promise; + extensionInstallationStateStore: ExtensionInstallationStateStore; + confirm: Confirm; +} + +const attemptInstallByInfo = ({ + attemptInstall, + getBaseRegistryUrl, + extensionInstallationStateStore, + confirm, +}: Dependencies): AttemptInstallByInfo => ( + async (info) => { + const { name, version, requireConfirmation = false } = info; + const disposer = extensionInstallationStateStore.startPreInstall(); + const baseUrl = await getBaseRegistryUrl(); + const registryUrl = new URLParse(baseUrl).set("pathname", name).toString(); + let json: any; + let finalVersion = version; + + try { + json = await downloadJson({ url: registryUrl }).promise; + + if (!json || json.error || typeof json.versions !== "object" || !json.versions) { + const message = json?.error ? `: ${json.error}` : ""; + + Notifications.error(`Failed to get registry information for that extension${message}`); + + return disposer(); + } + } catch (error) { + if (error instanceof SyntaxError) { + // assume invalid JSON + console.warn("Set registry has invalid json", { url: baseUrl }, error); + Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON"); + } else { + console.error("Failed to download registry information", error); + Notifications.error(`Failed to get valid registry information for that extension. ${error}`); + } + + return disposer(); + } + + if (version) { + if (!json.versions[version]) { + if (json["dist-tags"][version]) { + finalVersion = json["dist-tags"][version]; + } else { + Notifications.error(( +

+ {"The "} + {name} + {" extension does not have a version or tag "} + {version} + . +

+ )); + + return disposer(); + } + } + } else { + const versions = Object.keys(json.versions) + .map(version => new SemVer(version, { loose: true, includePrerelease: true })) + // ignore pre-releases for auto picking the version + .filter(version => version.prerelease.length === 0); + + const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev); + + if (!latestVersion) { + console.error("No versions supplied for that extension", { name }); + Notifications.error(`No versions found for ${name}`); + + return disposer(); + } + + finalVersion = latestVersion.format(); + } + + if (requireConfirmation) { + const proceed = await confirm({ + message: ( +

+ Are you sure you want to install + {" "} + + {name} + @ + {finalVersion} + + ? +

+ ), + labelCancel: "Cancel", + labelOk: "Install", + }); + + if (!proceed) { + return disposer(); + } + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const url = json.versions[finalVersion!].dist.tarball; + const fileName = path.basename(url); + const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 }); + + return attemptInstall({ fileName, dataP }, disposer); + } +); + +const attemptInstallByInfoInjectable = getInjectable({ + id: "attempt-install-by-info", + instantiate: (di) => attemptInstallByInfo({ + attemptInstall: di.inject(attemptInstallInjectable), + getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable), + extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), + confirm: di.inject(confirmInjectable), + }), +}); + +export default attemptInstallByInfoInjectable; diff --git a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts deleted file mode 100644 index 3703c8e56e..0000000000 --- a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { attemptInstallByInfo } from "./attempt-install-by-info"; -import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; -import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable"; -import extensionInstallationStateStoreInjectable - from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; - -const attemptInstallByInfoInjectable = getInjectable({ - id: "attempt-install-by-info", - - instantiate: (di) => - attemptInstallByInfo({ - attemptInstall: di.inject(attemptInstallInjectable), - getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), - }), -}); - -export default attemptInstallByInfoInjectable; diff --git a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx b/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx deleted file mode 100644 index df6279cf43..0000000000 --- a/src/renderer/components/+extensions/attempt-install-by-info/attempt-install-by-info.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { ExtendableDisposer } from "../../../../common/utils"; -import { downloadFile, downloadJson } from "../../../../common/utils"; -import { Notifications } from "../../notifications"; -import { ConfirmDialog } from "../../confirm-dialog"; -import React from "react"; -import path from "path"; -import { SemVer } from "semver"; -import URLParse from "url-parse"; -import type { InstallRequest } from "../attempt-install/install-request"; -import lodash from "lodash"; -import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; - -export interface ExtensionInfo { - name: string; - version?: string; - requireConfirmation?: boolean; -} - -interface Dependencies { - attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise; - getBaseRegistryUrl: () => Promise; - extensionInstallationStateStore: ExtensionInstallationStateStore; -} - -export const attemptInstallByInfo = ({ attemptInstall, getBaseRegistryUrl, extensionInstallationStateStore }: Dependencies) => async ({ - name, - version, - requireConfirmation = false, -}: ExtensionInfo) => { - const disposer = extensionInstallationStateStore.startPreInstall(); - const baseUrl = await getBaseRegistryUrl(); - const registryUrl = new URLParse(baseUrl).set("pathname", name).toString(); - let json: any; - - try { - json = await downloadJson({ url: registryUrl }).promise; - - if (!json || json.error || typeof json.versions !== "object" || !json.versions) { - const message = json?.error ? `: ${json.error}` : ""; - - Notifications.error(`Failed to get registry information for that extension${message}`); - - return disposer(); - } - } catch (error) { - if (error instanceof SyntaxError) { - // assume invalid JSON - console.warn("Set registry has invalid json", { url: baseUrl }, error); - Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON"); - } else { - console.error("Failed to download registry information", error); - Notifications.error(`Failed to get valid registry information for that extension. ${error}`); - } - - return disposer(); - } - - if (version) { - if (!json.versions[version]) { - if (json["dist-tags"][version]) { - version = json["dist-tags"][version]; - } else { - Notifications.error( -

- {"The "} - {name} - {" extension does not have a version or tag "} - {version} - . -

, - ); - - return disposer(); - } - } - } else { - const versions = Object.keys(json.versions) - .map( - version => - new SemVer(version, { loose: true, includePrerelease: true }), - ) - // ignore pre-releases for auto picking the version - .filter(version => version.prerelease.length === 0); - const latestVersion = lodash.reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev); - - if (!latestVersion) { - console.error("No versions supplied for that extension", { name }); - Notifications.error(`No versions found for ${name}`); - - return disposer(); - } - - version = latestVersion.format(); - } - - if (requireConfirmation) { - const proceed = await ConfirmDialog.confirm({ - message: ( -

- {"Are you sure you want to install "} - {`${name}@${version}`} - ? -

- ), - labelCancel: "Cancel", - labelOk: "Install", - }); - - if (!proceed) { - return disposer(); - } - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const url = json.versions[version!].dist.tarball; - const fileName = path.basename(url); - const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 }); - - return attemptInstall({ fileName, dataP }, disposer); -}; diff --git a/src/renderer/components/+extensions/confirm-uninstall-extension.injectable.tsx b/src/renderer/components/+extensions/confirm-uninstall-extension.injectable.tsx new file mode 100644 index 0000000000..2157d32b21 --- /dev/null +++ b/src/renderer/components/+extensions/confirm-uninstall-extension.injectable.tsx @@ -0,0 +1,56 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import React from "react"; +import type { InstalledExtension } from "../../../extensions/extension-discovery/extension-discovery"; +import type { LensExtensionId } from "../../../extensions/lens-extension"; +import { extensionDisplayName } from "../../../extensions/lens-extension"; +import type { Confirm } from "../confirm-dialog/confirm.injectable"; +import confirmInjectable from "../confirm-dialog/confirm.injectable"; +import uninstallExtensionInjectable from "./uninstall-extension/uninstall-extension.injectable"; + +interface Dependencies { + uninstallExtension: (id: LensExtensionId) => Promise; + confirm: Confirm; +} + +export type ConfirmUninstallExtension = (ext: InstalledExtension) => Promise; + +const confirmUninstallExtension = ({ + uninstallExtension, + confirm, +}: Dependencies): ConfirmUninstallExtension => ( + async (extension) => { + const displayName = extensionDisplayName( + extension.manifest.name, + extension.manifest.version, + ); + const confirmed = await confirm({ + message: ( +

+ {"Are you sure you want to uninstall extension "} + {displayName} + ? +

+ ), + labelOk: "Yes", + labelCancel: "No", + }); + + if (confirmed) { + await uninstallExtension(extension.id); + } + } +); + +const confirmUninstallExtensionInjectable = getInjectable({ + id: "confirm-uninstall-extension", + instantiate: (di) => confirmUninstallExtension({ + uninstallExtension: di.inject(uninstallExtensionInjectable), + confirm: di.inject(confirmInjectable), + }), +}); + +export default confirmUninstallExtensionInjectable; diff --git a/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.injectable.ts b/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.injectable.ts deleted file mode 100644 index 560b5adc74..0000000000 --- a/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.injectable.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { confirmUninstallExtension } from "./confirm-uninstall-extension"; -import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable"; - -const confirmUninstallExtensionInjectable = getInjectable({ - id: "confirm-uninstall-extension", - - instantiate: (di) => - confirmUninstallExtension({ - uninstallExtension: di.inject(uninstallExtensionInjectable), - }), -}); - -export default confirmUninstallExtensionInjectable; diff --git a/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.tsx b/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.tsx deleted file mode 100644 index 71bbe16a8b..0000000000 --- a/src/renderer/components/+extensions/confirm-uninstall-extension/confirm-uninstall-extension.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import React from "react"; -import type { InstalledExtension } from "../../../../extensions/extension-discovery/extension-discovery"; -import type { LensExtensionId } from "../../../../extensions/lens-extension"; -import { extensionDisplayName } from "../../../../extensions/lens-extension"; -import { ConfirmDialog } from "../../confirm-dialog"; - -interface Dependencies { - uninstallExtension: (id: LensExtensionId) => Promise; -} - -export const confirmUninstallExtension = - ({ uninstallExtension }: Dependencies) => - async (extension: InstalledExtension): Promise => { - const displayName = extensionDisplayName( - extension.manifest.name, - extension.manifest.version, - ); - const confirmed = await ConfirmDialog.confirm({ - message: ( -

- {"Are you sure you want to uninstall extension "} - {displayName} - ? -

- ), - labelOk: "Yes", - labelCancel: "No", - }); - - if (confirmed) { - await uninstallExtension(extension.id); - } - }; diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 59744ccea6..d937450ab8 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -26,7 +26,8 @@ import { withInjectables } from "@ogre-tools/injectable-react"; import userExtensionsInjectable from "./user-extensions/user-extensions.injectable"; import enableExtensionInjectable from "./enable-extension/enable-extension.injectable"; import disableExtensionInjectable from "./disable-extension/disable-extension.injectable"; -import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable"; +import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable"; +import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable"; import installFromInputInjectable from "./install-from-input/install-from-input.injectable"; import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable"; import type { LensExtensionId } from "../../../extensions/lens-extension"; @@ -39,7 +40,7 @@ interface Dependencies { userExtensions: IComputedValue; enableExtension: (id: LensExtensionId) => void; disableExtension: (id: LensExtensionId) => void; - confirmUninstallExtension: (extension: InstalledExtension) => Promise; + confirmUninstallExtension: ConfirmUninstallExtension; installFromInput: (input: string) => Promise; installFromSelectFileDialog: () => Promise; installOnDrop: (files: File[]) => Promise; diff --git a/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts b/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts index 36596cd1ff..f5417758e8 100644 --- a/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts +++ b/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; import { installFromInput } from "./install-from-input"; -import attemptInstallByInfoInjectable from "../attempt-install-by-info/attempt-install-by-info.injectable"; +import attemptInstallByInfoInjectable from "../attempt-install-by-info.injectable"; import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; diff --git a/src/renderer/components/+extensions/install-from-input/install-from-input.tsx b/src/renderer/components/+extensions/install-from-input/install-from-input.tsx index 3f4c9a4480..366e3498ca 100644 --- a/src/renderer/components/+extensions/install-from-input/install-from-input.tsx +++ b/src/renderer/components/+extensions/install-from-input/install-from-input.tsx @@ -12,7 +12,7 @@ import path from "path"; import React from "react"; import { readFileNotify } from "../read-file-notify/read-file-notify"; import type { InstallRequest } from "../attempt-install/install-request"; -import type { ExtensionInfo } from "../attempt-install-by-info/attempt-install-by-info"; +import type { ExtensionInfo } from "../attempt-install-by-info.injectable"; import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; import { AsyncInputValidationError } from "../../input/input_validators"; diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/details.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/details.tsx index 6a23c4eb58..7fcb29094b 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/details.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/details.tsx @@ -12,7 +12,6 @@ import React from "react"; import type { ClusterRoleBinding } from "../../../../common/k8s-api/endpoints"; import { autoBind, ObservableHashSet, prevDefault } from "../../../utils"; import { AddRemoveButtons } from "../../add-remove-buttons"; -import { ConfirmDialog } from "../../confirm-dialog"; import { DrawerTitle } from "../../drawer"; import type { KubeObjectDetailsProps } from "../../kube-object-details"; import { KubeObjectMeta } from "../../kube-object-meta"; @@ -20,15 +19,22 @@ import { Table, TableCell, TableHead, TableRow } from "../../table"; import { ClusterRoleBindingDialog } from "./dialog"; import { clusterRoleBindingStore } from "./legacy-store"; import { hashSubject } from "../hashers"; +import type { OpenConfirmDialog } from "../../confirm-dialog/open.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import openConfirmDialogInjectable from "../../confirm-dialog/open.injectable"; export interface ClusterRoleBindingDetailsProps extends KubeObjectDetailsProps { } +interface Dependencies { + openConfirmDialog: OpenConfirmDialog; +} + @observer -export class ClusterRoleBindingDetails extends React.Component { +class NonInjectedClusterRoleBindingDetails extends React.Component { selectedSubjects = new ObservableHashSet([], hashSubject); - constructor(props: ClusterRoleBindingDetailsProps) { + constructor(props: ClusterRoleBindingDetailsProps & Dependencies) { super(props); autoBind(this); } @@ -42,10 +48,10 @@ export class ClusterRoleBindingDetails extends React.Component clusterRoleBindingStore.removeSubjects(clusterRoleBinding, selectedSubjects), labelOk: `Remove`, message: ( @@ -127,3 +133,10 @@ export class ClusterRoleBindingDetails extends React.Component(NonInjectedClusterRoleBindingDetails, { + getProps: (di, props) => ({ + ...props, + openConfirmDialog: di.inject(openConfirmDialogInjectable), + }), +}); diff --git a/src/renderer/components/+user-management/+role-bindings/details.tsx b/src/renderer/components/+user-management/+role-bindings/details.tsx index f532c5454a..bf0f573b5e 100644 --- a/src/renderer/components/+user-management/+role-bindings/details.tsx +++ b/src/renderer/components/+user-management/+role-bindings/details.tsx @@ -11,7 +11,6 @@ import React from "react"; import type { RoleBinding } from "../../../../common/k8s-api/endpoints"; import { prevDefault } from "../../../utils"; import { AddRemoveButtons } from "../../add-remove-buttons"; -import { ConfirmDialog } from "../../confirm-dialog"; import { DrawerTitle } from "../../drawer"; import type { KubeObjectDetailsProps } from "../../kube-object-details"; import { KubeObjectMeta } from "../../kube-object-meta"; @@ -20,13 +19,20 @@ import { RoleBindingDialog } from "./dialog"; import { roleBindingStore } from "./legacy-store"; import { ObservableHashSet } from "../../../../common/utils/hash-set"; import { hashSubject } from "../hashers"; +import type { OpenConfirmDialog } from "../../confirm-dialog/open.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import openConfirmDialogInjectable from "../../confirm-dialog/open.injectable"; export interface RoleBindingDetailsProps extends KubeObjectDetailsProps { } +interface Dependencies { + openConfirmDialog: OpenConfirmDialog; +} + @observer -export class RoleBindingDetails extends React.Component { - selectedSubjects = new ObservableHashSet([], hashSubject); +class NonInjectedRoleBindingDetails extends React.Component { + private readonly selectedSubjects = new ObservableHashSet([], hashSubject); async componentDidMount() { disposeOnUnmount(this, [ @@ -37,10 +43,10 @@ export class RoleBindingDetails extends React.Component } removeSelectedSubjects = () => { - const { object: roleBinding } = this.props; + const { object: roleBinding, openConfirmDialog } = this.props; const { selectedSubjects } = this; - ConfirmDialog.open({ + openConfirmDialog({ ok: () => roleBindingStore.removeSubjects(roleBinding, selectedSubjects.toJSON()), labelOk: `Remove`, message: ( @@ -122,3 +128,10 @@ export class RoleBindingDetails extends React.Component ); } } + +export const RoleBindingDetails = withInjectables(NonInjectedRoleBindingDetails, { + getProps: (di, props) => ({ + ...props, + openConfirmDialog: di.inject(openConfirmDialogInjectable), + }), +}); diff --git a/src/renderer/components/+workloads-cronjobs/cron-job-menu.tsx b/src/renderer/components/+workloads-cronjobs/cron-job-menu.tsx index bce6d47856..18fa009467 100644 --- a/src/renderer/components/+workloads-cronjobs/cron-job-menu.tsx +++ b/src/renderer/components/+workloads-cronjobs/cron-job-menu.tsx @@ -9,80 +9,95 @@ import { cronJobApi } from "../../../common/k8s-api/endpoints"; import { MenuItem } from "../menu"; import { CronJobTriggerDialog } from "./cronjob-trigger-dialog"; import { Icon } from "../icon"; -import { ConfirmDialog } from "../confirm-dialog"; import { Notifications } from "../notifications"; +import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import openConfirmDialogInjectable from "../confirm-dialog/open.injectable"; -export function CronJobMenu(props: KubeObjectMenuProps) { - const { object, toolbar } = props; +export interface CronJobMenuProps extends KubeObjectMenuProps {} - return ( - <> - CronJobTriggerDialog.open(object)}> - - Trigger - - - {object.isSuspend() - ? ( - ConfirmDialog.open({ - ok: async () => { - try { - await cronJobApi.resume({ namespace: object.getNs(), name: object.getName() }); - } catch (err) { - Notifications.checkedError(err, "Unknown error occured while resuming CronJob"); - } - }, - labelOk: `Resume`, - message: ( -

- {"Resume CronJob "} - {object.getName()} - ? -

- ), - })} - > - - Resume -
- ) - : ( - ConfirmDialog.open({ - ok: async () => { - try { - await cronJobApi.suspend({ namespace: object.getNs(), name: object.getName() }); - } catch (err) { - Notifications.checkedError(err, "Unknown error occured while suspending CronJob"); - } - }, - labelOk: `Suspend`, - message: ( -

- {"Suspend CronJob "} - {object.getName()} - ? -

), - })} - > - - Suspend -
- ) - } - - ); +interface Dependencies { + openConfirmDialog: OpenConfirmDialog; } + +const NonInjectedCronJobMenu = ({ + object, + toolbar, + openConfirmDialog, +}: Dependencies & CronJobMenuProps) => ( + <> + CronJobTriggerDialog.open(object)}> + + Trigger + + + {object.isSuspend() + ? ( + openConfirmDialog({ + ok: async () => { + try { + await cronJobApi.resume({ namespace: object.getNs(), name: object.getName() }); + } catch (err) { + Notifications.checkedError(err, "Unknown error occured while resuming CronJob"); + } + }, + labelOk: `Resume`, + message: ( +

+ {"Resume CronJob "} + {object.getName()} + ? +

+ ), + })} + > + + Resume +
+ ) + : ( + openConfirmDialog({ + ok: async () => { + try { + await cronJobApi.suspend({ namespace: object.getNs(), name: object.getName() }); + } catch (err) { + Notifications.checkedError(err, "Unknown error occured while suspending CronJob"); + } + }, + labelOk: `Suspend`, + message: ( +

+ {"Suspend CronJob "} + {object.getName()} + ? +

), + })} + > + + Suspend +
+ ) + } + +); + +export const CronJobMenu = withInjectables(NonInjectedCronJobMenu, { + getProps: (di, props) => ({ + ...props, + openConfirmDialog: di.inject(openConfirmDialogInjectable), + }), +}); diff --git a/src/renderer/components/+workloads-deployments/deployment-menu.tsx b/src/renderer/components/+workloads-deployments/deployment-menu.tsx index 696cc0e20b..ce8b031ce9 100644 --- a/src/renderer/components/+workloads-deployments/deployment-menu.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-menu.tsx @@ -7,18 +7,20 @@ import type { KubeObjectMenuProps } from "../kube-object-menu"; import type { Deployment, DeploymentApi } from "../../../common/k8s-api/endpoints"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; -import { ConfirmDialog } from "../confirm-dialog"; import { Notifications } from "../notifications"; import type { OpenDeploymentScaleDialog } from "./scale/open.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; import deploymentApiInjectable from "../../../common/k8s-api/endpoints/deployment.api.injectable"; import openDeploymentScaleDialogInjectable from "./scale/open.injectable"; +import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable"; +import openConfirmDialogInjectable from "../confirm-dialog/open.injectable"; export interface DeploymentMenuProps extends KubeObjectMenuProps {} interface Dependencies { openDeploymentScaleDialog: OpenDeploymentScaleDialog; deploymentApi: DeploymentApi; + openConfirmDialog: OpenConfirmDialog; } const NonInjectedDeploymentMenu = ({ @@ -26,6 +28,7 @@ const NonInjectedDeploymentMenu = ({ object, openDeploymentScaleDialog, toolbar, + openConfirmDialog, }: Dependencies & DeploymentMenuProps) => ( <> openDeploymentScaleDialog(object)}> @@ -37,7 +40,7 @@ const NonInjectedDeploymentMenu = ({ Scale ConfirmDialog.open({ + onClick={() => openConfirmDialog({ ok: async () => { try { @@ -74,5 +77,6 @@ export const DeploymentMenu = withInjectables ...props, deploymentApi: di.inject(deploymentApiInjectable), openDeploymentScaleDialog: di.inject(openDeploymentScaleDialogInjectable), + openConfirmDialog: di.inject(openConfirmDialogInjectable), }), }); diff --git a/src/renderer/components/+workloads-statefulsets/stateful-set-menu.tsx b/src/renderer/components/+workloads-statefulsets/stateful-set-menu.tsx deleted file mode 100644 index 4231d8b1f6..0000000000 --- a/src/renderer/components/+workloads-statefulsets/stateful-set-menu.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import React from "react"; -import type { KubeObjectMenuProps } from "../kube-object-menu"; -import type { StatefulSet } from "../../../common/k8s-api/endpoints"; -import { MenuItem } from "../menu"; -import { Icon } from "../icon"; -import type { OpenStatefulSetScaleDialog } from "./scale/open-dialog.injectable"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import openStatefulSetScaleDialogInjectable from "./scale/open-dialog.injectable"; - -export interface StatefulSetMenuProps extends KubeObjectMenuProps {} - -interface Dependencies { - openStatefulSetScaleDialog: OpenStatefulSetScaleDialog; -} - -const NonInjectedStatefulSetMenu = ({ - object, - toolbar, - openStatefulSetScaleDialog, -}: Dependencies & StatefulSetMenuProps) => ( - <> - openStatefulSetScaleDialog(object)}> - - Scale - - -); - -export const StatefulSetMenu = withInjectables(NonInjectedStatefulSetMenu, { - getProps: (di, props) => ({ - ...props, - openStatefulSetScaleDialog: di.inject(openStatefulSetScaleDialogInjectable), - }), -}); diff --git a/src/renderer/components/animate/animate.tsx b/src/renderer/components/animate/animate.tsx index 94a83991fa..920e6b8cef 100644 --- a/src/renderer/components/animate/animate.tsx +++ b/src/renderer/components/animate/animate.tsx @@ -5,8 +5,8 @@ import "./animate.scss"; import React from "react"; -import { observable, reaction, makeObservable } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; +import { observable, makeObservable } from "mobx"; +import { observer } from "mobx-react"; import { cssNames, noop } from "../../utils"; export type AnimateName = "opacity" | "slide-right" | "opacity-scale" | string; @@ -47,15 +47,24 @@ class DefaultedAnimate extends React.Component>; } + private toggle(enter: boolean) { + if (enter) { + this.enter(); + } else { + this.leave(); + } + } + componentDidMount() { - disposeOnUnmount(this, [ - reaction(() => this.props.enter, enter => { - if (enter) this.enter(); - else this.leave(); - }, { - fireImmediately: true, - }), - ]); + this.toggle(this.props.enter); + } + + componentDidUpdate(prevProps: Readonly): void { + const { enter } = this.props; + + if (prevProps.enter !== enter) { + this.toggle(enter); + } } enter() { diff --git a/src/renderer/components/confirm-dialog/confirm-dialog.tsx b/src/renderer/components/confirm-dialog/confirm-dialog.tsx index c5270b3487..82ace9b7be 100644 --- a/src/renderer/components/confirm-dialog/confirm-dialog.tsx +++ b/src/renderer/components/confirm-dialog/confirm-dialog.tsx @@ -7,7 +7,8 @@ import "./confirm-dialog.scss"; import type { ReactNode } from "react"; import React from "react"; -import { observable, makeObservable } from "mobx"; +import type { IObservableValue } from "mobx"; +import { observable, makeObservable, computed } from "mobx"; import { observer } from "mobx-react"; import { cssNames, noop, prevDefault } from "../../utils"; import type { ButtonProps } from "../button"; @@ -16,6 +17,8 @@ import type { DialogProps } from "../dialog"; import { Dialog } from "../dialog"; import { Icon } from "../icon"; import { Notifications } from "../notifications"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import confirmDialogStateInjectable from "./state.injectable"; export interface ConfirmDialogProps extends Partial { } @@ -34,6 +37,10 @@ export interface ConfirmDialogBooleanParams { cancelButtonProps?: Partial; } +interface Dependencies { + state: IObservableValue; +} + const defaultParams = { ok: noop, cancel: noop, @@ -42,41 +49,18 @@ const defaultParams = { icon: , }; -const dialogState = observable.box(); - @observer -export class ConfirmDialog extends React.Component { +class NonInjectedConfirmDialog extends React.Component { @observable isSaving = false; - constructor(props: ConfirmDialogProps & typeof defaultParams) { + constructor(props: ConfirmDialogProps & Dependencies) { super(props); makeObservable(this); } - static open(params: ConfirmDialogParams) { - dialogState.set(params); - } - - static confirm(params: ConfirmDialogBooleanParams): Promise { - return new Promise(resolve => { - ConfirmDialog.open({ - ok: () => resolve(true), - cancel: () => resolve(false), - ...params, - }); - }); - } - - static defaultParams = { - ok: noop, - cancel: noop, - labelOk: "Ok", - labelCancel: "Cancel", - icon: , - }; - + @computed get params() { - return Object.assign({}, ConfirmDialog.defaultParams, dialogState.get()); + return Object.assign({}, defaultParams, this.props.state.get() ?? {} as ConfirmDialogParams); } ok = async () => { @@ -100,7 +84,7 @@ export class ConfirmDialog extends React.Component { ); } finally { this.isSaving = false; - dialogState.set(undefined); + this.props.state.set(undefined); } }; @@ -128,18 +112,18 @@ export class ConfirmDialog extends React.Component { ); } finally { this.isSaving = false; - dialogState.set(undefined); + this.props.state.set(undefined); } }; render() { - const { className, ...dialogProps } = this.props; + const { state, className, ...dialogProps } = this.props; + const isOpen = Boolean(state.get()); const { icon, labelOk, labelCancel, message, okButtonProps = {}, cancelButtonProps = {}, } = this.params; - const isOpen = Boolean(dialogState.get()); return ( { {...(isOpen ? { "data-testid": "confirmation-dialog" } : {})} >
- {icon} + {icon} {" "} {message}
@@ -178,3 +162,10 @@ export class ConfirmDialog extends React.Component { ); } } + +export const ConfirmDialog = withInjectables(NonInjectedConfirmDialog, { + getProps: (di, props) => ({ + ...props, + state: di.inject(confirmDialogStateInjectable), + }), +}); diff --git a/src/renderer/components/confirm-dialog/confirm.injectable.ts b/src/renderer/components/confirm-dialog/confirm.injectable.ts new file mode 100644 index 0000000000..ac7be031bf --- /dev/null +++ b/src/renderer/components/confirm-dialog/confirm.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { ConfirmDialogBooleanParams } from "./confirm-dialog"; +import openConfirmDialogInjectable from "./open.injectable"; + +export type Confirm = (params: ConfirmDialogBooleanParams) => Promise; + +const confirmInjectable = getInjectable({ + id: "confirm", + instantiate: (di): Confirm => { + const open = di.inject(openConfirmDialogInjectable); + + return (params) => new Promise(resolve => { + open({ + ok: () => resolve(true), + cancel: () => resolve(false), + ...params, + }); + }); + }, +}); + +export default confirmInjectable; diff --git a/src/renderer/components/confirm-dialog/open.injectable.ts b/src/renderer/components/confirm-dialog/open.injectable.ts new file mode 100644 index 0000000000..5e8b806891 --- /dev/null +++ b/src/renderer/components/confirm-dialog/open.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { ConfirmDialogParams } from "./confirm-dialog"; +import confirmDialogStateInjectable from "./state.injectable"; + +export type OpenConfirmDialog = (params: ConfirmDialogParams) => void; + +const openConfirmDialogInjectable = getInjectable({ + id: "open-confirm-dialog", + instantiate: (di): OpenConfirmDialog => { + const state = di.inject(confirmDialogStateInjectable); + + return params => state.set(params); + }, +}); + +export default openConfirmDialogInjectable; diff --git a/src/renderer/components/confirm-dialog/state.injectable.ts b/src/renderer/components/confirm-dialog/state.injectable.ts new file mode 100644 index 0000000000..a5f1212cce --- /dev/null +++ b/src/renderer/components/confirm-dialog/state.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { observable } from "mobx"; +import type { ConfirmDialogParams } from "."; + +const confirmDialogStateInjectable = getInjectable({ + id: "confirm-dialog-state", + instantiate: () => observable.box(), +}); + +export default confirmDialogStateInjectable; diff --git a/src/renderer/components/confirm-dialog/with-confirm.injectable.ts b/src/renderer/components/confirm-dialog/with-confirm.injectable.ts new file mode 100644 index 0000000000..364c9266de --- /dev/null +++ b/src/renderer/components/confirm-dialog/with-confirm.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { ConfirmDialogParams } from "./confirm-dialog"; +import { getInjectable } from "@ogre-tools/injectable"; +import openConfirmDialogInjectable from "./open.injectable"; + +export type WithConfirmation = (params: ConfirmDialogParams) => () => void; + +const withConfirmationInjectable = getInjectable({ + id: "with-confirmation", + instantiate: (di): WithConfirmation => { + const open = di.inject(openConfirmDialogInjectable); + + return (params) => () => open(params); + }, +}); + +export default withConfirmationInjectable; diff --git a/src/renderer/components/drawer/drawer.tsx b/src/renderer/components/drawer/drawer.tsx index 9adcb11cdb..41e06799da 100644 --- a/src/renderer/components/drawer/drawer.tsx +++ b/src/renderer/components/drawer/drawer.tsx @@ -208,7 +208,7 @@ class NonInjectedDrawer extends React.Component {toolbar} - +
{ - menuItem.onClick(); - }, - message: menuItem.confirm.message, - }); - } else { - menuItem.onClick(); - } +interface Dependencies { + normalizeMenuItem: NormalizeCatalogEntityContextMenu; } -export const HotbarIcon = observer(({ menuItems = [], size = 40, tooltip, ...props }: HotbarIconProps) => { +const NonInjectedHotbarIcon = observer(({ + menuItems = [], + size = 40, + tooltip, + normalizeMenuItem, + ...props +}: HotbarIconProps & Dependencies) => { const { uid, title, src, material, active, className, source, disabled, onMenuOpen, onClick, children, ...rest } = props; const id = `hotbarIcon-${uid}`; const [menuOpen, setMenuOpen] = useState(false); @@ -88,13 +83,22 @@ export const HotbarIcon = observer(({ menuItems = [], size = 40, tooltip, ...pro close={() => toggleMenu()} > { - menuItems.map((menuItem) => ( - onMenuItemClick(menuItem)}> - {menuItem.title} - - )) + menuItems + .map(normalizeMenuItem) + .map((menuItem) => ( + + {menuItem.title} + + )) }
); }); + +export const HotbarIcon = withInjectables(NonInjectedHotbarIcon, { + getProps: (di, props) => ({ + ...props, + normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable), + }), +}); diff --git a/src/renderer/components/hotbar/hotbar-remove-command.tsx b/src/renderer/components/hotbar/hotbar-remove-command.tsx index c99c2e3e0e..0199b7f739 100644 --- a/src/renderer/components/hotbar/hotbar-remove-command.tsx +++ b/src/renderer/components/hotbar/hotbar-remove-command.tsx @@ -7,19 +7,22 @@ import React from "react"; import { observer } from "mobx-react"; import { Select } from "../select"; import hotbarStoreInjectable from "../../../common/hotbars/store.injectable"; -import { ConfirmDialog } from "../confirm-dialog"; import { withInjectables } from "@ogre-tools/injectable-react"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; import type { HotbarStore } from "../../../common/hotbars/store"; +import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable"; +import openConfirmDialogInjectable from "../confirm-dialog/open.injectable"; interface Dependencies { closeCommandOverlay: () => void; + openConfirmDialog: OpenConfirmDialog; hotbarStore: HotbarStore; } const NonInjectedHotbarRemoveCommand = observer(({ closeCommandOverlay, hotbarStore, + openConfirmDialog, }: Dependencies) => (