diff --git a/src/behaviours/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap b/src/behaviours/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap
new file mode 100644
index 0000000000..3ae620ccf6
--- /dev/null
+++ b/src/behaviours/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap
@@ -0,0 +1,11084 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with failure renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given change in configuration renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given clicking the context menu for second namespace, when clicking to edit namespace renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given clicking the context menu for second namespace, when clicking to edit namespace when second namespace resolves renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given clicking the context menu for second namespace, when clicking to edit namespace when second namespace resolves when clicking dock tab for the first namespace renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given invalid change in configuration renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given no changes in the configuration, when selecting to save renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given no changes in the configuration, when selecting to save when saving resolves with failure renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given no changes in the configuration, when selecting to save when saving resolves with success renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace when selecting to cancel renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace when selecting to save and close renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace when selecting to save and close when saving resolves with failure renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace when selecting to save and close when saving resolves with success renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves without namespace renders 1`] = `
+
+
+
+`;
diff --git a/src/behaviours/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap b/src/behaviours/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap
new file mode 100644
index 0000000000..25a80a2702
--- /dev/null
+++ b/src/behaviours/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap
@@ -0,0 +1,1188 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`cluster/namespaces - edit namespaces from previously opened tab given tab was previously opened, when application is started renders 1`] = `
+
+
+
+`;
+
+exports[`cluster/namespaces - edit namespaces from previously opened tab given tab was previously opened, when application is started when call for namespace resolves with namespace renders 1`] = `
+
+
+
+`;
diff --git a/src/behaviours/cluster/namespaces/edit-namespace-from-new-tab.test.tsx b/src/behaviours/cluster/namespaces/edit-namespace-from-new-tab.test.tsx
new file mode 100644
index 0000000000..49610f138d
--- /dev/null
+++ b/src/behaviours/cluster/namespaces/edit-namespace-from-new-tab.test.tsx
@@ -0,0 +1,991 @@
+/**
+ * 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 type { RenderResult } from "@testing-library/react";
+import { fireEvent } from "@testing-library/react";
+import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
+import navigateToNamespacesInjectable from "../../../common/front-end-routing/routes/cluster/namespaces/navigate-to-namespaces.injectable";
+import React from "react";
+import createEditResourceTabInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-tab.injectable";
+import getRandomIdForEditResourceTabInjectable from "../../../renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
+import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
+import type { CallForPatchResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
+import callForPatchResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
+import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
+import { Namespace } from "../../../common/k8s-api/endpoints";
+import showSuccessNotificationInjectable from "../../../renderer/components/notifications/show-success-notification.injectable";
+import showErrorNotificationInjectable from "../../../renderer/components/notifications/show-error-notification.injectable";
+import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
+import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
+import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
+import { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready";
+
+jest.mock("../../../renderer/components/tooltip/withTooltip", () => ({
+ withTooltip:
+ (Target: any) =>
+ ({ tooltip, ...props }: any) => {
+ if (tooltip) {
+ const testId = props["data-testid"];
+
+ return (
+ <>
+
+
+ {tooltip.children || tooltip}
+
+ >
+ );
+ }
+
+ return ;
+ },
+}));
+
+describe("cluster/namespaces - edit namespace from new tab", () => {
+ let builder: ApplicationBuilder;
+ let callForNamespaceMock: AsyncFnMock;
+ let callForPatchNamespaceMock: AsyncFnMock;
+ let showSuccessNotificationMock: jest.Mock;
+ let showErrorNotificationMock: jest.Mock;
+ let storagesAreReady: () => Promise;
+
+ beforeEach(() => {
+ builder = getApplicationBuilder();
+
+ builder.setEnvironmentToClusterFrame();
+
+ callForNamespaceMock = asyncFn();
+ callForPatchNamespaceMock = asyncFn();
+
+ showSuccessNotificationMock = jest.fn();
+ showErrorNotificationMock = jest.fn();
+
+ builder.beforeApplicationStart(({ rendererDi }) => {
+ rendererDi.override(
+ directoryForLensLocalStorageInjectable,
+ () => "/some-directory-for-lens-local-storage",
+ );
+
+ rendererDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
+
+ storagesAreReady = controlWhenStoragesAreReady(rendererDi);
+
+ rendererDi.override(
+ showSuccessNotificationInjectable,
+ () => showSuccessNotificationMock,
+ );
+
+ rendererDi.override(
+ showErrorNotificationInjectable,
+ () => showErrorNotificationMock,
+ );
+
+ rendererDi.override(getRandomIdForEditResourceTabInjectable, () =>
+ jest
+ .fn(() => "some-irrelevant-random-id")
+ .mockReturnValueOnce("some-first-tab-id")
+ .mockReturnValueOnce("some-second-tab-id"),
+ );
+
+ rendererDi.override(callForResourceInjectable, () => async (selfLink: string) => {
+ if (
+ [
+ "/apis/some-api-version/namespaces/some-uid",
+ "/apis/some-api-version/namespaces/some-other-uid",
+ ].includes(selfLink)
+ ) {
+ return await callForNamespaceMock(selfLink);
+ }
+
+ return undefined;
+ });
+
+ rendererDi.override(callForPatchResourceInjectable, () => async (namespace, ...args) => {
+ if (
+ [
+ "/apis/some-api-version/namespaces/some-uid",
+ "/apis/some-api-version/namespaces/some-other-uid",
+ ].includes(namespace.selfLink)
+ ) {
+ return await callForPatchNamespaceMock(namespace, ...args);
+ }
+
+ return undefined;
+ });
+ });
+
+ builder.allowKubeResource("namespaces");
+ });
+
+ describe("when navigating to namespaces", () => {
+ let rendered: RenderResult;
+ let rendererDi: DiContainer;
+
+ beforeEach(async () => {
+ rendered = await builder.render();
+
+ await storagesAreReady();
+
+ rendererDi = builder.dis.rendererDi;
+
+ const navigateToNamespaces = rendererDi.inject(
+ navigateToNamespacesInjectable,
+ );
+
+ navigateToNamespaces();
+
+ const dockStore = rendererDi.inject(dockStoreInjectable);
+
+ // TODO: Make TerminalWindow unit testable to allow realistic behaviour
+ dockStore.closeTab("terminal");
+ });
+
+ // TODO: Implement skipped tests when loading of resources can be tested
+ xit("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ xit("calls for namespaces", () => {
+
+ });
+
+ xit("shows spinner", () => {
+
+ });
+
+ describe("when namespaces resolve", () => {
+ beforeEach(() => {
+
+ });
+
+ xit("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ xit("does not show spinner anymore", () => {
+
+ });
+
+ describe("when clicking the context menu for a namespace", () => {
+ beforeEach(() => {
+
+ });
+
+ xit("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ xit("does not show edit resource tab yet", () => {
+
+ });
+
+ describe("when clicking to edit namespace", () => {
+ beforeEach(() => {
+ // TODO: Make implementation match the description (tests above)
+ const namespaceStub = new Namespace(someNamespaceDataStub);
+
+ const createEditResourceTab = rendererDi.inject(createEditResourceTabInjectable);
+
+ createEditResourceTab(namespaceStub);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows dock tab for editing namespace", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("shows spinner in the dock tab", () => {
+ expect(
+ rendered.getByTestId("edit-resource-tab-spinner"),
+ ).toBeInTheDocument();
+ });
+
+ it("calls for namespace", () => {
+ expect(callForNamespaceMock).toHaveBeenCalledWith(
+ "/apis/some-api-version/namespaces/some-uid",
+ );
+ });
+
+ describe("when call for namespace resolves with namespace", () => {
+ let someNamespace: Namespace;
+
+ beforeEach(async () => {
+ someNamespace = new Namespace({
+ apiVersion: "some-api-version",
+ kind: "Namespace",
+
+ metadata: {
+ uid: "some-uid",
+ name: "some-name",
+ resourceVersion: "some-resource-version",
+ selfLink: "/apis/some-api-version/namespaces/some-uid",
+ somePropertyToBeRemoved: "some-value",
+ somePropertyToBeChanged: "some-old-value",
+ },
+ });
+
+ await callForNamespaceMock.resolve({
+ callWasSuccessful: true,
+ response: someNamespace,
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show spinner anymore", () => {
+ expect(
+ rendered.queryByTestId("edit-resource-tab-spinner"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("has the configuration in editor", () => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ ) as HTMLTextAreaElement;
+
+ expect(input.value).toBe(`apiVersion: some-api-version
+kind: Namespace
+metadata:
+ uid: some-uid
+ name: some-name
+ resourceVersion: some-resource-version
+ selfLink: /apis/some-api-version/namespaces/some-uid
+ somePropertyToBeRemoved: some-value
+ somePropertyToBeChanged: some-old-value
+`);
+ });
+
+ describe("given no changes in the configuration, when selecting to save", () => {
+ beforeEach(() => {
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(saveButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("calls for save with empty values", () => {
+ expect(callForPatchNamespaceMock).toHaveBeenCalledWith(
+ someNamespace,
+ [],
+ );
+ });
+
+ it("shows spinner", () => {
+ expect(
+ rendered.getByTestId("saving-edit-resource-from-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("save button is disabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).toHaveAttribute("disabled");
+ });
+
+ it("save and close button is disabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-and-close-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).toHaveAttribute("disabled");
+ });
+
+ describe("when saving resolves with success", () => {
+ beforeEach(async () => {
+ await callForPatchNamespaceMock.resolve({
+ callWasSuccessful: true,
+ response: { name: "some-name", kind: "Namespace" },
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show spinner anymore", () => {
+ expect(
+ rendered.queryByTestId("saving-edit-resource-from-tab-for-some-first-tab-id"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("save button is enabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).not.toHaveAttribute("disabled");
+ });
+
+ it("save and close button is enabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-and-close-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).not.toHaveAttribute("disabled");
+ });
+
+ it("shows success notification", () => {
+ expect(showSuccessNotificationMock).toHaveBeenCalled();
+ });
+
+ it("does not show error notification", () => {
+ expect(showErrorNotificationMock).not.toHaveBeenCalled();
+ });
+
+ it("does not close the dock tab", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("when saving resolves with failure", () => {
+ beforeEach(async () => {
+ await callForPatchNamespaceMock.resolve({
+ callWasSuccessful: false,
+ error: "some-error",
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show spinner anymore", () => {
+ expect(
+ rendered.queryByTestId("edit-resource-tab-spinner"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("save button is enabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).not.toHaveAttribute("disabled");
+ });
+
+ it("save and close button is enabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-and-close-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).not.toHaveAttribute("disabled");
+ });
+
+ it("does not show success notification", () => {
+ expect(showSuccessNotificationMock).not.toHaveBeenCalled();
+ });
+
+ it("shows error notification", () => {
+ expect(showErrorNotificationMock).toHaveBeenCalled();
+ });
+
+ it("does not close the dock tab", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("when selecting to save and close", () => {
+ beforeEach(() => {
+ const saveButton = rendered.getByTestId(
+ "save-and-close-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(saveButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not close the tab yet", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ describe("when saving resolves with success", () => {
+ beforeEach(async () => {
+ await callForPatchNamespaceMock.resolve({
+ callWasSuccessful: true,
+ response: { name: "some-name", kind: "Namespace" },
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("closes the dock tab", () => {
+ expect(
+ rendered.queryByTestId("dock-tab-for-some-first-tab-id"),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("when saving resolves with failure", () => {
+ beforeEach(async () => {
+ await callForPatchNamespaceMock.resolve({
+ callWasSuccessful: false,
+ error: "Some error",
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ // TODO: Not doable at the moment because info panel controls closing of the tab
+ xit("does not close the dock tab", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("when selecting to cancel", () => {
+ beforeEach(() => {
+ const cancelButton = rendered.getByTestId(
+ "cancel-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(cancelButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not have dock tab anymore", () => {
+ expect(
+ rendered.queryByTestId("dock-tab-for-some-first-tab-id"),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given change in configuration", () => {
+ beforeEach(() => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ ) as HTMLInputElement;
+
+ fireEvent.change(input, {
+ target: {
+ value: `apiVersion: some-api-version
+kind: Namespace
+metadata:
+ uid: some-uid
+ name: some-name
+ resourceVersion: some-resource-version
+ selfLink: /apis/some-api-version/namespaces/some-uid
+ somePropertyToBeChanged: some-changed-value
+ someAddedProperty: some-new-value
+`,
+ },
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("has the changed configuration in editor", () => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ ) as HTMLTextAreaElement;
+
+ expect(input.value).toBe(`apiVersion: some-api-version
+kind: Namespace
+metadata:
+ uid: some-uid
+ name: some-name
+ resourceVersion: some-resource-version
+ selfLink: /apis/some-api-version/namespaces/some-uid
+ somePropertyToBeChanged: some-changed-value
+ someAddedProperty: some-new-value
+`);
+ });
+
+ it("stores the changed configuration", async () => {
+ const readJsonFile = rendererDi.inject(
+ readJsonFileInjectable,
+ );
+
+ const actual = (await readJsonFile(
+ "/some-directory-for-lens-local-storage/some-cluster-id.json",
+ )) as any;
+
+ expect(
+ actual.edit_resource_store["some-first-tab-id"],
+ ).toEqual({
+ resource: "/apis/some-api-version/namespaces/some-uid",
+ firstDraft: `apiVersion: some-api-version
+kind: Namespace
+metadata:
+ uid: some-uid
+ name: some-name
+ resourceVersion: some-resource-version
+ selfLink: /apis/some-api-version/namespaces/some-uid
+ somePropertyToBeRemoved: some-value
+ somePropertyToBeChanged: some-old-value
+`,
+ draft: `apiVersion: some-api-version
+kind: Namespace
+metadata:
+ uid: some-uid
+ name: some-name
+ resourceVersion: some-resource-version
+ selfLink: /apis/some-api-version/namespaces/some-uid
+ somePropertyToBeChanged: some-changed-value
+ someAddedProperty: some-new-value
+`,
+ });
+ });
+
+ describe("when selecting to save", () => {
+ beforeEach(() => {
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(saveButton);
+ });
+
+ it("calls for save with changed configuration", () => {
+ expect(callForPatchNamespaceMock).toHaveBeenCalledWith(
+ someNamespace,
+ [
+ {
+ op: "remove",
+ path: "/metadata/somePropertyToBeRemoved",
+ },
+ {
+ op: "add",
+ path: "/metadata/someAddedProperty",
+ value: "some-new-value",
+ },
+ {
+ op: "replace",
+ path: "/metadata/somePropertyToBeChanged",
+ value: "some-changed-value",
+ },
+ ],
+ );
+ });
+
+ it("given save resolves and another change in configuration, when saving, calls for save with changed configuration", async () => {
+ await callForPatchNamespaceMock.resolve({
+ callWasSuccessful: true,
+
+ response: {
+ name: "some-name",
+ kind: "Namespace",
+ },
+ });
+
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ ) as HTMLInputElement;
+
+ fireEvent.change(input, {
+ target: {
+ value: `apiVersion: some-api-version
+kind: Namespace
+metadata:
+ uid: some-uid
+ name: some-name
+ resourceVersion: some-resource-version
+ selfLink: /apis/some-api-version/namespaces/some-uid
+ somePropertyToBeChanged: some-changed-value
+ someAddedProperty: some-new-value
+ someOtherAddedProperty: some-other-new-value
+`,
+ },
+ });
+
+
+ callForPatchNamespaceMock.mockClear();
+
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(saveButton);
+
+ expect(callForPatchNamespaceMock).toHaveBeenCalledWith(
+ someNamespace,
+ [
+ {
+ op: "add",
+ path: "/metadata/someOtherAddedProperty",
+ value: "some-other-new-value",
+ },
+ ],
+ );
+ });
+ });
+ });
+
+ describe("given invalid change in configuration", () => {
+ beforeEach(() => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ ) as HTMLInputElement;
+
+ fireEvent.change(input, {
+ target: {
+ value: "@some-invalid-configuration@",
+ },
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("has the changed configuration in editor", () => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ ) as HTMLTextAreaElement;
+
+ expect(input.value).toBe(`@some-invalid-configuration@`);
+ });
+
+ it("save button is disabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).toHaveAttribute("disabled");
+ });
+
+ it("save and close button is disabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-and-close-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).toHaveAttribute("disabled");
+ });
+
+ describe("when valid change in configuration", () => {
+
+ beforeEach(() => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ ) as HTMLInputElement;
+
+
+ fireEvent.change(input, {
+ target: {
+ value: `apiVersion: some-api-version
+kind: Namespace
+metadata:
+ uid: some-uid
+ name: some-name
+ resourceVersion: some-resource-version
+ selfLink: /apis/some-api-version/namespaces/some-uid
+`,
+ },
+ });
+
+ });
+
+ it("save button is enabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).not.toHaveAttribute("disabled");
+ });
+
+ it("save and close button is enabled", () => {
+ const saveButton = rendered.getByTestId(
+ "save-and-close-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ expect(saveButton).not.toHaveAttribute("disabled");
+ });
+ });
+
+ });
+
+ describe("given clicking the context menu for second namespace, when clicking to edit namespace", () => {
+ beforeEach(() => {
+ callForNamespaceMock.mockClear();
+
+ // TODO: Make implementation match the description
+ const namespaceStub = new Namespace(someOtherNamespaceDataStub);
+
+ const createEditResourceTab = rendererDi.inject(createEditResourceTabInjectable);
+
+ createEditResourceTab(namespaceStub);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows dock tab for editing second namespace", () => {
+ expect(
+ rendered.getByTestId("dock-tab-content-for-some-second-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("still has dock tab for first namespace", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("shows spinner in the dock tab", () => {
+ expect(
+ rendered.getByTestId("edit-resource-tab-spinner"),
+ ).toBeInTheDocument();
+ });
+
+ it("calls for second namespace", () => {
+ expect(callForNamespaceMock).toHaveBeenCalledWith(
+ "/apis/some-api-version/namespaces/some-other-uid",
+ );
+ });
+
+ describe("when second namespace resolves", () => {
+ let someOtherNamespace: Namespace;
+
+ beforeEach(async () => {
+ someOtherNamespace = new Namespace({
+ apiVersion: "some-api-version",
+ kind: "Namespace",
+
+ metadata: {
+ uid: "some-other-uid",
+ name: "some-other-name",
+ resourceVersion: "some-resource-version",
+ selfLink:
+ "/apis/some-api-version/namespaces/some-other-uid",
+ },
+ });
+
+ await callForNamespaceMock.resolve({
+ callWasSuccessful: true,
+ response: someOtherNamespace,
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("has the configuration in editor", () => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-second-tab-id",
+ ) as HTMLTextAreaElement;
+
+ expect(input.value).toBe(`apiVersion: some-api-version
+kind: Namespace
+metadata:
+ uid: some-other-uid
+ name: some-other-name
+ resourceVersion: some-resource-version
+ selfLink: /apis/some-api-version/namespaces/some-other-uid
+`);
+ });
+
+ it("when selecting to save, calls for save of second namespace", () => {
+ callForPatchNamespaceMock.mockClear();
+
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-second-tab-id",
+ );
+
+ fireEvent.click(saveButton);
+
+ expect(callForPatchNamespaceMock).toHaveBeenCalledWith(
+ someOtherNamespace,
+ [],
+ );
+ });
+
+ describe("when clicking dock tab for the first namespace", () => {
+ beforeEach(() => {
+ callForNamespaceMock.mockClear();
+
+ const tab = rendered.getByTestId("dock-tab-for-some-first-tab-id");
+
+ fireEvent.click(tab);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows dock tab for editing first namespace", () => {
+ expect(
+ rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("still has dock tab for second namespace", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-second-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("does not show spinner in the dock tab", () => {
+ expect(
+ rendered.queryByTestId("edit-resource-tab-spinner"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("does not call for namespace", () => {
+ expect(callForNamespaceMock).not.toHaveBeenCalled();
+ });
+
+ it("has configuration in the editor", () => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ ) as HTMLTextAreaElement;
+
+ expect(input.value).toBe(`apiVersion: some-api-version
+kind: Namespace
+metadata:
+ uid: some-uid
+ name: some-name
+ resourceVersion: some-resource-version
+ selfLink: /apis/some-api-version/namespaces/some-uid
+ somePropertyToBeRemoved: some-value
+ somePropertyToBeChanged: some-old-value
+`);
+ });
+
+ it("when selecting to save, calls for save of first namespace", () => {
+ callForPatchNamespaceMock.mockClear();
+
+ const saveButton = rendered.getByTestId(
+ "save-edit-resource-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(saveButton);
+
+ expect(callForPatchNamespaceMock).toHaveBeenCalledWith(
+ someNamespace,
+ [],
+ );
+ });
+ });
+ });
+ });
+ });
+
+ describe("when call for namespace resolves without namespace", () => {
+ beforeEach(async () => {
+ await callForNamespaceMock.resolve({
+ callWasSuccessful: true,
+ response: undefined,
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("still shows the dock tab for editing namespace", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("shows error message", () => {
+ expect(
+ rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
+ ).toHaveTextContent("Resource not found");
+ });
+
+ it("does not show error notification", () => {
+ expect(showErrorNotificationMock).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("when call for namespace resolves with failure", () => {
+ beforeEach(async () => {
+ await callForNamespaceMock.resolve({
+ callWasSuccessful: false,
+ error: "some-error",
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("still shows the dock tab for editing namespace", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("shows error message", () => {
+ expect(
+ rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
+ ).toHaveTextContent("Resource not found");
+ });
+
+ it("shows error notification", () => {
+ expect(showErrorNotificationMock).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+const someNamespaceDataStub = {
+ apiVersion: "some-api-version",
+ kind: "Namespace",
+ metadata: {
+ uid: "some-uid",
+ name: "some-name",
+ resourceVersion: "some-resource-version",
+ selfLink: "/apis/some-api-version/namespaces/some-uid",
+ },
+};
+
+const someOtherNamespaceDataStub = {
+ apiVersion: "some-api-version",
+ kind: "Namespace",
+ metadata: {
+ uid: "some-other-uid",
+ name: "some-other-name",
+ resourceVersion: "some-resource-version",
+ selfLink: "/apis/some-api-version/namespaces/some-other-uid",
+ },
+};
diff --git a/src/behaviours/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx b/src/behaviours/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx
new file mode 100644
index 0000000000..c5e7287988
--- /dev/null
+++ b/src/behaviours/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx
@@ -0,0 +1,172 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { RenderResult } from "@testing-library/react";
+import { act } from "@testing-library/react";
+import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
+import React from "react";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
+import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
+import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
+import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
+import { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready";
+import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
+import { TabKind } from "../../../renderer/components/dock/dock/store";
+import { Namespace } from "../../../common/k8s-api/endpoints";
+
+jest.mock("../../../renderer/components/tooltip/withTooltip", () => ({
+ withTooltip:
+ (Target: any) =>
+ ({ tooltip, ...props }: any) => {
+ if (tooltip) {
+ const testId = props["data-testid"];
+
+ return (
+ <>
+
+
+ {tooltip.children || tooltip}
+
+ >
+ );
+ }
+
+ return ;
+ },
+}));
+
+describe("cluster/namespaces - edit namespaces from previously opened tab", () => {
+ let builder: ApplicationBuilder;
+ let callForNamespaceMock: AsyncFnMock;
+ let storagesAreReady: () => Promise;
+
+ beforeEach(() => {
+ builder = getApplicationBuilder();
+
+ builder.setEnvironmentToClusterFrame();
+
+ callForNamespaceMock = asyncFn();
+
+ builder.beforeApplicationStart(({ rendererDi }) => {
+ rendererDi.override(
+ directoryForLensLocalStorageInjectable,
+ () => "/some-directory-for-lens-local-storage",
+ );
+
+ rendererDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
+
+ storagesAreReady = controlWhenStoragesAreReady(rendererDi);
+
+ rendererDi.override(callForResourceInjectable, () => callForNamespaceMock);
+ });
+
+ builder.allowKubeResource("namespaces");
+ });
+
+ describe("given tab was previously opened, when application is started", () => {
+ let rendered: RenderResult;
+
+ beforeEach(async () => {
+ const writeJsonFile = builder.dis.rendererDi.inject(writeJsonFileInjectable);
+
+ await writeJsonFile(
+ "/some-directory-for-lens-local-storage/some-cluster-id.json",
+ {
+ dock: {
+ height: 300,
+ tabs: [
+ {
+ id: "some-first-tab-id",
+ kind: TabKind.EDIT_RESOURCE,
+ title: "Namespace: some-namespace",
+ pinned: false,
+ },
+ ],
+
+ isOpen: true,
+ },
+
+ edit_resource_store: {
+ "some-first-tab-id": {
+ resource: "/apis/some-api-version/namespaces/some-uid",
+ draft: "some-saved-configuration",
+ },
+ },
+ },
+ );
+
+ rendered = await builder.render();
+
+ await storagesAreReady();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows dock tab for editing namespace", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("shows spinner in the dock tab", () => {
+ expect(
+ rendered.getByTestId("edit-resource-tab-spinner"),
+ ).toBeInTheDocument();
+ });
+
+ it("calls for namespace", () => {
+ expect(callForNamespaceMock).toHaveBeenCalledWith(
+ "/apis/some-api-version/namespaces/some-uid",
+ );
+ });
+
+ describe("when call for namespace resolves with namespace", () => {
+ let someNamespace: Namespace;
+
+ beforeEach(async () => {
+ someNamespace = new Namespace({
+ apiVersion: "some-api-version",
+ kind: "Namespace",
+
+ metadata: {
+ uid: "some-uid",
+ name: "some-name",
+ resourceVersion: "some-resource-version",
+ selfLink: "/apis/some-api-version/namespaces/some-uid",
+ somePropertyToBeRemoved: "some-value",
+ somePropertyToBeChanged: "some-old-value",
+ },
+ });
+
+ // TODO: Figure out why act is needed here. In CI it works without it.
+ await act(async () => {
+ await callForNamespaceMock.resolve({
+ callWasSuccessful: true,
+ response: someNamespace,
+ });
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("has the saved configuration in editor", () => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ ) as HTMLTextAreaElement;
+
+ expect(input.value).toBe("some-saved-configuration");
+ });
+ });
+ });
+});
diff --git a/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap b/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap
index eb02f57fc8..745c657614 100644
--- a/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap
+++ b/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap
@@ -2644,10 +2644,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-default-configuration
+
@@ -3651,10 +3652,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
class="Spinner singleColor center"
data-testid="install-chart-configuration-spinner"
/>
-
+ >
+ some-default-configuration
+
@@ -4624,10 +4626,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-default-configuration-for-other-version
+
@@ -5597,10 +5600,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-default-configuration
+
@@ -6590,10 +6594,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ @some-invalid-configuration@
+
@@ -7565,10 +7570,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-default-configuration
+
@@ -8567,10 +8573,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-default-configuration
+
@@ -9548,10 +9555,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-default-configuration
+
@@ -13030,10 +13038,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-default-configuration
+
@@ -15102,10 +15111,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-other-default-configuration
+
@@ -16122,10 +16132,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-default-configuration
+
@@ -17095,10 +17106,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-valid-configuration
+
@@ -18068,10 +18080,11 @@ exports[`installing helm chart from new tab given tab for installing chart was n
Install
-
+ >
+ some-default-configuration
+
diff --git a/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-previously-opened-tab.test.ts.snap b/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-previously-opened-tab.test.ts.snap
index b0e67015f3..8c5475a888 100644
--- a/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-previously-opened-tab.test.ts.snap
+++ b/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-previously-opened-tab.test.ts.snap
@@ -1243,10 +1243,11 @@ exports[`installing helm chart from previously opened tab given tab for installi
Install
-
+ >
+ some-stored-configuration
+
diff --git a/src/behaviours/helm-releases/__snapshots__/showing-details-for-helm-release.test.ts.snap b/src/behaviours/helm-releases/__snapshots__/showing-details-for-helm-release.test.ts.snap
index 3c36512476..ba191595b2 100644
--- a/src/behaviours/helm-releases/__snapshots__/showing-details-for-helm-release.test.ts.snap
+++ b/src/behaviours/helm-releases/__snapshots__/showing-details-for-helm-release.test.ts.snap
@@ -7336,10 +7336,11 @@ exports[`showing details for helm release given application is started when navi
User-supplied values only
-
+ >
+ some-configuration
+
-
+ >
+ some-new-configuration
+
-
+ >
+ some-new-configuration
+
-
+ >
+ some-new-configuration
+
-
+ >
+ some-new-configuration
+
-
+ >
+ some-other-configuration
+
(kind: string): boolean => {
+ if ((kind as KubeResource) in apiResourceRecord) {
+ return allowedResources.includes(kind);
+ }
+
+ const apiResource = apiResources.find(resource => resource.kind === kind);
+
+ if (apiResource) {
+ return allowedResources.includes(apiResource.apiName);
+ }
+
+ return true; // allowed by default for other resources
+};
diff --git a/src/common/k8s-api/endpoints/cluster-role-binding.api.injectable.ts b/src/common/k8s-api/endpoints/cluster-role-binding.api.injectable.ts
index c06f3baaff..38ba76d661 100644
--- a/src/common/k8s-api/endpoints/cluster-role-binding.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/cluster-role-binding.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { ClusterRoleBindingApi } from "./cluster-role-binding.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const clusterRoleBindingApiInjectable = getInjectable({
id: "cluster-role-binding-api",
@@ -14,6 +15,8 @@ const clusterRoleBindingApiInjectable = getInjectable({
return new ClusterRoleBindingApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default clusterRoleBindingApiInjectable;
diff --git a/src/common/k8s-api/endpoints/cluster-role.api.injectable.ts b/src/common/k8s-api/endpoints/cluster-role.api.injectable.ts
index 19a8f83542..9b56bc2559 100644
--- a/src/common/k8s-api/endpoints/cluster-role.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/cluster-role.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { ClusterRoleApi } from "./cluster-role.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const clusterRoleApiInjectable = getInjectable({
id: "cluster-role-api",
@@ -14,6 +15,8 @@ const clusterRoleApiInjectable = getInjectable({
return new ClusterRoleApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default clusterRoleApiInjectable;
diff --git a/src/common/k8s-api/endpoints/cluster.api.injectable.ts b/src/common/k8s-api/endpoints/cluster.api.injectable.ts
index fd82ae93f3..8d51e630ee 100644
--- a/src/common/k8s-api/endpoints/cluster.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/cluster.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { ClusterApi } from "./cluster.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const clusterApiInjectable = getInjectable({
id: "cluster-api",
@@ -14,6 +15,8 @@ const clusterApiInjectable = getInjectable({
return new ClusterApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default clusterApiInjectable;
diff --git a/src/common/k8s-api/endpoints/component-status.api.injectable.ts b/src/common/k8s-api/endpoints/component-status.api.injectable.ts
index fbe4b7f176..395c825401 100644
--- a/src/common/k8s-api/endpoints/component-status.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/component-status.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { ComponentStatusApi } from "./component-status.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const componentStatusApiInjectable = getInjectable({
id: "component-status-api",
@@ -14,6 +15,8 @@ const componentStatusApiInjectable = getInjectable({
return new ComponentStatusApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default componentStatusApiInjectable;
diff --git a/src/common/k8s-api/endpoints/config-map.api.injectable.ts b/src/common/k8s-api/endpoints/config-map.api.injectable.ts
index 715e1ba728..d643849f70 100644
--- a/src/common/k8s-api/endpoints/config-map.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/config-map.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { ConfigMapApi } from "./config-map.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const configMapApiInjectable = getInjectable({
id: "config-map-api",
@@ -14,6 +15,8 @@ const configMapApiInjectable = getInjectable({
return new ConfigMapApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default configMapApiInjectable;
diff --git a/src/common/k8s-api/endpoints/cron-job.api.injectable.ts b/src/common/k8s-api/endpoints/cron-job.api.injectable.ts
index 9390d5d4c8..a22909861e 100644
--- a/src/common/k8s-api/endpoints/cron-job.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/cron-job.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { CronJobApi } from "./cron-job.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const cronJobApiInjectable = getInjectable({
id: "cron-job-api",
@@ -14,6 +15,8 @@ const cronJobApiInjectable = getInjectable({
return new CronJobApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default cronJobApiInjectable;
diff --git a/src/common/k8s-api/endpoints/custom-resource-definition.api.injectable.ts b/src/common/k8s-api/endpoints/custom-resource-definition.api.injectable.ts
index abe728850c..c441b07c3e 100644
--- a/src/common/k8s-api/endpoints/custom-resource-definition.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/custom-resource-definition.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { CustomResourceDefinitionApi } from "./custom-resource-definition.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const customResourceDefinitionApiInjectable = getInjectable({
id: "custom-resource-definition-api",
@@ -14,6 +15,8 @@ const customResourceDefinitionApiInjectable = getInjectable({
return new CustomResourceDefinitionApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default customResourceDefinitionApiInjectable;
diff --git a/src/common/k8s-api/endpoints/daemon-set.api.injectable.ts b/src/common/k8s-api/endpoints/daemon-set.api.injectable.ts
index 346742c996..a1e1d65084 100644
--- a/src/common/k8s-api/endpoints/daemon-set.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/daemon-set.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { DaemonSetApi } from "./daemon-set.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const daemonSetApiInjectable = getInjectable({
id: "daemon-set-api",
@@ -14,6 +15,8 @@ const daemonSetApiInjectable = getInjectable({
return new DaemonSetApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default daemonSetApiInjectable;
diff --git a/src/common/k8s-api/endpoints/deployment.api.injectable.ts b/src/common/k8s-api/endpoints/deployment.api.injectable.ts
index 5b94d949aa..26c98c39e6 100644
--- a/src/common/k8s-api/endpoints/deployment.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/deployment.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { DeploymentApi } from "./deployment.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const deploymentApiInjectable = getInjectable({
id: "deployment-api",
@@ -14,6 +15,8 @@ const deploymentApiInjectable = getInjectable({
return new DeploymentApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default deploymentApiInjectable;
diff --git a/src/common/k8s-api/endpoints/endpoint.api.injectable.ts b/src/common/k8s-api/endpoints/endpoint.api.injectable.ts
index 44a6595b6c..79a57cd3e7 100644
--- a/src/common/k8s-api/endpoints/endpoint.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/endpoint.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { EndpointsApi } from "./endpoint.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const endpointsApiInjectable = getInjectable({
id: "endpoints-api",
@@ -14,6 +15,8 @@ const endpointsApiInjectable = getInjectable({
return new EndpointsApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default endpointsApiInjectable;
diff --git a/src/common/k8s-api/endpoints/events.api.injectable.ts b/src/common/k8s-api/endpoints/events.api.injectable.ts
index c4fbe81224..83f0697d31 100644
--- a/src/common/k8s-api/endpoints/events.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/events.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { KubeEventApi } from "./events.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const kubeEventApiInjectable = getInjectable({
id: "kube-event-api",
@@ -14,6 +15,8 @@ const kubeEventApiInjectable = getInjectable({
return new KubeEventApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default kubeEventApiInjectable;
diff --git a/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.injectable.ts b/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.injectable.ts
index 70b6458456..7449053661 100644
--- a/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/horizontal-pod-autoscaler.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { HorizontalPodAutoscalerApi } from "./horizontal-pod-autoscaler.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const horizontalPodAutoscalerApiInjectable = getInjectable({
id: "horizontal-pod-autoscaler-api",
@@ -14,6 +15,8 @@ const horizontalPodAutoscalerApiInjectable = getInjectable({
return new HorizontalPodAutoscalerApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default horizontalPodAutoscalerApiInjectable;
diff --git a/src/common/k8s-api/endpoints/ingress.api.injectable.ts b/src/common/k8s-api/endpoints/ingress.api.injectable.ts
index ca280508b2..350f8824e4 100644
--- a/src/common/k8s-api/endpoints/ingress.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/ingress.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { IngressApi } from "./ingress.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const ingressApiInjectable = getInjectable({
id: "ingress-api",
@@ -14,6 +15,8 @@ const ingressApiInjectable = getInjectable({
return new IngressApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default ingressApiInjectable;
diff --git a/src/common/k8s-api/endpoints/job.api.injectable.ts b/src/common/k8s-api/endpoints/job.api.injectable.ts
index c0bbe9161e..a9c4252e59 100644
--- a/src/common/k8s-api/endpoints/job.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/job.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { JobApi } from "./job.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const jobApiInjectable = getInjectable({
id: "job-api",
@@ -14,6 +15,8 @@ const jobApiInjectable = getInjectable({
return new JobApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default jobApiInjectable;
diff --git a/src/common/k8s-api/endpoints/limit-range.api.injectable.ts b/src/common/k8s-api/endpoints/limit-range.api.injectable.ts
index 1842fdf33e..c2898c998a 100644
--- a/src/common/k8s-api/endpoints/limit-range.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/limit-range.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { LimitRangeApi } from "./limit-range.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const limitRangeApiInjectable = getInjectable({
id: "limit-range-api",
@@ -14,6 +15,8 @@ const limitRangeApiInjectable = getInjectable({
return new LimitRangeApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default limitRangeApiInjectable;
diff --git a/src/common/k8s-api/endpoints/namespace.api.injectable.ts b/src/common/k8s-api/endpoints/namespace.api.injectable.ts
index 0ff259f58d..c55c6b9521 100644
--- a/src/common/k8s-api/endpoints/namespace.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/namespace.api.injectable.ts
@@ -6,14 +6,18 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { NamespaceApi } from "./namespace.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const namespaceApiInjectable = getInjectable({
id: "namespace-api",
+
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "namespaceApi is only available in certain environments");
return new NamespaceApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default namespaceApiInjectable;
diff --git a/src/common/k8s-api/endpoints/network-policy.api.injectable.ts b/src/common/k8s-api/endpoints/network-policy.api.injectable.ts
index e351451f53..58a316605a 100644
--- a/src/common/k8s-api/endpoints/network-policy.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/network-policy.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { NetworkPolicyApi } from "./network-policy.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const networkPolicyApiInjectable = getInjectable({
id: "network-policy-api",
@@ -14,6 +15,8 @@ const networkPolicyApiInjectable = getInjectable({
return new NetworkPolicyApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default networkPolicyApiInjectable;
diff --git a/src/common/k8s-api/endpoints/node.api.injectable.ts b/src/common/k8s-api/endpoints/node.api.injectable.ts
index d382e9a7ba..4a0faf3270 100644
--- a/src/common/k8s-api/endpoints/node.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/node.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { NodeApi } from "./node.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const nodeApiInjectable = getInjectable({
id: "node-api",
@@ -14,6 +15,8 @@ const nodeApiInjectable = getInjectable({
return new NodeApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default nodeApiInjectable;
diff --git a/src/common/k8s-api/endpoints/persistent-volume-claim.api.injectable.ts b/src/common/k8s-api/endpoints/persistent-volume-claim.api.injectable.ts
index fcd539b429..e765cf0c38 100644
--- a/src/common/k8s-api/endpoints/persistent-volume-claim.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/persistent-volume-claim.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { PersistentVolumeClaimApi } from "./persistent-volume-claim.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const persistentVolumeClaimApiInjectable = getInjectable({
id: "persistent-volume-claim-api",
@@ -14,6 +15,8 @@ const persistentVolumeClaimApiInjectable = getInjectable({
return new PersistentVolumeClaimApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default persistentVolumeClaimApiInjectable;
diff --git a/src/common/k8s-api/endpoints/persistent-volume.api.injectable.ts b/src/common/k8s-api/endpoints/persistent-volume.api.injectable.ts
index 7627b9d3c3..38ad80845c 100644
--- a/src/common/k8s-api/endpoints/persistent-volume.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/persistent-volume.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { PersistentVolumeApi } from "./persistent-volume.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const persistentVolumeApiInjectable = getInjectable({
id: "persistent-volume-api",
@@ -14,6 +15,8 @@ const persistentVolumeApiInjectable = getInjectable({
return new PersistentVolumeApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default persistentVolumeApiInjectable;
diff --git a/src/common/k8s-api/endpoints/pod-disruption-budget.api.injectable.ts b/src/common/k8s-api/endpoints/pod-disruption-budget.api.injectable.ts
index afc8190eb3..c2e793433b 100644
--- a/src/common/k8s-api/endpoints/pod-disruption-budget.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/pod-disruption-budget.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { PodDisruptionBudgetApi } from "./pod-disruption-budget.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const podDisruptionBudgetApiInjectable = getInjectable({
id: "pod-disruption-budget-api",
@@ -14,6 +15,8 @@ const podDisruptionBudgetApiInjectable = getInjectable({
return new PodDisruptionBudgetApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default podDisruptionBudgetApiInjectable;
diff --git a/src/common/k8s-api/endpoints/pod-metrics.api.injectable.ts b/src/common/k8s-api/endpoints/pod-metrics.api.injectable.ts
index 6d5d8efaf6..ecdbfa920d 100644
--- a/src/common/k8s-api/endpoints/pod-metrics.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/pod-metrics.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { PodMetricsApi } from "./pod-metrics.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const podMetricsApiInjectable = getInjectable({
id: "pod-metrics-api",
@@ -14,6 +15,8 @@ const podMetricsApiInjectable = getInjectable({
return new PodMetricsApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default podMetricsApiInjectable;
diff --git a/src/common/k8s-api/endpoints/pod-security-policy.api.injectable.ts b/src/common/k8s-api/endpoints/pod-security-policy.api.injectable.ts
index 81b65660a9..44e25a54b1 100644
--- a/src/common/k8s-api/endpoints/pod-security-policy.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/pod-security-policy.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { PodSecurityPolicyApi } from "./pod-security-policy.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const podSecurityPolicyApiInjectable = getInjectable({
id: "pod-security-policy-api",
@@ -14,6 +15,8 @@ const podSecurityPolicyApiInjectable = getInjectable({
return new PodSecurityPolicyApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default podSecurityPolicyApiInjectable;
diff --git a/src/common/k8s-api/endpoints/pod.api.injectable.ts b/src/common/k8s-api/endpoints/pod.api.injectable.ts
index b92d213f77..f88a396bab 100644
--- a/src/common/k8s-api/endpoints/pod.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/pod.api.injectable.ts
@@ -6,14 +6,18 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { PodApi } from "./pod.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const podApiInjectable = getInjectable({
id: "pod-api",
+
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "podApi is only available in certain environments");
return new PodApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default podApiInjectable;
diff --git a/src/common/k8s-api/endpoints/priority-class.api.injectable.ts b/src/common/k8s-api/endpoints/priority-class.api.injectable.ts
index 4599839580..8aae454967 100644
--- a/src/common/k8s-api/endpoints/priority-class.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/priority-class.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { PriorityClassApi } from "./priority-class.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const priorityClassApiInjectable = getInjectable({
id: "priority-class-api",
@@ -14,6 +15,8 @@ const priorityClassApiInjectable = getInjectable({
return new PriorityClassApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default priorityClassApiInjectable;
diff --git a/src/common/k8s-api/endpoints/replica-set.api.injectable.ts b/src/common/k8s-api/endpoints/replica-set.api.injectable.ts
index 8831173403..2bda99b703 100644
--- a/src/common/k8s-api/endpoints/replica-set.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/replica-set.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { ReplicaSetApi } from "./replica-set.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const replicaSetApiInjectable = getInjectable({
id: "replica-set-api",
@@ -14,6 +15,8 @@ const replicaSetApiInjectable = getInjectable({
return new ReplicaSetApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default replicaSetApiInjectable;
diff --git a/src/common/k8s-api/endpoints/resource-quota.api.injectable.ts b/src/common/k8s-api/endpoints/resource-quota.api.injectable.ts
index 6cedcba453..b10865fe47 100644
--- a/src/common/k8s-api/endpoints/resource-quota.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/resource-quota.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { ResourceQuotaApi } from "./resource-quota.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const resourceQuotaApiInjectable = getInjectable({
id: "resource-quota-api",
@@ -14,6 +15,8 @@ const resourceQuotaApiInjectable = getInjectable({
return new ResourceQuotaApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default resourceQuotaApiInjectable;
diff --git a/src/common/k8s-api/endpoints/role-binding.api.injectable.ts b/src/common/k8s-api/endpoints/role-binding.api.injectable.ts
index 8e0f04b92c..7b802f78ce 100644
--- a/src/common/k8s-api/endpoints/role-binding.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/role-binding.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { RoleBindingApi } from "./role-binding.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const roleBindingApiInjectable = getInjectable({
id: "role-binding-api",
@@ -14,6 +15,8 @@ const roleBindingApiInjectable = getInjectable({
return new RoleBindingApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default roleBindingApiInjectable;
diff --git a/src/common/k8s-api/endpoints/role.api.injectable.ts b/src/common/k8s-api/endpoints/role.api.injectable.ts
index 0b6949357b..d2b09f8bf7 100644
--- a/src/common/k8s-api/endpoints/role.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/role.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { RoleApi } from "./role.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const roleApiInjectable = getInjectable({
id: "role-api",
@@ -14,6 +15,8 @@ const roleApiInjectable = getInjectable({
return new RoleApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default roleApiInjectable;
diff --git a/src/common/k8s-api/endpoints/secret.api.injectable.ts b/src/common/k8s-api/endpoints/secret.api.injectable.ts
index 67ebd3d541..916fbe1e93 100644
--- a/src/common/k8s-api/endpoints/secret.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/secret.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { SecretApi } from "./secret.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const secretApiInjectable = getInjectable({
id: "secret-api",
@@ -14,6 +15,8 @@ const secretApiInjectable = getInjectable({
return new SecretApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default secretApiInjectable;
diff --git a/src/common/k8s-api/endpoints/self-subject-rules-reviews.api.injectable.ts b/src/common/k8s-api/endpoints/self-subject-rules-reviews.api.injectable.ts
index cc77399c02..b66a2b4fc9 100644
--- a/src/common/k8s-api/endpoints/self-subject-rules-reviews.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/self-subject-rules-reviews.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { SelfSubjectRulesReviewApi } from "./self-subject-rules-reviews.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const selfSubjectRulesReviewApiInjectable = getInjectable({
id: "self-subject-rules-review-api",
@@ -14,6 +15,8 @@ const selfSubjectRulesReviewApiInjectable = getInjectable({
return new SelfSubjectRulesReviewApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default selfSubjectRulesReviewApiInjectable;
diff --git a/src/common/k8s-api/endpoints/service-account.api.injectable.ts b/src/common/k8s-api/endpoints/service-account.api.injectable.ts
index 3b48b0eb98..da202d6ccd 100644
--- a/src/common/k8s-api/endpoints/service-account.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/service-account.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { ServiceAccountApi } from "./service-account.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const serviceAccountApiInjectable = getInjectable({
id: "service-account-api",
@@ -14,6 +15,8 @@ const serviceAccountApiInjectable = getInjectable({
return new ServiceAccountApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default serviceAccountApiInjectable;
diff --git a/src/common/k8s-api/endpoints/service.api.injectable.ts b/src/common/k8s-api/endpoints/service.api.injectable.ts
index db5f62b89e..31885238de 100644
--- a/src/common/k8s-api/endpoints/service.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/service.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { ServiceApi } from "./service.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const serviceApiInjectable = getInjectable({
id: "service-api",
@@ -14,6 +15,8 @@ const serviceApiInjectable = getInjectable({
return new ServiceApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default serviceApiInjectable;
diff --git a/src/common/k8s-api/endpoints/stateful-set.api.injectable.ts b/src/common/k8s-api/endpoints/stateful-set.api.injectable.ts
index a28dd5e967..fa206cf957 100644
--- a/src/common/k8s-api/endpoints/stateful-set.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/stateful-set.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { StatefulSetApi } from "./stateful-set.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const statefulSetApiInjectable = getInjectable({
id: "stateful-set-api",
@@ -14,6 +15,8 @@ const statefulSetApiInjectable = getInjectable({
return new StatefulSetApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default statefulSetApiInjectable;
diff --git a/src/common/k8s-api/endpoints/storage-class.api.injectable.ts b/src/common/k8s-api/endpoints/storage-class.api.injectable.ts
index 8594e231a0..7f1abab299 100644
--- a/src/common/k8s-api/endpoints/storage-class.api.injectable.ts
+++ b/src/common/k8s-api/endpoints/storage-class.api.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
import { StorageClassApi } from "./storage-class.api";
+import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
const storageClassApiInjectable = getInjectable({
id: "storage-class-api",
@@ -14,6 +15,8 @@ const storageClassApiInjectable = getInjectable({
return new StorageClassApi();
},
+
+ injectionToken: kubeApiInjectionToken,
});
export default storageClassApiInjectable;
diff --git a/src/common/k8s-api/kube-api/get-kube-api-from-path.injectable.ts b/src/common/k8s-api/kube-api/get-kube-api-from-path.injectable.ts
new file mode 100644
index 0000000000..669185684f
--- /dev/null
+++ b/src/common/k8s-api/kube-api/get-kube-api-from-path.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 { parseKubeApi } from "../kube-api-parse";
+import { kubeApiInjectionToken } from "./kube-api-injection-token";
+import type { KubeApi } from "../kube-api";
+
+const getKubeApiFromPathInjectable = getInjectable({
+ id: "get-kube-api-from-path",
+
+ instantiate: (di) => {
+ const kubeApis = di.injectMany(kubeApiInjectionToken);
+
+ return (apiPath: string) => {
+ const parsed = parseKubeApi(apiPath);
+
+ const kubeApi = kubeApis.find((api) => api.apiBase === parsed.apiBase);
+
+ return (kubeApi as KubeApi) || undefined;
+ };
+ },
+});
+
+export default getKubeApiFromPathInjectable;
diff --git a/src/common/k8s-api/kube-api/kube-api-injection-token.ts b/src/common/k8s-api/kube-api/kube-api-injection-token.ts
new file mode 100644
index 0000000000..92b6636274
--- /dev/null
+++ b/src/common/k8s-api/kube-api/kube-api-injection-token.ts
@@ -0,0 +1,10 @@
+/**
+ * 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 { KubeApi } from "../kube-api";
+
+export const kubeApiInjectionToken = getInjectionToken>({
+ id: "kube-api-injection-token",
+});
diff --git a/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.global-override-for-injectable.ts b/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.global-override-for-injectable.ts
new file mode 100644
index 0000000000..ca999211f5
--- /dev/null
+++ b/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.global-override-for-injectable.ts
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getGlobalOverride } from "../../../../../../common/test-utils/get-global-override";
+import callForPatchResourceInjectable from "./call-for-patch-resource.injectable";
+
+export default getGlobalOverride(callForPatchResourceInjectable, () => () => {
+ throw new Error(
+ "Tried to call patching of kube resource without explicit override.",
+ );
+});
diff --git a/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable.ts b/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable.ts
new file mode 100644
index 0000000000..097ef1ccf3
--- /dev/null
+++ b/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable.ts
@@ -0,0 +1,49 @@
+/**
+ * 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 { AsyncResult } from "../../../../../../common/utils/async-result";
+import apiManagerInjectable from "../../../../../../common/k8s-api/api-manager/manager.injectable";
+import type { JsonPatch } from "../../../../../../common/k8s-api/kube-object.store";
+import type { KubeObject } from "../../../../../../common/k8s-api/kube-object";
+import assert from "assert";
+import { getErrorMessage } from "../../../../../../common/utils/get-error-message";
+
+export type CallForPatchResource = (
+ item: KubeObject,
+ patch: JsonPatch
+) => Promise>;
+
+const callForPatchResourceInjectable = getInjectable({
+ id: "call-for-patch-resource",
+ instantiate: (di): CallForPatchResource => {
+ const apiManager = di.inject(apiManagerInjectable);
+
+ return async (item, patch) => {
+ const store = apiManager.getStore(item.selfLink);
+
+ assert(store);
+
+ let kubeObject: KubeObject;
+
+ try {
+ kubeObject = await store.patch(item, patch);
+ } catch (e: any) {
+ return {
+ callWasSuccessful: false,
+ error: getErrorMessage(e),
+ };
+ }
+
+ return {
+ callWasSuccessful: true,
+ response: { name: kubeObject.getName(), kind: kubeObject.kind },
+ };
+ };
+ },
+
+ causesSideEffects: true,
+});
+
+export default callForPatchResourceInjectable;
diff --git a/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.global-override-for-injectable.ts b/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.global-override-for-injectable.ts
new file mode 100644
index 0000000000..a4e768da9a
--- /dev/null
+++ b/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.global-override-for-injectable.ts
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getGlobalOverride } from "../../../../../../common/test-utils/get-global-override";
+import callForResourceInjectable from "./call-for-resource.injectable";
+
+export default getGlobalOverride(callForResourceInjectable, () => () => {
+ throw new Error(
+ "Tried to call for kube resource without explicit override.",
+ );
+});
diff --git a/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable.ts b/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable.ts
new file mode 100644
index 0000000000..a74da59c78
--- /dev/null
+++ b/src/renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable.ts
@@ -0,0 +1,50 @@
+/**
+ * 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 { KubeObject } from "../../../../../../common/k8s-api/kube-object";
+import { parseKubeApi } from "../../../../../../common/k8s-api/kube-api-parse";
+import type { AsyncResult } from "../../../../../../common/utils/async-result";
+import { getErrorMessage } from "../../../../../../common/utils/get-error-message";
+import apiManagerInjectable from "../../../../../../common/k8s-api/api-manager/manager.injectable";
+import { waitUntilDefined } from "../../../../../../common/utils";
+
+export type CallForResource = (
+ selfLink: string
+) => Promise>;
+
+const callForResourceInjectable = getInjectable({
+ id: "call-for-resource",
+
+ instantiate: (di): CallForResource => {
+ const apiManager = di.inject(apiManagerInjectable);
+
+ return async (apiPath: string) => {
+ const api = await waitUntilDefined(() => apiManager.getApi(apiPath));
+
+ const parsed = parseKubeApi(apiPath);
+
+ if (!api || !parsed.name) {
+ return { callWasSuccessful: false, error: "Invalid API path" };
+ }
+
+ let resource: KubeObject | null;
+
+ try {
+ resource = await api.get({
+ name: parsed.name,
+ namespace: parsed.namespace,
+ });
+ } catch (e) {
+ return { callWasSuccessful: false, error: getErrorMessage(e) };
+ }
+
+ return { callWasSuccessful: true, response: resource || undefined };
+ };
+ },
+
+ causesSideEffects: true,
+});
+
+export default callForResourceInjectable;
diff --git a/src/renderer/components/dock/edit-resource/edit-resource-model/edit-resource-model.injectable.tsx b/src/renderer/components/dock/edit-resource/edit-resource-model/edit-resource-model.injectable.tsx
new file mode 100644
index 0000000000..f6e3d09d63
--- /dev/null
+++ b/src/renderer/components/dock/edit-resource/edit-resource-model/edit-resource-model.injectable.tsx
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
+import type { CallForResource } from "./call-for-resource/call-for-resource.injectable";
+import callForResourceInjectable from "./call-for-resource/call-for-resource.injectable";
+import { waitUntilDefined } from "../../../../../common/utils";
+import editResourceTabStoreInjectable from "../store.injectable";
+import type { EditingResource, EditResourceTabStore } from "../store";
+import { action, computed, observable, runInAction } from "mobx";
+import type { KubeObject } from "../../../../../common/k8s-api/kube-object";
+import yaml from "js-yaml";
+import assert from "assert";
+import type { CallForPatchResource } from "./call-for-patch-resource/call-for-patch-resource.injectable";
+import callForPatchResourceInjectable from "./call-for-patch-resource/call-for-patch-resource.injectable";
+import { createPatch } from "rfc6902";
+import type { ShowNotification } from "../../../notifications";
+import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
+import React from "react";
+import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
+
+const editResourceModelInjectable = getInjectable({
+ id: "edit-resource-model",
+
+ instantiate: async (di, tabId: string) => {
+ const store = di.inject(editResourceTabStoreInjectable);
+
+ const model = new EditResourceModel({
+ callForResource: di.inject(callForResourceInjectable),
+ callForPatchResource: di.inject(callForPatchResourceInjectable),
+ showSuccessNotification: di.inject(showSuccessNotificationInjectable),
+ showErrorNotification: di.inject(showErrorNotificationInjectable),
+ store,
+ tabId,
+
+ waitForEditingResource: () =>
+ waitUntilDefined(() => store.getData(tabId)),
+ });
+
+ await model.load();
+
+ return model;
+ },
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (di, tabId: string) => tabId,
+ }),
+});
+
+export default editResourceModelInjectable;
+
+interface Dependencies {
+ callForResource: CallForResource;
+ callForPatchResource: CallForPatchResource;
+ waitForEditingResource: () => Promise;
+ showSuccessNotification: ShowNotification;
+ showErrorNotification: ShowNotification;
+ store: EditResourceTabStore;
+ tabId: string;
+}
+
+export class EditResourceModel {
+ constructor(private dependencies: Dependencies) {
+ }
+
+ readonly configuration = {
+ value: computed(() => this.editingResource.draft || this.editingResource.firstDraft || ""),
+
+ onChange: action((value: string) => {
+ this.editingResource.draft = value;
+ this.configuration.error.value.set("");
+ }),
+
+ error: {
+ value: observable.box(""),
+
+ onChange: action((error: string) => {
+ this.configuration.error.value.set(error);
+ }),
+ },
+ };
+
+ @observable private _resource: KubeObject | undefined;
+
+ @computed get shouldShowErrorAboutNoResource() {
+ return !this._resource;
+ }
+
+ @computed get resource() {
+ assert(this._resource, "Resource does not have data");
+
+ return this._resource;
+ }
+
+ @computed get editingResource() {
+ const resource = this.dependencies.store.getData(this.dependencies.tabId);
+
+ assert(resource, "Resource is not present in the store");
+
+ return resource;
+ }
+
+ @computed private get selfLink() {
+ return this.editingResource.resource;
+ }
+
+ load = async () => {
+ await this.dependencies.waitForEditingResource();
+
+ const result = await this.dependencies.callForResource(this.selfLink);
+
+ if (!result.callWasSuccessful) {
+ this.dependencies.showErrorNotification(
+ `Loading resource failed: ${result.error}`,
+ );
+
+ return;
+ }
+
+ const resource = result.response;
+
+ runInAction(() => {
+ this._resource = resource;
+ });
+
+ if (!resource) {
+ return;
+ }
+
+ runInAction(() => {
+ this.editingResource.firstDraft = yaml.dump(resource.toPlainObject());
+ });
+ };
+
+ get namespace() {
+ return this.resource.metadata.namespace || "default";
+ }
+
+ get name() {
+ return this.resource.metadata.name;
+ }
+
+ get kind() {
+ return this.resource.kind;
+ }
+
+ save = async () => {
+ const currentValue = this.configuration.value.get();
+ const currentVersion = yaml.load(currentValue);
+ const firstVersion = yaml.load(this.editingResource.firstDraft ?? currentValue);
+ const patches = createPatch(firstVersion, currentVersion);
+
+ const result = await this.dependencies.callForPatchResource(this.resource, patches);
+
+ if (!result.callWasSuccessful) {
+ this.dependencies.showErrorNotification(
+
+ Failed to save resource:
+ {" "}
+ {result.error}
+
,
+ );
+
+ return;
+ }
+
+ const { kind, name } = result.response;
+
+ this.dependencies.showSuccessNotification(
+
+ {kind}
+ {" "}
+ {name}
+ {" updated."}
+
,
+ );
+
+ runInAction(() => {
+ this.editingResource.firstDraft = currentValue;
+ });
+ };
+}
diff --git a/src/renderer/components/dock/edit-resource/edit-resource-tab.injectable.ts b/src/renderer/components/dock/edit-resource/edit-resource-tab.injectable.ts
index 5d9382e4ce..c3eb15f0f9 100644
--- a/src/renderer/components/dock/edit-resource/edit-resource-tab.injectable.ts
+++ b/src/renderer/components/dock/edit-resource/edit-resource-tab.injectable.ts
@@ -6,52 +6,52 @@ import { getInjectable } from "@ogre-tools/injectable";
import editResourceTabStoreInjectable from "./store.injectable";
import dockStoreInjectable from "../dock/store.injectable";
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
-import type { DockStore, DockTabCreateSpecific, TabId } from "../dock/store";
+import type { DockTabCreateSpecific, TabId } from "../dock/store";
import { TabKind } from "../dock/store";
-import type { EditResourceTabStore } from "./store";
import { runInAction } from "mobx";
-
-interface Dependencies {
- dockStore: DockStore;
- editResourceStore: EditResourceTabStore;
-}
-
-const createEditResourceTab = ({ dockStore, editResourceStore }: Dependencies) => (object: KubeObject, tabParams: DockTabCreateSpecific = {}): TabId => {
- // use existing tab if already opened
- const tabId = editResourceStore.getTabIdByResource(object);
-
- if (tabId) {
- dockStore.open();
- dockStore.selectTab(tabId);
-
- return tabId;
- }
-
- return runInAction(() => {
- const tab = dockStore.createTab(
- {
- title: `${object.kind}: ${object.getName()}`,
- ...tabParams,
- kind: TabKind.EDIT_RESOURCE,
- },
- false,
- );
-
- editResourceStore.setData(tab.id, {
- resource: object.selfLink,
- });
-
- return tab.id;
- });
-};
+import getRandomIdForEditResourceTabInjectable from "./get-random-id-for-edit-resource-tab.injectable";
const createEditResourceTabInjectable = getInjectable({
id: "create-edit-resource-tab",
- instantiate: (di) => createEditResourceTab({
- dockStore: di.inject(dockStoreInjectable),
- editResourceStore: di.inject(editResourceTabStoreInjectable),
- }),
+ instantiate: (di) => {
+ const dockStore = di.inject(dockStoreInjectable);
+ const editResourceStore = di.inject(editResourceTabStoreInjectable);
+ const getRandomId = di.inject(getRandomIdForEditResourceTabInjectable);
+
+ return (
+ object: KubeObject,
+ tabParams: DockTabCreateSpecific = {},
+ ): TabId => {
+ // use existing tab if already opened
+ const tabId = editResourceStore.getTabIdByResource(object);
+
+ if (tabId) {
+ dockStore.open();
+ dockStore.selectTab(tabId);
+
+ return tabId;
+ }
+
+ return runInAction(() => {
+ const tab = dockStore.createTab(
+ {
+ id: getRandomId(),
+ title: `${object.kind}: ${object.getName()}`,
+ ...tabParams,
+ kind: TabKind.EDIT_RESOURCE,
+ },
+ false,
+ );
+
+ editResourceStore.setData(tab.id, {
+ resource: object.selfLink,
+ });
+
+ return tab.id;
+ });
+ };
+ },
});
export default createEditResourceTabInjectable;
diff --git a/src/renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable.ts b/src/renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable.ts
new file mode 100644
index 0000000000..e71330d261
--- /dev/null
+++ b/src/renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable.ts
@@ -0,0 +1,13 @@
+/**
+ * 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 getRandomIdInjectable from "../../../../common/utils/get-random-id.injectable";
+
+const getRandomIdForEditResourceTabInjectable = getInjectable({
+ id: "get-random-id-for-edit-resource-tab",
+ instantiate: (di) => di.inject(getRandomIdInjectable),
+});
+
+export default getRandomIdForEditResourceTabInjectable;
diff --git a/src/renderer/components/dock/edit-resource/store.injectable.ts b/src/renderer/components/dock/edit-resource/store.injectable.ts
index 3a8126abf3..dbf4f44a6d 100644
--- a/src/renderer/components/dock/edit-resource/store.injectable.ts
+++ b/src/renderer/components/dock/edit-resource/store.injectable.ts
@@ -5,14 +5,12 @@
import { getInjectable } from "@ogre-tools/injectable";
import { EditResourceTabStore } from "./store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
-import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
const editResourceTabStoreInjectable = getInjectable({
id: "edit-resource-tab-store",
instantiate: (di) => new EditResourceTabStore({
createStorage: di.inject(createStorageInjectable),
- apiManager: di.inject(apiManagerInjectable),
}),
});
diff --git a/src/renderer/components/dock/edit-resource/store.ts b/src/renderer/components/dock/edit-resource/store.ts
index 97890db272..13a2d30ad9 100644
--- a/src/renderer/components/dock/edit-resource/store.ts
+++ b/src/renderer/components/dock/edit-resource/store.ts
@@ -5,10 +5,7 @@
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
-import type { TabId } from "../dock/store";
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
-import type { ApiManager } from "../../../../common/k8s-api/api-manager";
-import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
export interface EditingResource {
resource: string; // resource path, e.g. /api/v1/namespaces/default
@@ -16,50 +13,14 @@ export interface EditingResource {
firstDraft?: string;
}
-export interface EditResourceTabStoreDependencies extends DockTabStoreDependencies {
- readonly apiManager: ApiManager;
-}
-
export class EditResourceTabStore extends DockTabStore {
- constructor(protected readonly dependencies: EditResourceTabStoreDependencies) {
+ constructor(protected readonly dependencies: DockTabStoreDependencies) {
super(dependencies, {
storageKey: "edit_resource_store",
});
}
- protected finalizeDataForSave({ draft, ...data }: EditingResource): EditingResource {
- return data; // skip saving draft to local-storage
- }
-
- isReady(tabId: TabId) {
- return super.isReady(tabId) && Boolean(this.getResource(tabId)); // ready to edit resource
- }
-
- getStore(tabId: TabId): KubeObjectStore | undefined {
- const apiPath = this.getResourcePath(tabId);
-
- return apiPath
- ? this.dependencies.apiManager.getStore(apiPath)
- : undefined;
- }
-
- getResource(tabId: TabId): KubeObject | undefined {
- const apiPath = this.getResourcePath(tabId);
-
- return apiPath
- ? this.dependencies.apiManager.getStore(apiPath)?.getByPath(apiPath)
- : undefined;
- }
-
- getResourcePath(tabId: TabId): string | undefined {
- return this.getData(tabId)?.resource;
- }
-
getTabIdByResource(object: KubeObject): string | undefined {
return this.findTabIdFromData(({ resource }) => object.selfLink === resource);
}
-
- clearInitialDraft(tabId: TabId): void {
- delete this.getData(tabId)?.firstDraft;
- }
}
diff --git a/src/renderer/components/dock/edit-resource/view.tsx b/src/renderer/components/dock/edit-resource/view.tsx
index 73f2077152..35714d2d24 100644
--- a/src/renderer/components/dock/edit-resource/view.tsx
+++ b/src/renderer/components/dock/edit-resource/view.tsx
@@ -4,169 +4,82 @@
*/
import React from "react";
-import { autorun, makeObservable, observable } from "mobx";
-import { disposeOnUnmount, observer } from "mobx-react";
-import yaml from "js-yaml";
-import type { DockTab, TabId } from "../dock/store";
-import type { EditingResource, EditResourceTabStore } from "./store";
+import { observer } from "mobx-react";
+import type { DockTab } from "../dock/store";
+import { Spinner } from "../../spinner";
+import { withInjectables } from "@ogre-tools/injectable-react";
+import type { EditResourceModel } from "./edit-resource-model/edit-resource-model.injectable";
+import editResourceModelInjectable from "./edit-resource-model/edit-resource-model.injectable";
+import { EditorPanel } from "../editor-panel";
import { InfoPanel } from "../info-panel";
import { Badge } from "../../badge";
-import { EditorPanel } from "../editor-panel";
-import { Spinner } from "../../spinner";
-import type { KubeObject } from "../../../../common/k8s-api/kube-object";
-import { createPatch } from "rfc6902";
-import { withInjectables } from "@ogre-tools/injectable-react";
-import editResourceTabStoreInjectable from "./store.injectable";
-import { noop, onceDefined } from "../../../utils";
-import closeDockTabInjectable from "../dock/close-dock-tab.injectable";
-import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
+import { Notice } from "../../+extensions/notice";
export interface EditResourceProps {
tab: DockTab;
}
interface Dependencies {
- editResourceStore: EditResourceTabStore;
- closeTab: (tabId: TabId) => void;
+ model: EditResourceModel;
}
-interface SaveDraftArgs {
- tabData: EditingResource;
- resource: KubeObject;
- store: KubeObjectStore;
-}
-
-@observer
-class NonInjectedEditResource extends React.Component {
- @observable error = "";
- @observable draft = "";
-
- constructor(props: EditResourceProps & Dependencies) {
- super(props);
- makeObservable(this);
- }
-
- componentDidMount(): void {
- disposeOnUnmount(this, [
- onceDefined(
- () => {
- const tabData = this.tabData;
- const resource = this.resource;
-
- if (tabData && resource) {
- return { tabData, resource };
- }
-
- return undefined;
- },
- ({ tabData, resource }) => {
- if (typeof tabData.draft === "string") {
- this.draft = tabData.draft;
- } else {
- this.draft = tabData.firstDraft = yaml.dump(resource.toPlainObject());
- }
- },
- ),
- autorun(() => {
- const store = this.store;
- const tabData = this.tabData;
- const resource = this.resource;
-
- if (!resource && store && tabData) {
- if (store.isLoaded) {
- // auto-close tab when resource removed from store
- this.props.closeTab(this.props.tab.id);
- } else if (!store.isLoading) {
- // preload resource for editing
- store.loadFromPath(tabData.resource).catch(noop);
- }
- }
- }),
- ]);
- }
-
- get tabId() {
- return this.props.tab.id;
- }
-
- get store() {
- return this.props.editResourceStore.getStore(this.props.tab.id);
- }
-
- get resource() {
- return this.props.editResourceStore.getResource(this.tabId);
- }
-
- get tabData() {
- return this.props.editResourceStore.getData(this.tabId);
- }
-
- async save({ resource, store, tabData }: SaveDraftArgs) {
- if (this.error) {
- return null;
- }
-
- const currentVersion = yaml.load(this.draft);
- const firstVersion = yaml.load(tabData.firstDraft ?? this.draft);
- const patches = createPatch(firstVersion, currentVersion);
- const updatedResource = await store.patch(resource, patches);
-
- this.props.editResourceStore.clearInitialDraft(this.tabId);
-
- return (
-
- {updatedResource.kind}
- {" "}
- {updatedResource.getName()}
- {" updated."}
-
- );
- }
-
- render() {
- const { tabId, error, draft, tabData, resource, store } = this;
-
- if (!tabData || !resource || !store) {
- return ;
- }
-
+const NonInjectedEditResource = observer(
+ ({ model, tab: { id: tabId }}: EditResourceProps & Dependencies) => {
return (
-
this.save({ resource, store, tabData })}
- submitLabel="Save"
- submittingMessage="Applying.."
- controls={(
-
- Kind:
-
- Name:
-
- Namespace:
-
-
- )}
- />
- {
- this.error = "";
- this.draft = tabData.draft = draft;
- }}
- onError={error => this.error = String(error)}
- />
+ {model.shouldShowErrorAboutNoResource && (
+
+ Resource not found
+
+ )}
+
+ {!model.shouldShowErrorAboutNoResource && (
+ <>
+
+ Kind:
+
+ Name:
+
+ Namespace:
+
+
+ )}
+ />
+
+ >
+ )}
);
- }
-}
+ },
+);
-export const EditResource = withInjectables(NonInjectedEditResource, {
- getProps: (di, props) => ({
- editResourceStore: di.inject(editResourceTabStoreInjectable),
- closeTab: di.inject(closeDockTabInjectable),
- ...props,
- }),
-});
+export const EditResource = withInjectables(
+ NonInjectedEditResource,
+ {
+ getPlaceholder: () => (
+
+ ),
+
+ getProps: async (di, props) => ({
+ model: await di.inject(editResourceModelInjectable, props.tab.id),
+ ...props,
+ }),
+ },
+);
diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx
index 22ecea94ec..48a53a0308 100644
--- a/src/renderer/components/dock/info-panel.tsx
+++ b/src/renderer/components/dock/info-panel.tsx
@@ -39,6 +39,7 @@ export interface OptionalProps {
showNotifications?: boolean;
showStatusPanel?: boolean;
submitTestId?: string;
+ submitAndCloseTestId?: string;
cancelTestId?: string;
submittingTestId?: string;
}
@@ -167,6 +168,7 @@ class NonInjectedInfoPanel extends Component {
label={`${submitLabel} & Close`}
onClick={submitAndClose}
disabled={isDisabled}
+ data-testid={this.props.submitAndCloseTestId}
/>
)}
>
diff --git a/src/renderer/components/monaco-editor/__mocks__/monaco-editor.tsx b/src/renderer/components/monaco-editor/__mocks__/monaco-editor.tsx
index e7b01cc876..23524a476b 100644
--- a/src/renderer/components/monaco-editor/__mocks__/monaco-editor.tsx
+++ b/src/renderer/components/monaco-editor/__mocks__/monaco-editor.tsx
@@ -12,7 +12,7 @@ class FakeMonacoEditor extends React.Component {
const { id, value, onChange, onError, language = "yaml" } = this.props;
return (
- {
diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx
index 995e1cb54e..da07584816 100644
--- a/src/renderer/components/test-utils/get-application-builder.tsx
+++ b/src/renderer/components/test-utils/get-application-builder.tsx
@@ -5,7 +5,7 @@
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable";
-import type { IObservableArray, ObservableSet } from "mobx";
+import type { ObservableSet } from "mobx";
import { computed, observable, runInAction } from "mobx";
import React from "react";
import { Router } from "react-router";
@@ -36,7 +36,6 @@ import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-f
import startMainApplicationInjectable from "../../../main/start-main-application/start-main-application.injectable";
import startFrameInjectable from "../../start-frame/start-frame.injectable";
import type { NamespaceStore } from "../+namespaces/store";
-import namespaceStoreInjectable from "../+namespaces/store.injectable";
import historyInjectable from "../../navigation/history.injectable";
import type { MinimalTrayMenuItem } from "../../../main/tray/electron-tray/electron-tray.injectable";
import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable";
@@ -61,6 +60,8 @@ import { ClusterFrame } from "../../frames/cluster-frame/cluster-frame";
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
import activeKubernetesClusterInjectable from "../../cluster-frame-context/active-kubernetes-cluster.injectable";
import { catalogEntityFromCluster } from "../../../main/cluster-manager";
+import namespaceStoreInjectable from "../+namespaces/store.injectable";
+import { isAllowedResource } from "../../../common/cluster/is-allowed-resource";
type Callback = (dis: DiContainers) => void | Promise;
@@ -208,7 +209,7 @@ export const getApplicationBuilder = () => {
},
}));
- let allowedResourcesState: IObservableArray;
+ let allowedResourcesState: KubeResource[];
let rendered: RenderResult;
const enableExtensionsFor = (
@@ -398,16 +399,27 @@ export const getApplicationBuilder = () => {
const clusterStub = {
accessibleNamespaces: [],
+ isAllowedResource: isAllowedResource(allowedResourcesState),
} as unknown as Cluster;
rendererDi.override(activeKubernetesClusterInjectable, () =>
computed(() => catalogEntityFromCluster(clusterStub)),
);
+ // TODO: Figure out a way to remove this stub.
const namespaceStoreStub = {
+ isLoaded: true,
contextNamespaces: [],
+ contextItems: [],
+ api: {
+ kind: "Namespace",
+ },
items: [],
selectNamespaces: () => {},
+ getByPath: () => undefined,
+ pickOnlySelected: () => [],
+ isSelectedAll: () => false,
+ getTotalCount: () => 0,
} as unknown as NamespaceStore;
const clusterFrameContextFake = new ClusterFrameContext(
diff --git a/src/renderer/utils/create-storage/storages-are-ready.ts b/src/renderer/utils/create-storage/storages-are-ready.ts
index 641c64cf8b..04f2595472 100644
--- a/src/renderer/utils/create-storage/storages-are-ready.ts
+++ b/src/renderer/utils/create-storage/storages-are-ready.ts
@@ -22,5 +22,7 @@ export const controlWhenStoragesAreReady = (di: DiContainer) => {
// TODO: Remove when typing is added to the library
(di as any).decorateFunction(createStorageInjectable, decorated);
- return async () => void await Promise.all(storagesAreReady);
+ return async () => {
+ await Promise.all(storagesAreReady);
+ };
};