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`] = `
+
+
+
+`;
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}
- />
-
-
+ return (
+
+
+
+
+ )}
+ onOverSizeLimit={OverSizeLimitStyle.FILTER}
+ handler={onIconPick}
+ />
+
- );
- }
-}
+
+ );
+});
+
+export const ClusterIconSetting = withInjectables
(NonInjectedClusterIconSetting, {
+ getProps: (di, props) => {
+ const computedInjectMany = di.inject(computedInjectManyInjectable);
+
+ return {
+ ...props,
+ menuItems: computedInjectMany(clusterIconSettingsMenuInjectionToken),
+ showErrorNotification: di.inject(showErrorNotificationInjectable),
+ };
+ },
+});