diff --git a/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap b/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap new file mode 100644 index 0000000000..97eac8fca2 --- /dev/null +++ b/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Icon settings given no external registrations for cluster settings menu injection token renders 1`] = ` + +
+
+
+
+
+ + +
+
+ + + more_horiz + + +
+
+
+ +`; diff --git a/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx b/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx new file mode 100644 index 0000000000..74fd2a6654 --- /dev/null +++ b/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx @@ -0,0 +1,101 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { DiContainer } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { RenderResult } from "@testing-library/react"; +import React from "react"; +import { KubernetesCluster } from "../../../../common/catalog-entities"; +import { Cluster } from "../../../../common/cluster/cluster"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import { renderFor } from "../../test-utils/renderFor"; +import { ClusterIconSetting } from "../icon-settings"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { clusterIconSettingsMenuInjectionToken } from "../cluster-settings-menu-injection-token"; +import { runInAction } from "mobx"; + +const cluster = new Cluster({ + contextName: "some-context", + id: "some-id", + kubeConfigPath: "/some/path/to/kubeconfig", +}, { + clusterServerUrl: "https://localhost:9999", +}); + +const clusterEntity = new KubernetesCluster({ + metadata: { + labels: {}, + name: "some-kubernetes-cluster", + uid: "some-entity-id", + }, + spec: { + kubeconfigContext: "some-context", + kubeconfigPath: "/some/path/to/kubeconfig", + }, + status: { + phase: "connecting", + }, +}); + +const newMenuItem = getInjectable({ + id: "cluster-icon-settings-menu-test-item", + + instantiate: () => ({ + id: "test-menu-item", + title: "Hello World", + onClick: (preferences) => { + preferences.clusterName = "Hello World"; + }, + }), + + injectionToken: clusterIconSettingsMenuInjectionToken, +}); + +describe("Icon settings", () => { + let rendered: RenderResult; + let di: DiContainer; + + beforeEach(() => { + di = getDiForUnitTesting(); + + const render = renderFor(di); + + rendered = render( + , + ); + }); + + describe("given no external registrations for cluster settings menu injection token", () => { + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("has predefined menu item", async () => { + userEvent.click(await screen.findByTestId("icon-for-menu-actions-for-cluster-icon-settings-for-some-entity-id")); + + expect(rendered.getByText("Upload Icon")).toBeInTheDocument(); + }); + + it("has menu item from build-in registration", async () => { + userEvent.click(await screen.findByTestId("icon-for-menu-actions-for-cluster-icon-settings-for-some-entity-id")); + + expect(rendered.getByText("Clear")).toBeInTheDocument(); + }); + }); + + describe("given external registrations for cluster settings menu injection token", () => { + beforeEach(() => { + runInAction(() => { + di.register(newMenuItem); + }); + }); + + it("has menu item from external registration", async () => { + userEvent.click(await screen.findByTestId("icon-for-menu-actions-for-cluster-icon-settings-for-some-entity-id")); + + expect(rendered.getByText("Hello World")).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts new file mode 100644 index 0000000000..697c68bb40 --- /dev/null +++ b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts @@ -0,0 +1,27 @@ +/** + * 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 { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token"; + +const clusterIconSettingsMenuClearItem = getInjectable({ + id: "cluster-icon-settings-menu-clear-item", + + instantiate: () => ({ + id: "clear-icon-menu-item", + title: "Clear", + disabled: (preferences) => !preferences.icon, + onClick: (preferences) => { + /** + * NOTE: this needs to be `null` rather than `undefined` so that we can + * tell the difference between it not being there and being cleared. + */ + preferences.icon = null; + }, + }), + + injectionToken: clusterIconSettingsMenuInjectionToken, +}); + +export default clusterIconSettingsMenuClearItem; diff --git a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-injection-token.ts b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-injection-token.ts new file mode 100644 index 0000000000..15dbba2754 --- /dev/null +++ b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-injection-token.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { ClusterPreferences } from "../../../common/cluster-types"; + +export interface ClusterIconMenuItem { + id: string; + title: string; + disabled?: (preferences: ClusterPreferences) => boolean; + onClick: (preferences: ClusterPreferences) => void; +} + +export const clusterIconSettingsMenuInjectionToken = getInjectionToken({ + id: "cluster-icon-settings-menu-injection-token", +}); diff --git a/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx b/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx index f5813368e8..cd66c50694 100644 --- a/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx +++ b/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx @@ -3,106 +3,109 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import React from "react"; -import type { Cluster } from "../../../common/cluster/cluster"; -import { observable } from "mobx"; +import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { IComputedValue } from "mobx"; import { observer } from "mobx-react"; +import React from "react"; import type { KubernetesCluster } from "../../../common/catalog-entities"; +import type { Cluster } from "../../../common/cluster/cluster"; +import { Avatar } from "../avatar"; import { FilePicker, OverSizeLimitStyle } from "../file-picker"; import { MenuActions, MenuItem } from "../menu"; -import { Avatar } from "../avatar"; -import autoBindReact from "auto-bind/react"; - -enum GeneralInputStatus { - CLEAN = "clean", - ERROR = "error", -} +import type { ShowNotification } from "../notifications"; +import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; +import type { ClusterIconMenuItem } from "./cluster-settings-menu-injection-token"; +import { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token"; export interface ClusterIconSettingProps { cluster: Cluster; entity: KubernetesCluster; } -@observer -export class ClusterIconSetting extends React.Component { - @observable status = GeneralInputStatus.CLEAN; - @observable errorText?: string; +interface Dependencies { + menuItems: IComputedValue; + showErrorNotification: ShowNotification; +} - constructor(props: ClusterIconSettingProps) { - super(props); - autoBindReact(this); - } - private element = React.createRef(); +const NonInjectedClusterIconSetting = observer((props: ClusterIconSettingProps & Dependencies) => { + const element = React.createRef(); + const { cluster, entity } = props; + const menuId = `menu-actions-for-cluster-icon-settings-for-${entity.getId()}`; - async onIconPick([file]: File[]) { + const onIconPick = async ([file]: File[]) => { if (!file) { return; } - const { cluster } = this.props; - try { const buf = Buffer.from(await file.arrayBuffer()); cluster.preferences.icon = `data:${file.type};base64,${buf.toString("base64")}`; } catch (e) { - this.errorText = String(e); - this.status = GeneralInputStatus.ERROR; + props.showErrorNotification(String(e)); } - } + }; - clearIcon() { - /** - * NOTE: this needs to be `null` rather than `undefined` so that we can - * tell the difference between it not being there and being cleared. - */ - this.props.cluster.preferences.icon = null; - } - - onUploadClick() { - this.element + const onUploadClick = () => { + element .current ?.querySelector("input[type=file]") ?.click(); - } + }; - render() { - const { entity } = this.props; - - return ( -
-
-
- - )} - onOverSizeLimit={OverSizeLimitStyle.FILTER} - handler={this.onIconPick} - /> -
- - - Upload Icon - - this.clearIcon()} disabled={!this.props.cluster.preferences.icon}> - Clear - - + return ( +
+
+
+ + )} + onOverSizeLimit={OverSizeLimitStyle.FILTER} + handler={onIconPick} + />
+ + + Upload Icon + + {props.menuItems.get().map(item => ( + item.onClick(cluster.preferences)} + key={item.id} + disabled={item.disabled?.(cluster.preferences)}> + {item.title} + + ), + )} +
- ); - } -} +
+ ); +}); + +export const ClusterIconSetting = withInjectables(NonInjectedClusterIconSetting, { + getProps: (di, props) => { + const computedInjectMany = di.inject(computedInjectManyInjectable); + + return { + ...props, + menuItems: computedInjectMany(clusterIconSettingsMenuInjectionToken), + showErrorNotification: di.inject(showErrorNotificationInjectable), + }; + }, +});