-`;
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
new file mode 100644
index 0000000000..ad5d354124
--- /dev/null
+++ b/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap
@@ -0,0 +1,18718 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts renders 1`] = `
+
+
+
+
+
+
+ Chart: some-repository/some-name
+
+
+ content_copy
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ some-description
+
+ Install
+
+
+
+
+ Version
+
+
+
+
+
+
+
+
+ some-version
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Maintainers
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given changing version to be installed renders 1`] = `
+
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given changing version to be installed when version is selected renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given changing version to be installed when version is selected when default configuration resolves renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given custom name is inputted renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given invalid change in configuration renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given namespace selection is opened renders 1`] = `
+
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given namespace selection is opened when namespace is selected renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given no changes in configuration, when installing the chart renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given no changes in configuration, when installing the chart when installation resolves renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+
+ Chart Release
+
+ some-release
+
+ successfully created.
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given no changes in configuration, when installing the chart when installation resolves when selected to see the installed release renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+
+ Chart Release
+
+ some-release
+
+ successfully created.
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given no changes in configuration, when installing the chart when installation resolves when selected to show execution output renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+
+ Chart Release
+
+ some-release
+
+ successfully created.
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given opening details for second chart, when details resolve renders 1`] = `
+
+
+
+
+
+
+ Chart: some-repository/some-other-name
+
+
+ content_copy
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ some-description
+
+ Install
+
+
+
+
+ Version
+
+
+
+
+
+
+
+
+ some-version
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Maintainers
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given opening details for second chart, when details resolve when selecting to install second chart renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given opening details for second chart, when details resolve when selecting to install second chart when configuration and versions resolve renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given opening details for second chart, when details resolve when selecting to install second chart when configuration and versions resolve when selecting the dock tab for installing first chart renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve given valid change in configuration renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from new tab given tab for installing chart was not previously opened and application is started, when navigating to helm charts when selecting to install the chart when default configuration and versions resolve when cancelled renders 1`] = `
+
+
+
+`;
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
new file mode 100644
index 0000000000..b0e67015f3
--- /dev/null
+++ b/src/behaviours/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-previously-opened-tab.test.ts.snap
@@ -0,0 +1,1257 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`installing helm chart from previously opened tab given tab for installing chart was previously opened, when application is started renders 1`] = `
+
+
+
+`;
+
+exports[`installing helm chart from previously opened tab given tab for installing chart was previously opened, when application is started when configuration and version resolves renders 1`] = `
+
+
+
+`;
diff --git a/src/behaviours/helm-charts/installing-chart/__snapshots__/opening-dock-tab-for-installing-helm-chart.test.ts.snap b/src/behaviours/helm-charts/installing-chart/__snapshots__/opening-dock-tab-for-installing-helm-chart.test.ts.snap
new file mode 100644
index 0000000000..0f3c4937ca
--- /dev/null
+++ b/src/behaviours/helm-charts/installing-chart/__snapshots__/opening-dock-tab-for-installing-helm-chart.test.ts.snap
@@ -0,0 +1,6352 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`opening dock tab for installing helm chart given application is started, when navigating to helm charts renders 1`] = `
+
+
+
+`;
+
+exports[`opening dock tab for installing helm chart given application is started, when navigating to helm charts when charts resolve renders 1`] = `
+
+
+
+`;
+
+exports[`opening dock tab for installing helm chart given application is started, when navigating to helm charts when charts resolve when opening details of a chart renders 1`] = `
+
+
+
+
+
+
+ Chart: some-repository/some-name
+
+
+ content_copy
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+`;
+
+exports[`opening dock tab for installing helm chart given application is started, when navigating to helm charts when charts resolve when opening details of a chart when chart versions resolve renders 1`] = `
+
+
+
+
+
+
+ Chart: some-repository/some-name
+
+
+ content_copy
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ some-description
+
+ Install
+
+
+
+
+ Version
+
+
+
+
+
+
+
+
+ some-version
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Maintainers
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`opening dock tab for installing helm chart given application is started, when navigating to helm charts when charts resolve when opening details of a chart when chart versions resolve when readme resolves renders 1`] = `
+
+
+
+
+
+
+ Chart: some-repository/some-name
+
+
+ content_copy
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ some-description
+
+ Install
+
+
+
+
+ Version
+
+
+
+
+
+
+
+
+ some-version
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Maintainers
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`opening dock tab for installing helm chart given application is started, when navigating to helm charts when charts resolve when opening details of a chart when chart versions resolve when readme resolves when selecting different version renders 1`] = `
+
+
+
+
+
+
+ Chart: some-repository/some-name
+
+
+ content_copy
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ some-description
+
+ Install
+
+
+
+
+ Version
+
+
+
+
+
+
+
+
+ some-other-version
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Maintainers
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`opening dock tab for installing helm chart given application is started, when navigating to helm charts when charts resolve when opening details of a chart when chart versions resolve when readme resolves when selecting different version when readme resolves renders 1`] = `
+
+
+
+
+
+
+ Chart: some-repository/some-name
+
+
+ content_copy
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ some-description
+
+ Install
+
+
+
+
+ Version
+
+
+
+
+
+
+
+
+ some-other-version
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Maintainers
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`opening dock tab for installing helm chart given application is started, when navigating to helm charts when charts resolve when opening details of a chart when chart versions resolve when readme resolves when selecting to install the chart renders 1`] = `
+
+
+
+`;
diff --git a/src/behaviours/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts b/src/behaviours/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts
new file mode 100644
index 0000000000..f3ee91257d
--- /dev/null
+++ b/src/behaviours/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts
@@ -0,0 +1,1024 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+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 type { CallForHelmCharts } from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable";
+import callForHelmChartsInjectable from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable";
+import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
+import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable";
+import type { CallForHelmChartValues } from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable";
+import callForHelmChartValuesInjectable from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable";
+import type { CallForCreateHelmRelease } from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable";
+import callForCreateHelmReleaseInjectable from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable";
+import currentPathInjectable from "../../../renderer/routes/current-path.injectable";
+import namespaceStoreInjectable from "../../../renderer/components/+namespaces/store.injectable";
+import type { NamespaceStore } from "../../../renderer/components/+namespaces/store";
+import type { CallForHelmChartReadme } from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable";
+import callForHelmChartReadmeInjectable from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable";
+import type { CallForHelmChartVersions } from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
+import callForHelmChartVersionsInjectable from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
+import { overrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
+import writeJsonFileInjectable from "../../../common/fs/write-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 dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
+import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
+import type { DiContainer } from "@ogre-tools/injectable";
+
+// TODO: Make tooltips free of side effects by making it deterministic
+jest.mock("../../../renderer/components/tooltip/withTooltip", () => ({
+ withTooltip: (target: any) => target,
+}));
+
+describe("installing helm chart from new tab", () => {
+ let builder: ApplicationBuilder;
+ let rendererDi: DiContainer;
+ let callForHelmChartsMock: AsyncFnMock
;
+ let callForHelmChartVersionsMock: AsyncFnMock;
+ let callForHelmChartReadmeMock: AsyncFnMock;
+ let callForHelmChartValuesMock: AsyncFnMock;
+ let callForCreateHelmReleaseMock: AsyncFnMock;
+
+ beforeEach(() => {
+ builder = getApplicationBuilder();
+
+ rendererDi = builder.dis.rendererDi;
+
+ overrideFsWithFakes(rendererDi);
+
+ callForHelmChartsMock = asyncFn();
+ callForHelmChartVersionsMock = asyncFn();
+ callForHelmChartReadmeMock = asyncFn();
+ callForHelmChartValuesMock = asyncFn();
+ callForCreateHelmReleaseMock = asyncFn();
+
+ builder.beforeApplicationStart(({ rendererDi }) => {
+ rendererDi.override(
+ directoryForLensLocalStorageInjectable,
+ () => "/some-directory-for-lens-local-storage",
+ );
+
+ rendererDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
+
+ rendererDi.override(
+ callForHelmChartsInjectable,
+ () => callForHelmChartsMock,
+ );
+
+ rendererDi.override(
+ callForHelmChartVersionsInjectable,
+ () => callForHelmChartVersionsMock,
+ );
+
+ rendererDi.override(
+ callForHelmChartReadmeInjectable,
+ () => callForHelmChartReadmeMock,
+ );
+
+ rendererDi.override(
+ callForHelmChartValuesInjectable,
+ () => callForHelmChartValuesMock,
+ );
+
+ rendererDi.override(
+ callForCreateHelmReleaseInjectable,
+ () => callForCreateHelmReleaseMock,
+ );
+
+ // TODO: Replace store mocking with mock for the actual side-effect (where the namespaces are coming from)
+ rendererDi.override(
+ namespaceStoreInjectable,
+ () =>
+ ({
+ contextNamespaces: [],
+ items: [
+ { getName: () => "default" },
+ { getName: () => "some-other-namespace" },
+ ],
+ selectNamespaces: () => {},
+ } as unknown as NamespaceStore),
+ );
+
+ rendererDi.override(getRandomInstallChartTabIdInjectable, () =>
+ jest
+ .fn(() => "some-irrelevant-tab-id")
+ .mockReturnValueOnce("some-first-tab-id")
+ .mockReturnValueOnce("some-second-tab-id"),
+ );
+ });
+
+ builder.setEnvironmentToClusterFrame();
+ });
+
+ describe("given tab for installing chart was not previously opened and application is started, when navigating to helm charts", () => {
+ let rendered: RenderResult;
+
+ beforeEach(async () => {
+ rendered = await builder.render();
+
+ builder.helmCharts.navigate({
+ chartName: "some-name",
+ repo: "some-repository",
+ });
+
+ const writeJsonFile = rendererDi.inject(writeJsonFileInjectable);
+
+ writeJsonFile(
+ "/some-directory-for-lens-local-storage/some-cluster-id.json",
+ {
+ dock: {
+ height: 300,
+ tabs: [],
+ isOpen: false,
+ },
+ },
+ );
+
+ const dockStore = rendererDi.inject(dockStoreInjectable);
+
+ // TODO: Make TerminalWindow unit testable to allow realistic behaviour
+ dockStore.closeTab("terminal");
+
+ await callForHelmChartsMock.resolve([
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-other-name",
+ version: "some-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+ ]);
+
+ await callForHelmChartVersionsMock.resolve([
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-other-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+ ]);
+
+ await callForHelmChartReadmeMock.resolve("some-readme");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when selecting to install the chart", () => {
+ beforeEach(() => {
+ callForHelmChartVersionsMock.mockClear();
+
+ const installButton = rendered.getByTestId(
+ "install-chart-for-some-repository-some-name",
+ );
+
+ fireEvent.click(installButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows dock tab for installing the chart", () => {
+ expect(
+ rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("calls for default configuration of the chart", () => {
+ expect(callForHelmChartValuesMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-name",
+ "some-version",
+ );
+ });
+
+ it("calls for available versions", () => {
+ expect(callForHelmChartVersionsMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-name",
+ );
+ });
+
+ it("shows spinner in dock tab", () => {
+ expect(
+ rendered.getByTestId("install-chart-tab-spinner"),
+ ).toBeInTheDocument();
+ });
+
+ it("given default configuration resolves but versions have not resolved yet, still shows the spinner", async () => {
+ await callForHelmChartValuesMock.resolve(
+ "some-default-configuration",
+ );
+
+ expect(
+ rendered.getByTestId("install-chart-tab-spinner"),
+ ).toBeInTheDocument();
+ });
+
+ it("given versions resolve but default configuration has not resolved yet, still shows the spinner", async () => {
+ await callForHelmChartVersionsMock.resolve([]);
+
+ expect(
+ rendered.getByTestId("install-chart-tab-spinner"),
+ ).toBeInTheDocument();
+ });
+
+ describe("when default configuration and versions resolve", () => {
+ beforeEach(async () => {
+ await callForHelmChartValuesMock.resolve(
+ "some-default-configuration",
+ );
+
+ await callForHelmChartVersionsMock.resolve([
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-other-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+ ]);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show spinner anymore", () => {
+ expect(
+ rendered.queryByTestId("install-chart-tab-spinner"),
+ ).not.toBeInTheDocument();
+ });
+
+ describe("when cancelled", () => {
+ beforeEach(() => {
+ const cancelButton = rendered.getByTestId(
+ "cancel-install-chart-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(cancelButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("closes the tab", () => {
+ expect(
+ rendered.queryByTestId("dock-tab-for-some-first-tab-id"),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given no changes in configuration, when installing the chart", () => {
+ let installButton: HTMLButtonElement;
+
+ beforeEach(() => {
+ installButton = rendered.getByTestId(
+ "install-chart-from-tab-for-some-first-tab-id",
+ ) as HTMLButtonElement;
+
+ fireEvent.click(installButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows spinner in dock tab", () => {
+ expect(
+ rendered.getByTestId(
+ "installing-chart-from-tab-some-first-tab-id",
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("install button is disabled", () => {
+ expect(installButton).toHaveAttribute("disabled");
+ });
+
+ it("calls for installation with default configuration", () => {
+ expect(callForCreateHelmReleaseMock).toHaveBeenCalledWith({
+ chart: "some-name",
+ name: undefined,
+ namespace: "default",
+ repo: "some-repository",
+ values: "some-default-configuration",
+ version: "some-version",
+ });
+ });
+
+ describe("when installation resolves", () => {
+ beforeEach(async () => {
+ await callForCreateHelmReleaseMock.resolve({
+ log: "some-execution-output",
+
+ release: {
+ resources: [],
+ name: "some-release",
+ namespace: "default",
+ version: "some-version",
+ config: "some-config",
+ manifest: "some-manifest",
+
+ info: {
+ deleted: "some-deleted",
+ description: "some-description",
+ first_deployed: "some-first-deployed",
+ last_deployed: "some-last-deployed",
+ notes: "some-notes",
+ status: "some-status",
+ },
+ },
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show spinner anymore", () => {
+ expect(
+ rendered.queryByTestId(
+ "installing-chart-from-tab-some-first-tab-id",
+ ),
+ ).not.toBeInTheDocument();
+ });
+
+ describe("when selected to see the installed release", () => {
+ beforeEach(() => {
+ const releaseButton = rendered.getByTestId(
+ "show-release-some-release-for-some-first-tab-id",
+ );
+
+ fireEvent.click(releaseButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows the details of installed release", () => {
+ const currentPath = rendererDi
+ .inject(currentPathInjectable)
+ .get();
+
+ expect(currentPath).toBe(
+ "/helm/releases/default/some-release",
+ );
+ });
+
+ it("closes the dock tab", () => {
+ expect(
+ rendered.queryByTestId(
+ "dock-tab-for-some-first-tab-id",
+ ),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("when selected to show execution output", () => {
+ beforeEach(() => {
+ const showNotesButton = rendered.getByTestId(
+ "show-execution-output-for-some-release-in-some-first-tab-id",
+ );
+
+ fireEvent.click(showNotesButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows the execution output", () => {
+ expect(
+ rendered.getByTestId(
+ "logs-dialog-for-helm-chart-install",
+ ),
+ ).toHaveTextContent("some-execution-output");
+ });
+
+ it("does not close the dock tab", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+ });
+ });
+ });
+
+ describe("given opening details for second chart, when details resolve", () => {
+ beforeEach(async () => {
+ callForHelmChartReadmeMock.mockClear();
+ callForHelmChartVersionsMock.mockClear();
+
+ const row = rendered.getByTestId(
+ "helm-chart-row-for-some-repository-some-other-name",
+ );
+
+ fireEvent.click(row);
+
+ await callForHelmChartVersionsMock.resolve([
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-other-name",
+ version: "some-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+ ]);
+
+ await callForHelmChartReadmeMock.resolve("some-readme");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when selecting to install second chart", () => {
+ beforeEach(() => {
+ callForHelmChartVersionsMock.mockClear();
+
+ const installButton = rendered.getByTestId(
+ "install-chart-for-some-repository-some-other-name",
+ );
+
+ fireEvent.click(installButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows dock tab for installing second chart", () => {
+ expect(
+ rendered.getByTestId(
+ "dock-tab-content-for-some-second-tab-id",
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("still has the dock tab for installing first chart", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("calls for default configuration of the second chart", () => {
+ expect(callForHelmChartValuesMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-other-name",
+ "some-version",
+ );
+ });
+
+ it("calls for available versions for the second chart", () => {
+ expect(callForHelmChartVersionsMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-other-name",
+ );
+ });
+
+ it("shows spinner in dock tab", () => {
+ expect(
+ rendered.getByTestId("install-chart-tab-spinner"),
+ ).toBeInTheDocument();
+ });
+
+ describe("when configuration and versions resolve", () => {
+ beforeEach(async () => {
+ await callForHelmChartValuesMock.resolve(
+ "some-other-default-configuration",
+ );
+
+ await callForHelmChartVersionsMock.resolve([]);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show spinner anymore", () => {
+ expect(
+ rendered.queryByTestId("install-chart-tab-spinner"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("when installing the second chart, calls for installation of second chart", () => {
+ const installButton = rendered.getByTestId(
+ "install-chart-from-tab-for-some-second-tab-id",
+ );
+
+ fireEvent.click(installButton);
+
+ expect(
+ callForCreateHelmReleaseMock,
+ ).toHaveBeenCalledWith({
+ chart: "some-other-name",
+ name: undefined,
+ namespace: "default",
+ repo: "some-repository",
+ values: "some-other-default-configuration",
+ version: "some-version",
+ });
+ });
+
+ describe("when selecting the dock tab for installing first chart", () => {
+ beforeEach(() => {
+ callForHelmChartValuesMock.mockClear();
+ callForHelmChartVersionsMock.mockClear();
+
+ const tab = rendered.getByTestId(
+ "dock-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(tab);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not call for default configuration", () => {
+ expect(
+ callForHelmChartValuesMock,
+ ).not.toHaveBeenCalled();
+ });
+
+ it("does not call for available versions", () => {
+ expect(
+ callForHelmChartVersionsMock,
+ ).not.toHaveBeenCalled();
+ });
+
+ it("does not show spinner", () => {
+ expect(
+ rendered.queryByTestId("install-chart-tab-spinner"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("when installing the first chart, calls for installation of first chart", () => {
+ const installButton = rendered.getByTestId(
+ "install-chart-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(installButton);
+
+ expect(
+ callForCreateHelmReleaseMock,
+ ).toHaveBeenCalledWith({
+ chart: "some-name",
+ name: undefined,
+ namespace: "default",
+ repo: "some-repository",
+ values: "some-default-configuration",
+ version: "some-version",
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe("given changing version to be installed", () => {
+ let menu: { selectOption: (labelText: string) => void };
+
+ beforeEach(() => {
+ callForHelmChartVersionsMock.mockClear();
+ callForHelmChartValuesMock.mockClear();
+
+ const menuId =
+ "install-chart-version-select-for-some-first-tab-id";
+
+ menu = builder.select.openMenu(menuId);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when version is selected", () => {
+ let installButton: HTMLButtonElement;
+
+ beforeEach(() => {
+ installButton = rendered.getByTestId(
+ "install-chart-from-tab-for-some-first-tab-id",
+ ) as HTMLButtonElement;
+
+ menu.selectOption("some-other-version");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("calls for default configuration for the version of chart", () => {
+ expect(callForHelmChartValuesMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-name",
+ "some-other-version",
+ );
+ });
+
+ it("shows spinner", () => {
+ expect(
+ rendered.getByTestId(
+ "install-chart-configuration-spinner",
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("does not call for versions again", () => {
+ expect(
+ callForHelmChartVersionsMock,
+ ).not.toHaveBeenCalled();
+ });
+
+ it("install button is disabled", () => {
+ expect(installButton).toHaveAttribute("disabled");
+ });
+
+ it("stores the selected version", async () => {
+ const readJsonFile = rendererDi.inject(readJsonFileInjectable);
+
+ const actual = await readJsonFile(
+ "/some-directory-for-lens-local-storage/some-cluster-id.json",
+ ) as any;
+
+ const version = actual.install_charts["some-first-tab-id"].version;
+
+ expect(version).toBe("some-other-version");
+ });
+
+ describe("when default configuration resolves", () => {
+ beforeEach(async () => {
+ await callForHelmChartValuesMock.resolve(
+ "some-default-configuration-for-other-version",
+ );
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show spinner", () => {
+ expect(
+ rendered.queryByTestId(
+ "install-chart-configuration-spinner",
+ ),
+ ).not.toBeInTheDocument();
+ });
+
+ it("install button is enabled", () => {
+ expect(installButton).not.toHaveAttribute("disabled");
+ });
+
+ it("when installing the chart, calls for installation with changed version and default configuration", () => {
+ fireEvent.click(installButton);
+
+ expect(
+ callForCreateHelmReleaseMock,
+ ).toHaveBeenCalledWith({
+ chart: "some-name",
+ name: undefined,
+ namespace: "default",
+ repo: "some-repository",
+ values:
+ "some-default-configuration-for-other-version",
+ version: "some-other-version",
+ });
+ });
+ });
+ });
+ });
+
+ describe("given namespace selection is opened", () => {
+ let menu: { selectOption: (labelText: string) => void };
+
+ beforeEach(() => {
+ const menuId =
+ "install-chart-namespace-select-for-some-first-tab-id";
+
+ menu = builder.select.openMenu(menuId);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when namespace is selected", () => {
+ beforeEach(() => {
+ menu.selectOption("some-other-namespace");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("stores the selected namespace", async () => {
+ const readJsonFile = rendererDi.inject(readJsonFileInjectable);
+
+ const actual = await readJsonFile(
+ "/some-directory-for-lens-local-storage/some-cluster-id.json",
+ ) as any;
+
+ const namespace = actual.install_charts["some-first-tab-id"].namespace;
+
+ expect(namespace).toBe("some-other-namespace");
+ });
+
+ it("when installing the chart, calls for installation with changed namespace", () => {
+ const installButton = rendered.getByTestId(
+ "install-chart-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(installButton);
+
+ expect(callForCreateHelmReleaseMock).toHaveBeenCalledWith(
+ {
+ chart: "some-name",
+ name: undefined,
+ namespace: "some-other-namespace",
+ repo: "some-repository",
+ values: "some-default-configuration",
+ version: "some-version",
+ },
+ );
+ });
+ });
+ });
+
+ describe("given invalid change in configuration", () => {
+ let installButton: HTMLButtonElement;
+ let input: HTMLInputElement;
+
+ beforeEach(() => {
+ installButton = rendered.getByTestId(
+ "install-chart-from-tab-for-some-first-tab-id",
+ ) as HTMLButtonElement;
+
+ 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("updates the editor with the changed value", () => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ );
+
+ expect(input).toHaveValue("@some-invalid-configuration@");
+ });
+
+ it("install button is disabled", () => {
+ expect(installButton).toHaveAttribute("disabled");
+ });
+
+ it("when valid change in configuration, install button is enabled", () => {
+ fireEvent.change(input, {
+ target: { value: "some-valid-configuration" },
+ });
+
+ expect(installButton).not.toHaveAttribute("disabled");
+ });
+
+ it("given change in version, when default configuration resolves, install button is enabled", async () => {
+ builder.select
+ .openMenu(
+ "install-chart-version-select-for-some-first-tab-id",
+ )
+ .selectOption("some-other-version");
+
+ await callForHelmChartValuesMock.resolve(
+ "some-default-configuration-for-other-version",
+ );
+
+ expect(installButton).not.toHaveAttribute("disabled");
+ });
+ });
+
+ describe("given valid change in configuration", () => {
+ beforeEach(() => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ );
+
+ fireEvent.change(input, {
+ target: { value: "some-valid-configuration" },
+ });
+ });
+
+ it("updates the editor with the changed value", () => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ );
+
+ expect(input).toHaveValue("some-valid-configuration");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ 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;
+
+ const configuration = actual.install_charts["some-first-tab-id"].values;
+
+ expect(configuration).toBe("some-valid-configuration");
+ });
+
+ it("does not show spinner", () => {
+ expect(
+ rendered.queryByTestId("install-chart-tab-spinner"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("when installing the chart, calls for installation with changed configuration", () => {
+ const installButton = rendered.getByTestId(
+ "install-chart-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(installButton);
+
+ expect(callForCreateHelmReleaseMock).toHaveBeenCalledWith({
+ chart: "some-name",
+ name: undefined,
+ namespace: "default",
+ repo: "some-repository",
+ values: "some-valid-configuration",
+ version: "some-version",
+ });
+ });
+
+ it("given version is changed, when default configuration resolves, defaults back to default configuration", async () => {
+ builder.select
+ .openMenu(
+ "install-chart-version-select-for-some-first-tab-id",
+ )
+ .selectOption("some-other-version");
+
+ await callForHelmChartValuesMock.resolve(
+ "some-default-configuration-for-other-version",
+ );
+
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ );
+
+ expect(input).toHaveValue(
+ "some-default-configuration-for-other-version",
+ );
+ });
+ });
+
+ describe("given custom name is inputted", () => {
+ beforeEach(() => {
+ const input = rendered.getByTestId(
+ "install-chart-custom-name-input-for-some-first-tab-id",
+ );
+
+ fireEvent.change(input, {
+ target: { value: "some-custom-name" },
+ });
+
+ });
+
+ it("stores the changed custom name", async () => {
+ const readJsonFile = rendererDi.inject(readJsonFileInjectable);
+
+ const actual = await readJsonFile(
+ "/some-directory-for-lens-local-storage/some-cluster-id.json",
+ ) as any;
+
+ const customName = actual.install_charts["some-first-tab-id"].releaseName;
+
+ expect(customName).toBe("some-custom-name");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("when installed, calls for installation with custom name", () => {
+ const installButton = rendered.getByTestId(
+ "install-chart-from-tab-for-some-first-tab-id",
+ );
+
+ fireEvent.click(installButton);
+
+ expect(callForCreateHelmReleaseMock).toHaveBeenCalledWith({
+ chart: "some-name",
+ name: "some-custom-name",
+ namespace: "default",
+ repo: "some-repository",
+ values: "some-default-configuration",
+ version: "some-version",
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/behaviours/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts b/src/behaviours/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts
new file mode 100644
index 0000000000..1fdde9130c
--- /dev/null
+++ b/src/behaviours/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts
@@ -0,0 +1,260 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import type { RenderResult } 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 { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
+import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable";
+import type { CallForHelmChartValues } from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable";
+import callForHelmChartValuesInjectable from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable";
+import namespaceStoreInjectable from "../../../renderer/components/+namespaces/store.injectable";
+import type { NamespaceStore } from "../../../renderer/components/+namespaces/store";
+import type { CallForHelmChartVersions } from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
+import callForHelmChartVersionsInjectable from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
+import { overrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
+import writeJsonFileInjectable from "../../../common/fs/write-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 { TabKind } from "../../../renderer/components/dock/dock/store";
+import { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready";
+import type { DiContainer } from "@ogre-tools/injectable";
+import callForCreateHelmReleaseInjectable from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable";
+
+// TODO: Make tooltips free of side effects by making it deterministic
+jest.mock("../../../renderer/components/tooltip/withTooltip", () => ({
+ withTooltip: (target: any) => target,
+}));
+
+describe("installing helm chart from previously opened tab", () => {
+ let builder: ApplicationBuilder;
+ let rendererDi: DiContainer;
+ let callForHelmChartVersionsMock: AsyncFnMock;
+ let callForHelmChartValuesMock: AsyncFnMock;
+ let storagesAreReady: () => Promise;
+
+ beforeEach(() => {
+ builder = getApplicationBuilder();
+
+ rendererDi = builder.dis.rendererDi;
+
+ overrideFsWithFakes(rendererDi);
+
+ callForHelmChartVersionsMock = asyncFn();
+ callForHelmChartValuesMock = asyncFn();
+
+ builder.beforeApplicationStart(({ rendererDi }) => {
+ rendererDi.override(
+ directoryForLensLocalStorageInjectable,
+ () => "/some-directory-for-lens-local-storage",
+ );
+
+ rendererDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
+
+ storagesAreReady = controlWhenStoragesAreReady(rendererDi);
+
+ rendererDi.override(
+ callForHelmChartVersionsInjectable,
+ () => callForHelmChartVersionsMock,
+ );
+
+ rendererDi.override(
+ callForHelmChartValuesInjectable,
+ () => callForHelmChartValuesMock,
+ );
+
+ rendererDi.override(
+ callForHelmChartValuesInjectable,
+ () => callForHelmChartValuesMock,
+ );
+
+ rendererDi.override(
+ callForCreateHelmReleaseInjectable,
+ () => jest.fn(),
+ );
+
+ // TODO: Replace store mocking with mock for the actual side-effect (where the namespaces are coming from)
+ rendererDi.override(
+ namespaceStoreInjectable,
+ () =>
+ ({
+ contextNamespaces: [],
+ items: [
+ { getName: () => "default" },
+ { getName: () => "some-other-namespace" },
+ ],
+ selectNamespaces: () => {},
+ } as unknown as NamespaceStore),
+ );
+
+ rendererDi.override(getRandomInstallChartTabIdInjectable, () =>
+ jest
+ .fn(() => "some-irrelevant-tab-id")
+ .mockReturnValueOnce("some-first-tab-id"),
+ );
+ });
+
+ builder.setEnvironmentToClusterFrame();
+ });
+
+ describe("given tab for installing chart was previously opened, when application is started", () => {
+ let rendered: RenderResult;
+
+ beforeEach(async () => {
+ const writeJsonFile = rendererDi.inject(writeJsonFileInjectable);
+
+ writeJsonFile(
+ "/some-directory-for-lens-local-storage/some-cluster-id.json",
+ {
+ dock: {
+ height: 300,
+ tabs: [
+ {
+ id: "some-first-tab-id",
+ kind: TabKind.INSTALL_CHART,
+ title: "Helm Install: some-repository/some-name",
+ pinned: false,
+ },
+ ],
+
+ isOpen: true,
+ },
+
+ install_charts: {
+ "some-first-tab-id": {
+ name: "some-name",
+ repo: "some-repository",
+ version: "some-other-version",
+ values: "some-stored-configuration",
+ releaseName: "some-stored-custom-name",
+ namespace: "some-other-namespace",
+ },
+ },
+ },
+ );
+
+ rendered = await builder.render();
+
+ await storagesAreReady();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("still has the dock tab for installing chart", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("shows dock tab for installing the chart", () => {
+ expect(
+ rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("shows spinner in dock tab", () => {
+ expect(
+ rendered.getByTestId("install-chart-tab-spinner"),
+ ).toBeInTheDocument();
+ });
+
+ it("calls for default configuration of the chart", () => {
+ expect(callForHelmChartValuesMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-name",
+ "some-other-version",
+ );
+ });
+
+ it("calls for available versions", () => {
+ expect(callForHelmChartVersionsMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-name",
+ );
+ });
+
+ describe("when configuration and version resolves", () => {
+ beforeEach(async () => {
+ await callForHelmChartValuesMock.resolve(
+ "some-default-configuration",
+ );
+
+ await callForHelmChartVersionsMock.resolve([
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-other-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+ ]);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("has the stored configuration", () => {
+ const input = rendered.getByTestId(
+ "monaco-editor-for-some-first-tab-id",
+ );
+
+ expect(input).toHaveValue("some-stored-configuration");
+ });
+
+ it("has the stored custom name", () => {
+ const input = rendered.getByTestId(
+ "install-chart-custom-name-input-for-some-first-tab-id",
+ );
+
+ expect(input).toHaveValue("some-stored-custom-name");
+ });
+
+ it("has the stored version", () => {
+ const actual = builder.select.getValue(
+ "install-chart-version-select-for-some-first-tab-id",
+ );
+
+ expect(actual).toBe("some-other-version");
+
+ });
+
+ it("has the stored namespace", () => {
+ const actual = builder.select.getValue(
+ "install-chart-namespace-select-for-some-first-tab-id",
+ );
+
+ expect(actual).toBe("some-other-namespace");
+ });
+ });
+ });
+});
diff --git a/src/behaviours/helm-charts/installing-chart/opening-dock-tab-for-installing-helm-chart.test.ts b/src/behaviours/helm-charts/installing-chart/opening-dock-tab-for-installing-helm-chart.test.ts
new file mode 100644
index 0000000000..79ad98a1dc
--- /dev/null
+++ b/src/behaviours/helm-charts/installing-chart/opening-dock-tab-for-installing-helm-chart.test.ts
@@ -0,0 +1,361 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+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 type { CallForHelmCharts } from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable";
+import callForHelmChartsInjectable from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable";
+import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
+import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable";
+import callForHelmChartValuesInjectable from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable";
+import callForCreateHelmReleaseInjectable from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable";
+import type { CallForHelmChartReadme } from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable";
+import callForHelmChartReadmeInjectable from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable";
+import type { CallForHelmChartVersions } from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
+import callForHelmChartVersionsInjectable from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
+import { flushPromises } from "../../../common/test-utils/flush-promises";
+import { overrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
+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 dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
+import type { DiContainer } from "@ogre-tools/injectable";
+
+// TODO: Make tooltips free of side effects by making it deterministic
+jest.mock("../../../renderer/components/tooltip/withTooltip", () => ({
+ withTooltip: (target: any) => target,
+}));
+
+describe("opening dock tab for installing helm chart", () => {
+ let builder: ApplicationBuilder;
+ let rendererDi: DiContainer;
+ let callForHelmChartsMock: AsyncFnMock;
+ let callForHelmChartVersionsMock: AsyncFnMock;
+ let callForHelmChartReadmeMock: AsyncFnMock;
+ let callForHelmChartValuesMock: jest.Mock;
+
+ beforeEach(() => {
+ builder = getApplicationBuilder();
+
+ rendererDi = builder.dis.rendererDi;
+
+ overrideFsWithFakes(rendererDi);
+
+ callForHelmChartsMock = asyncFn();
+ callForHelmChartVersionsMock = asyncFn();
+ callForHelmChartReadmeMock = asyncFn();
+ callForHelmChartValuesMock = jest.fn();
+
+ builder.beforeApplicationStart(({ rendererDi }) => {
+ rendererDi.override(
+ directoryForLensLocalStorageInjectable,
+ () => "/some-directory-for-lens-local-storage",
+ );
+
+ rendererDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
+
+ rendererDi.override(
+ callForHelmChartsInjectable,
+ () => callForHelmChartsMock,
+ );
+
+ rendererDi.override(
+ callForHelmChartVersionsInjectable,
+ () => callForHelmChartVersionsMock,
+ );
+
+ rendererDi.override(
+ callForHelmChartReadmeInjectable,
+ () => callForHelmChartReadmeMock,
+ );
+
+ rendererDi.override(
+ callForHelmChartValuesInjectable,
+ () => callForHelmChartValuesMock,
+ );
+
+ rendererDi.override(
+ callForCreateHelmReleaseInjectable,
+ () => jest.fn(),
+ );
+
+ rendererDi.override(getRandomInstallChartTabIdInjectable, () =>
+ jest
+ .fn(() => "some-irrelevant-tab-id")
+ .mockReturnValueOnce("some-tab-id"),
+ );
+ });
+
+ builder.setEnvironmentToClusterFrame();
+ });
+
+ describe("given application is started, when navigating to helm charts", () => {
+ let rendered: RenderResult;
+
+ beforeEach(async () => {
+ rendered = await builder.render();
+
+ builder.helmCharts.navigate();
+
+ const dockStore = rendererDi.inject(dockStoreInjectable);
+
+ // TODO: Make TerminalWindow unit testable to allow realistic behaviour
+ dockStore.closeTab("terminal");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("calls for charts", () => {
+ expect(callForHelmChartsMock).toHaveBeenCalled();
+ });
+
+ describe("when charts resolve", () => {
+ beforeEach(async () => {
+ await callForHelmChartsMock.resolve([
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-other-name",
+ version: "some-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+ ]);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when opening details of a chart", () => {
+ beforeEach(() => {
+ const row = rendered.getByTestId(
+ "helm-chart-row-for-some-repository-some-name",
+ );
+
+ fireEvent.click(row);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("calls for chart versions", () => {
+ expect(callForHelmChartVersionsMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-name",
+ );
+ });
+
+ it("shows spinner", () => {
+ expect(
+ rendered.getByTestId("spinner-for-chart-details"),
+ ).toBeInTheDocument();
+ });
+
+ describe("when chart versions resolve", () => {
+ beforeEach(async () => {
+ await callForHelmChartVersionsMock.resolve([
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+
+ HelmChart.create({
+ apiVersion: "some-api-version",
+ name: "some-name",
+ version: "some-other-version",
+ repo: "some-repository",
+ created: "2015-10-21T07:28:00Z",
+ description: "some-description",
+ keywords: [],
+ sources: [],
+ urls: [],
+ annotations: {},
+ dependencies: [],
+ maintainers: [],
+ deprecated: false,
+ }),
+ ]);
+ });
+
+ it("calls for chart readme for the version", () => {
+ expect(callForHelmChartReadmeMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-name",
+ "some-version",
+ );
+ });
+
+ it("has the latest version as selected", () => {
+ const actual = builder.select.getValue(
+ "helm-chart-version-selector-some-repository-some-name",
+ );
+
+ expect(actual).toBe("some-version");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not shows spinner for details", () => {
+ expect(
+ rendered.queryByTestId("spinner-for-chart-details"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("shows spinner for readme", () => {
+ expect(
+ rendered.getByTestId("spinner-for-chart-readme"),
+ ).toBeInTheDocument();
+ });
+
+ describe("when readme resolves", () => {
+ beforeEach(async () => {
+ await callForHelmChartReadmeMock.resolve("some-readme");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show spinner anymore", () => {
+ expect(
+ rendered.queryByTestId("spinner-for-chart-readme"),
+ ).not.toBeInTheDocument();
+ });
+
+ describe("when selecting different version", () => {
+ beforeEach(() => {
+ callForHelmChartReadmeMock.mockClear();
+
+ builder.select
+ .openMenu(
+ "helm-chart-version-selector-some-repository-some-name",
+ )
+ .selectOption("some-other-version");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("selects the version", () => {
+ const actual = builder.select.getValue(
+ "helm-chart-version-selector-some-repository-some-name",
+ );
+
+ expect(actual).toBe("some-other-version");
+ });
+
+ it("calls for chart readme for the version", () => {
+ expect(callForHelmChartReadmeMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-name",
+ "some-other-version",
+ );
+ });
+
+ describe("when readme resolves", () => {
+ beforeEach(async () => {
+ await callForHelmChartReadmeMock.resolve("some-readme");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("when selecting to install chart, calls for the default configuration of the chart with specific version", async () => {
+ const installButton = rendered.getByTestId(
+ "install-chart-for-some-repository-some-name",
+ );
+
+ fireEvent.click(installButton);
+
+ await flushPromises();
+
+ expect(callForHelmChartValuesMock).toHaveBeenCalledWith(
+ "some-repository",
+ "some-name",
+ "some-other-version",
+ );
+ });
+ });
+ });
+
+ describe("when selecting to install the chart", () => {
+ beforeEach(() => {
+ callForHelmChartVersionsMock.mockClear();
+
+ const installButton = rendered.getByTestId(
+ "install-chart-for-some-repository-some-name",
+ );
+
+ fireEvent.click(installButton);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("has the dock tab for installing chart", () => {
+ expect(
+ rendered.getByTestId("dock-tab-for-some-tab-id"),
+ ).toBeInTheDocument();
+ });
+
+ it("shows dock tab for installing chart", () => {
+ expect(
+ rendered.getByTestId(
+ "dock-tab-content-for-some-tab-id",
+ ),
+ ).toBeInTheDocument();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/behaviours/helm-charts/navigation-to-helm-charts.test.ts b/src/behaviours/helm-charts/navigation-to-helm-charts.test.ts
deleted file mode 100644
index c539db173a..0000000000
--- a/src/behaviours/helm-charts/navigation-to-helm-charts.test.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import type { RenderResult } 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";
-
-describe("helm-charts - navigation to Helm charts", () => {
- let applicationBuilder: ApplicationBuilder;
-
- beforeEach(() => {
- applicationBuilder = getApplicationBuilder();
- });
-
- describe("when navigating to Helm charts", () => {
- let rendered: RenderResult;
-
- beforeEach(async () => {
- applicationBuilder.setEnvironmentToClusterFrame();
-
- rendered = await applicationBuilder.render();
-
- applicationBuilder.helmCharts.navigate();
- });
-
- it("renders", () => {
- expect(rendered.container).toMatchSnapshot();
- });
-
- it("shows page for Helm charts", () => {
- const page = rendered.getByTestId("page-for-helm-charts");
-
- expect(page).not.toBeNull();
- });
- });
-});
diff --git a/src/common/utils/get-random-id.injectable.ts b/src/common/utils/get-random-id.injectable.ts
index 3b96c50633..b6d8b99f41 100644
--- a/src/common/utils/get-random-id.injectable.ts
+++ b/src/common/utils/get-random-id.injectable.ts
@@ -7,7 +7,7 @@ import { v4 as getRandomId } from "uuid";
const getRandomIdInjectable = getInjectable({
id: "get-random-id",
- instantiate: () => getRandomId,
+ instantiate: () => () => getRandomId(),
causesSideEffects: true,
});
diff --git a/src/jest.setup.ts b/src/jest.setup.ts
index 6e7ef2fc85..481a56ab1a 100644
--- a/src/jest.setup.ts
+++ b/src/jest.setup.ts
@@ -36,3 +36,11 @@ process.on("unhandledRejection", (err: any) => {
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoderNode as unknown as typeof TextDecoder;
+
+global.ResizeObserver = class {
+ observe = () => {};
+ unobserve = () => {};
+ disconnect = () => {};
+};
+
+jest.mock("./renderer/components/monaco-editor/monaco-editor");
diff --git a/src/main/routes/helm/releases/install-chart-route.injectable.ts b/src/main/routes/helm/releases/install-chart-route.injectable.ts
index a585443193..75969c4a2f 100644
--- a/src/main/routes/helm/releases/install-chart-route.injectable.ts
+++ b/src/main/routes/helm/releases/install-chart-route.injectable.ts
@@ -18,8 +18,7 @@ const installChartArgsValidator = Joi.object before getChartDetails resolves renders 1`] = `
-
-
-
-
-
-
- Chart: a galaxy far far away/a name
-
-
- content_copy
-
-
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-`;
-
-exports[` before getChartDetails resolves when getChartDetails resolves with one version renders 1`] = `
-
-
-
-
-
-
- Chart: a galaxy far far away/a name
-
-
- content_copy
-
-
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Install
-
-
-
-
-
-
- Maintainers
-
-
-
-
-
-
-
-
- I am a readme
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[` before getChartDetails resolves with getChartDetails rejects renders 1`] = `
-
-
-
-
-
-
-
- info_outline
-
-
-
-
- Error: some error
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-
- Chart: a galaxy far far away/a name
-
-
- content_copy
-
-
-
-
-
-
- close
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/renderer/components/+helm-charts/details/readme-of-selected-helm-chart.injectable.ts b/src/renderer/components/+helm-charts/details/readme-of-selected-helm-chart.injectable.ts
new file mode 100644
index 0000000000..21e8bcf7a9
--- /dev/null
+++ b/src/renderer/components/+helm-charts/details/readme-of-selected-helm-chart.injectable.ts
@@ -0,0 +1,38 @@
+/**
+ * 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 { asyncComputed } from "@ogre-tools/injectable-react";
+import callForHelmChartReadmeInjectable from "./readme/call-for-helm-chart-readme.injectable";
+import helmChartDetailsVersionSelectionInjectable from "./versions/helm-chart-details-version-selection.injectable";
+import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
+
+const readmeOfSelectedHelmChartInjectable = getInjectable({
+ id: "readme-of-selected-helm-chart",
+
+ instantiate: (di, chart: HelmChart) => {
+ const selection = di.inject(helmChartDetailsVersionSelectionInjectable, chart);
+ const callForHelmChartReadme = di.inject(callForHelmChartReadmeInjectable);
+
+ return asyncComputed(async () => {
+ const chartVersion = selection.value.get();
+
+ if (!chartVersion) {
+ return "";
+ }
+
+ return await callForHelmChartReadme(
+ chartVersion.getRepository(),
+ chartVersion.getName(),
+ chartVersion.getVersion(),
+ );
+ }, "");
+ },
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (di, chart: HelmChart) => chart.getId(),
+ }),
+});
+
+export default readmeOfSelectedHelmChartInjectable;
diff --git a/src/renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable.ts b/src/renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable.ts
new file mode 100644
index 0000000000..ae9c99edeb
--- /dev/null
+++ b/src/renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable.ts
@@ -0,0 +1,29 @@
+/**
+ * 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 { getChartDetails } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
+
+export type CallForHelmChartReadme = (
+ repo: string,
+ name: string,
+ version: string,
+) => Promise;
+
+const callForHelmChartReadmeInjectable = getInjectable({
+ id: "call-for-helm-chart-readme",
+
+ instantiate:
+ (): CallForHelmChartReadme =>
+ async (repository: string, name: string, version: string) => {
+ // TODO: Dismantle wrong abstraction
+ const details = await getChartDetails(repository, name, { version });
+
+ return details.readme;
+ },
+
+ causesSideEffects: true,
+});
+
+export default callForHelmChartReadmeInjectable;
diff --git a/src/renderer/components/+helm-charts/details/versions-of-selected-helm-chart.injectable.ts b/src/renderer/components/+helm-charts/details/versions-of-selected-helm-chart.injectable.ts
new file mode 100644
index 0000000000..14ea00fce2
--- /dev/null
+++ b/src/renderer/components/+helm-charts/details/versions-of-selected-helm-chart.injectable.ts
@@ -0,0 +1,28 @@
+/**
+ * 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 { asyncComputed } from "@ogre-tools/injectable-react";
+import callForHelmChartVersionsInjectable from "./versions/call-for-helm-chart-versions.injectable";
+import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
+
+const versionsOfSelectedHelmChartInjectable = getInjectable({
+ id: "versions-of-selected-helm-chart",
+
+ instantiate: (di, chart: HelmChart) => {
+ const callForHelmChartVersions = di.inject(callForHelmChartVersionsInjectable);
+
+ return asyncComputed(
+ async () =>
+ await callForHelmChartVersions(chart.getRepository(), chart.getName()),
+ [],
+ );
+ },
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (di, chart: HelmChart) => chart.getId(),
+ }),
+});
+
+export default versionsOfSelectedHelmChartInjectable;
diff --git a/src/renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable.ts b/src/renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable.ts
new file mode 100644
index 0000000000..a20f5b60e9
--- /dev/null
+++ b/src/renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable.ts
@@ -0,0 +1,28 @@
+/**
+ * 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 { HelmChart } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
+import { getChartDetails } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
+
+export type CallForHelmChartVersions = (
+ repo: string,
+ name: string
+) => Promise;
+
+const callForHelmChartVersionsInjectable = getInjectable({
+ id: "call-for-helm-chart-versions",
+
+ instantiate:
+ (): CallForHelmChartVersions => async (repository: string, name: string) => {
+ // TODO: Dismantle wrong abstraction
+ const details = await getChartDetails(repository, name);
+
+ return details.versions;
+ },
+
+ causesSideEffects: true,
+});
+
+export default callForHelmChartVersionsInjectable;
diff --git a/src/renderer/components/+helm-charts/details/versions/helm-chart-details-version-selection.injectable.ts b/src/renderer/components/+helm-charts/details/versions/helm-chart-details-version-selection.injectable.ts
new file mode 100644
index 0000000000..b3be751b00
--- /dev/null
+++ b/src/renderer/components/+helm-charts/details/versions/helm-chart-details-version-selection.injectable.ts
@@ -0,0 +1,59 @@
+/**
+ * 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 { IComputedValue } from "mobx";
+import { computed, observable } from "mobx";
+import versionsOfSelectedHelmChartInjectable from "../versions-of-selected-helm-chart.injectable";
+import type { HelmChart } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
+import type { SingleValue } from "react-select";
+
+interface VersionSelectionOption {
+ label: string;
+ value: HelmChart;
+}
+
+export interface HelmChartDetailsVersionSelection {
+ value: IComputedValue;
+ options: IComputedValue;
+ onChange: (option: SingleValue) => void;
+}
+
+const helmChartDetailsVersionSelectionInjectable = getInjectable({
+ id: "helm-chart-details-version-selection",
+
+ instantiate: (di, chart: HelmChart): HelmChartDetailsVersionSelection => {
+ const versionsOfSelectedHelmChart = di.inject(
+ versionsOfSelectedHelmChartInjectable,
+ chart,
+ );
+
+ const state = observable.box();
+
+ return {
+ value: computed(
+ () => state.get() || versionsOfSelectedHelmChart.value.get()[0],
+ ),
+
+ options: computed(() =>
+ versionsOfSelectedHelmChart.value.get().map((chartVersion) => ({
+ label: chartVersion.version,
+ value: chartVersion,
+ })),
+ ),
+
+ onChange: (option) => {
+ if (option) {
+ state.set(option.value);
+ }
+ },
+ };
+ },
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (di, chart: HelmChart) => chart.getId(),
+ }),
+});
+
+export default helmChartDetailsVersionSelectionInjectable;
diff --git a/src/renderer/components/+helm-charts/helm-chart-details.test.tsx b/src/renderer/components/+helm-charts/helm-chart-details.test.tsx
deleted file mode 100644
index 4af6543a9d..0000000000
--- a/src/renderer/components/+helm-charts/helm-chart-details.test.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-
-import type { AsyncFnMock } from "@async-fn/jest";
-import asyncFn from "@async-fn/jest";
-import type { DiContainer } from "@ogre-tools/injectable";
-import type { RenderResult } from "@testing-library/react";
-import React from "react";
-import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
-import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
-import { getDiForUnitTesting } from "../../getDiForUnitTesting";
-import { noop } from "../../utils";
-import type { CreateInstallChartTab } from "../dock/install-chart/create-install-chart-tab.injectable";
-import createInstallChartTabInjectable from "../dock/install-chart/create-install-chart-tab.injectable";
-import { Notifications } from "../notifications";
-import type { DiRender } from "../test-utils/renderFor";
-import { renderFor } from "../test-utils/renderFor";
-import type { GetChartDetails } from "./get-char-details.injectable";
-import getChartDetailsInjectable from "./get-char-details.injectable";
-import { HelmChartDetails } from "./helm-chart-details";
-
-describe(" ", () => {
- let di: DiContainer;
- let getChartDetails: AsyncFnMock;
- let chart: HelmChart;
- let render: DiRender;
- let result: RenderResult;
- let createInstallChartTab: jest.MockedFunction;
-
- beforeEach(() => {
- di = getDiForUnitTesting({ doGeneralOverrides: true });
- getChartDetails = asyncFn();
- createInstallChartTab = jest.fn();
- chart = HelmChart.create({
- apiVersion: "some-api-version",
- created: "a long time ago",
- name: "a name",
- repo: "a galaxy far far away",
- version: "1",
- });
-
- di.override(directoryForLensLocalStorageInjectable, () => "some-directory-for-lens-local-storage");
- di.override(getChartDetailsInjectable, () => getChartDetails);
- di.override(createInstallChartTabInjectable, () => createInstallChartTab);
- render = renderFor(di);
- result = render((
- <>
-
-
- >
- ));
- });
-
- describe("before getChartDetails resolves", () => {
- it("renders", () => {
- expect(result.baseElement).toMatchSnapshot();
- });
-
- describe("when getChartDetails resolves with one version", () => {
- beforeEach(async () => {
- await getChartDetails.resolve({
- readme: "I am a readme",
- versions: [chart],
- });
- });
-
- it("renders", () => {
- expect(result.baseElement).toMatchSnapshot();
- });
-
- it("shows the readme", () => {
- expect(result.queryByTestId("helmchart-readme")).not.toBeNull();
- });
-
- it("shows the selected chart", () => {
- expect(result.queryByTestId("selected-chart-description")).not.toBeNull();
- });
- });
-
- describe("with getChartDetails rejects", () => {
- beforeEach(async () => {
- await getChartDetails.reject(new Error("some error"));
- });
-
- it("renders", () => {
- expect(result.baseElement).toMatchSnapshot();
- });
- });
- });
-});
diff --git a/src/renderer/components/+helm-charts/helm-chart-details.tsx b/src/renderer/components/+helm-charts/helm-chart-details.tsx
index e33f2836ae..d15552dedb 100644
--- a/src/renderer/components/+helm-charts/helm-chart-details.tsx
+++ b/src/renderer/components/+helm-charts/helm-chart-details.tsx
@@ -7,8 +7,7 @@ import "./helm-chart-details.scss";
import React, { Component } from "react";
import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
-import { computed, observable, reaction, runInAction } from "mobx";
-import { disposeOnUnmount, observer } from "mobx-react";
+import { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer";
import { autoBind, stopPropagation } from "../../utils";
import { MarkdownViewer } from "../markdown-viewer";
@@ -17,19 +16,19 @@ import { Button } from "../button";
import { Select } from "../select";
import { Badge } from "../badge";
import { Tooltip, withStyles } from "@material-ui/core";
+import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import createInstallChartTabInjectable from "../dock/install-chart/create-install-chart-tab.injectable";
-import type { ShowCheckedErrorNotification } from "../notifications/show-checked-error.injectable";
-import type { SingleValue } from "react-select";
-import AbortController from "abort-controller";
-import showCheckedErrorNotificationInjectable from "../notifications/show-checked-error.injectable";
-import type { GetChartDetails } from "./get-char-details.injectable";
-import getChartDetailsInjectable from "./get-char-details.injectable";
import { HelmChartIcon } from "./icon";
+import readmeOfSelectHelmChartInjectable from "./details/readme-of-selected-helm-chart.injectable";
+import versionsOfSelectedHelmChartInjectable from "./details/versions-of-selected-helm-chart.injectable";
+import type { HelmChartDetailsVersionSelection } from "./details/versions/helm-chart-details-version-selection.injectable";
+import helmChartDetailsVersionSelectionInjectable from "./details/versions/helm-chart-details-version-selection.injectable";
+import assert from "assert";
export interface HelmChartDetailsProps {
- chart: HelmChart;
hideDetails(): void;
+ chart: HelmChart;
}
const LargeTooltip = withStyles({
@@ -40,85 +39,34 @@ const LargeTooltip = withStyles({
interface Dependencies {
createInstallChartTab: (helmChart: HelmChart) => void;
- showCheckedErrorNotification: ShowCheckedErrorNotification;
- getChartDetails: GetChartDetails;
+ versions: IAsyncComputed;
+ readme: IAsyncComputed;
+ versionSelection: HelmChartDetailsVersionSelection;
}
@observer
class NonInjectedHelmChartDetails extends Component {
- readonly chartVersions = observable.array();
- readonly selectedChart = observable.box();
- readonly readme = observable.box(undefined);
- readonly chartVerionOptions = computed(() => (
- this.chartVersions.map(chart => ({
- value: chart,
- label: chart.version,
- }))
- ));
-
- private abortController = new AbortController();
-
constructor(props: HelmChartDetailsProps & Dependencies) {
super(props);
autoBind(this);
}
- componentWillUnmount() {
- this.abortController.abort();
+ get chart() {
+ return this.props.chart;
}
- componentDidMount() {
- disposeOnUnmount(this, [
- reaction(() => this.props.chart, async ({ name, repo, version }) => {
- runInAction(() => {
- this.selectedChart.set(undefined);
- this.chartVersions.clear();
- this.readme.set("");
- });
+ install() {
+ const chart = this.props.versionSelection.value.get();
- try {
- const { readme, versions } = await this.props.getChartDetails(repo, name, { version });
+ assert(chart);
- runInAction(() => {
- this.readme.set(readme);
- this.chartVersions.replace(versions);
- this.selectedChart.set(versions[0]);
- });
- } catch (error) {
- this.props.showCheckedErrorNotification(error, "Unknown error occured while getting chart details");
- }
- }, {
- fireImmediately: true,
- }),
- ]);
- }
-
- async onVersionChange(option: SingleValue<{ value: HelmChart }>) {
- const chart = option?.value ?? this.chartVersions[0];
-
- runInAction(() => {
- this.selectedChart.set(chart ?? undefined);
- this.readme.set(undefined);
- });
-
- try {
- this.abortController.abort();
- this.abortController = new AbortController();
- const { chart: { name, repo }} = this.props;
- const { readme } = await this.props.getChartDetails(repo, name, { version: chart.version, reqInit: { signal: this.abortController.signal }});
-
- this.readme.set(readme);
- } catch (error) {
- this.props.showCheckedErrorNotification(error, "Unknown error occured while getting chart details");
- }
- }
-
- install(selectedChart: HelmChart) {
- this.props.createInstallChartTab(selectedChart);
+ this.props.createInstallChartTab(chart);
this.props.hideDetails();
}
renderIntroduction(selectedChart: HelmChart) {
+ const testId = selectedChart.getFullName("-");
+
return (
this.install(selectedChart)}
+ onClick={this.install}
+ data-testid={`install-chart-for-${testId}`}
/>
(
chart.deprecated
? (
@@ -154,8 +103,8 @@ class NonInjectedHelmChartDetails extends Component chart.deprecated}
- value={selectedChart}
- onChange={this.onVersionChange}
+ value={this.props.versionSelection.value.get()}
+ onChange={this.props.versionSelection.onChange}
/>
@@ -190,44 +139,42 @@ class NonInjectedHelmChartDetails extends Component ;
- }
-
return (
-
+
);
}
renderContent() {
- const selectedChart = this.selectedChart.get();
+ const readmeIsLoading = this.props.readme.pending.get();
+ const versionsAreLoading = this.props.versions.pending.get();
- if (!selectedChart) {
- return ;
+ if (!this.chart || versionsAreLoading) {
+ return ;
}
return (
- {this.renderIntroduction(selectedChart)}
- {this.renderReadme()}
+ {this.renderIntroduction(this.chart)}
+
+ {readmeIsLoading ? (
+
+ ) : (
+ this.renderReadme()
+ )}
);
}
render() {
- const { chart, hideDetails } = this.props;
-
return (
{this.renderContent()}
@@ -239,7 +186,8 @@ export const HelmChartDetails = withInjectables ({
...props,
createInstallChartTab: di.inject(createInstallChartTabInjectable),
- showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
- getChartDetails: di.inject(getChartDetailsInjectable),
+ readme: di.inject(readmeOfSelectHelmChartInjectable, props.chart),
+ versions: di.inject(versionsOfSelectedHelmChartInjectable, props.chart),
+ versionSelection: di.inject(helmChartDetailsVersionSelectionInjectable, props.chart),
}),
});
diff --git a/src/renderer/components/+helm-charts/helm-chart-store.injectable.ts b/src/renderer/components/+helm-charts/helm-chart-store.injectable.ts
new file mode 100644
index 0000000000..ac05d23368
--- /dev/null
+++ b/src/renderer/components/+helm-charts/helm-chart-store.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 { HelmChartStore } from "./helm-chart.store";
+
+const helmChartStoreInjectable = getInjectable({
+ id: "helm-chart-store",
+ instantiate: () => new HelmChartStore(),
+});
+
+export default helmChartStoreInjectable;
diff --git a/src/renderer/components/+helm-charts/helm-charts.tsx b/src/renderer/components/+helm-charts/helm-charts.tsx
index 0b9deae526..5a603cf7aa 100644
--- a/src/renderer/components/+helm-charts/helm-charts.tsx
+++ b/src/renderer/components/+helm-charts/helm-charts.tsx
@@ -12,12 +12,15 @@ import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.ap
import { HelmChartDetails } from "./helm-chart-details";
import { ItemListLayout } from "../item-object-list/list-layout";
import type { IComputedValue } from "mobx";
+import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
import helmChartsRouteParametersInjectable from "./helm-charts-route-parameters.injectable";
import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import { HelmChartIcon } from "./icon";
+import helmChartsInjectable from "./helm-charts/helm-charts.injectable";
+import selectedHelmChartInjectable from "./helm-charts/selected-helm-chart.injectable";
enum columnId {
name = "name",
@@ -34,27 +37,15 @@ interface Dependencies {
};
navigateToHelmCharts: NavigateToHelmCharts;
+
+ charts: IAsyncComputed;
+ selectedChart: IComputedValue;
}
@observer
class NonInjectedHelmCharts extends Component {
- componentDidMount() {
- helmChartStore.loadAll();
- }
-
- get selectedChart() {
- const chartName = this.props.routeParameters.chartName.get();
- const repo = this.props.routeParameters.repo.get();
-
- if (!chartName || !repo) {
- return undefined;
- }
-
- return helmChartStore.getByName(chartName, repo);
- }
-
onDetails = (chart: HelmChart) => {
- if (chart === this.selectedChart) {
+ if (chart === this.props.selectedChart.get()) {
this.hideDetails();
} else {
this.showDetails(chart);
@@ -78,6 +69,8 @@ class NonInjectedHelmCharts extends Component {
};
render() {
+ const selectedChart = this.props.selectedChart.get();
+
return (
@@ -87,7 +80,7 @@ class NonInjectedHelmCharts extends Component {
tableId="helm_charts"
className="HelmCharts"
store={helmChartStore}
- getItems={() => helmChartStore.items}
+ getItems={() => this.props.charts.value.get()}
isSelectable={false}
sortingCallbacks={{
[columnId.name]: chart => chart.getName(),
@@ -105,6 +98,7 @@ class NonInjectedHelmCharts extends Component {
placeholder: "Search Helm Charts...",
},
})}
+ customizeTableRowProps={(item) => ({ testId: `helm-chart-row-for-${item.getFullName("-")}` })}
renderTableHeader={[
{ className: "icon", showWithColumn: columnId.name },
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
@@ -124,12 +118,12 @@ class NonInjectedHelmCharts extends Component {
{ title: chart.getRepository(), className: chart.getRepository().toLowerCase() },
{ className: "menu" },
]}
- detailsItem={this.selectedChart}
+ detailsItem={selectedChart}
onDetails={this.onDetails}
/>
- {this.selectedChart && (
+ {selectedChart && (
)}
@@ -145,6 +139,8 @@ export const HelmCharts = withInjectables(
getProps: (di) => ({
routeParameters: di.inject(helmChartsRouteParametersInjectable),
navigateToHelmCharts: di.inject(navigateToHelmChartsInjectable),
+ charts: di.inject(helmChartsInjectable),
+ selectedChart: di.inject(selectedHelmChartInjectable),
}),
},
);
diff --git a/src/renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable.ts b/src/renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable.ts
new file mode 100644
index 0000000000..5edc0d7bea
--- /dev/null
+++ b/src/renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable.ts
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
+import { listCharts } from "../../../../common/k8s-api/endpoints/helm-charts.api";
+
+export type CallForHelmCharts = () => Promise;
+
+const callForHelmChartsInjectable = getInjectable({
+ id: "call-for-helm-charts",
+ instantiate: (): CallForHelmCharts => async () => await listCharts(),
+ causesSideEffects: true,
+});
+
+export default callForHelmChartsInjectable;
diff --git a/src/renderer/components/+helm-charts/helm-charts/helm-charts.injectable.ts b/src/renderer/components/+helm-charts/helm-charts/helm-charts.injectable.ts
new file mode 100644
index 0000000000..1324e382a9
--- /dev/null
+++ b/src/renderer/components/+helm-charts/helm-charts/helm-charts.injectable.ts
@@ -0,0 +1,19 @@
+/**
+ * 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 { asyncComputed } from "@ogre-tools/injectable-react";
+import callForHelmChartsInjectable from "./call-for-helm-charts.injectable";
+
+const helmChartsInjectable = getInjectable({
+ id: "helm-charts",
+
+ instantiate: (di) => {
+ const callForHelmCharts = di.inject(callForHelmChartsInjectable);
+
+ return asyncComputed(async () => await callForHelmCharts(), []);
+ },
+});
+
+export default helmChartsInjectable;
diff --git a/src/renderer/components/+helm-charts/helm-charts/selected-helm-chart.injectable.ts b/src/renderer/components/+helm-charts/helm-charts/selected-helm-chart.injectable.ts
new file mode 100644
index 0000000000..47777fb783
--- /dev/null
+++ b/src/renderer/components/+helm-charts/helm-charts/selected-helm-chart.injectable.ts
@@ -0,0 +1,36 @@
+/**
+ * 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 { computed } from "mobx";
+import helmChartsRouteParametersInjectable from "../helm-charts-route-parameters.injectable";
+import helmChartsInjectable from "./helm-charts.injectable";
+
+const selectedHelmChartInjectable = getInjectable({
+ id: "selected-helm-chart",
+
+ instantiate: (di) => {
+ const { chartName, repo } = di.inject(helmChartsRouteParametersInjectable);
+ const helmCharts = di.inject(helmChartsInjectable);
+
+ return computed(() => {
+ const dereferencedChartName = chartName.get();
+ const deferencedRepository = repo.get();
+
+ if (!dereferencedChartName || !deferencedRepository) {
+ return undefined;
+ }
+
+ return helmCharts.value
+ .get()
+ .find(
+ (chart) =>
+ chart.getName() === dereferencedChartName &&
+ chart.getRepository() === deferencedRepository,
+ );
+ });
+ },
+});
+
+export default selectedHelmChartInjectable;
diff --git a/src/renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable.ts b/src/renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable.ts
new file mode 100644
index 0000000000..d5506b486e
--- /dev/null
+++ b/src/renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable.ts
@@ -0,0 +1,19 @@
+/**
+ * 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 { HelmReleaseCreatePayload, HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
+import { createRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
+
+export type CallForCreateHelmRelease = (
+ payload: HelmReleaseCreatePayload
+) => Promise;
+
+const callForCreateHelmReleaseInjectable = getInjectable({
+ id: "call-for-create-helm-release",
+ instantiate: (): CallForCreateHelmRelease => createRelease,
+ causesSideEffects: true,
+});
+
+export default callForCreateHelmReleaseInjectable;
diff --git a/src/renderer/components/+helm-releases/create-release/create-release.injectable.ts b/src/renderer/components/+helm-releases/create-release/create-release.injectable.ts
index dfcf38fee0..b2e292b034 100644
--- a/src/renderer/components/+helm-releases/create-release/create-release.injectable.ts
+++ b/src/renderer/components/+helm-releases/create-release/create-release.injectable.ts
@@ -6,19 +6,18 @@ import { getInjectable } from "@ogre-tools/injectable";
import type {
HelmReleaseCreatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
-import {
- createRelease,
-} from "../../../../common/k8s-api/endpoints/helm-releases.api";
import releasesInjectable from "../releases.injectable";
+import callForCreateHelmReleaseInjectable from "./call-for-create-helm-release.injectable";
const createReleaseInjectable = getInjectable({
id: "create-release",
instantiate: (di) => {
const releases = di.inject(releasesInjectable);
+ const callForCreateRelease = di.inject(callForCreateHelmReleaseInjectable);
return async (payload: HelmReleaseCreatePayload) => {
- const release = await createRelease(payload);
+ const release = await callForCreateRelease(payload);
releases.invalidate();
diff --git a/src/renderer/components/+helm-releases/release-menu.tsx b/src/renderer/components/+helm-releases/release-menu.tsx
index 6974396dfb..a53c59a682 100644
--- a/src/renderer/components/+helm-releases/release-menu.tsx
+++ b/src/renderer/components/+helm-releases/release-menu.tsx
@@ -85,6 +85,7 @@ class NonInjectedHelmReleaseMenu extends React.Component (
Remove Helm Release
+ {" "}
{release.name}
?
diff --git a/src/renderer/components/+helm-releases/releases.tsx b/src/renderer/components/+helm-releases/releases.tsx
index b8c642c2a5..38239e80c8 100644
--- a/src/renderer/components/+helm-releases/releases.tsx
+++ b/src/renderer/components/+helm-releases/releases.tsx
@@ -77,6 +77,7 @@ class NonInjectedHelmReleases extends Component {
<>
Remove
+ {" "}
{releaseNames}
?
>
diff --git a/src/renderer/components/dialog/logs-dialog.tsx b/src/renderer/components/dialog/logs-dialog.tsx
index 5cff3e5c00..e8feb79db1 100644
--- a/src/renderer/components/dialog/logs-dialog.tsx
+++ b/src/renderer/components/dialog/logs-dialog.tsx
@@ -13,8 +13,7 @@ import { Notifications } from "../notifications";
import { Button } from "../button";
import { Icon } from "../icon";
import { clipboard } from "electron";
-
-// todo: make as external BrowserWindow (?)
+import { kebabCase } from "lodash/fp";
export interface LogsDialogProps extends DialogProps {
title: string;
@@ -26,6 +25,7 @@ export function LogsDialog({ title, logs, ...dialogProps }: LogsDialogProps) {
{title}}
diff --git a/src/renderer/components/dock/dock-tab.tsx b/src/renderer/components/dock/dock-tab.tsx
index c988f1a696..df578675fd 100644
--- a/src/renderer/components/dock/dock-tab.tsx
+++ b/src/renderer/components/dock/dock-tab.tsx
@@ -115,6 +115,7 @@ class NonInjectedDockTab extends React.Component {
)}
+ data-testid={`dock-tab-for-${id}`}
/>
{this.renderMenu(id)}
>
diff --git a/src/renderer/components/dock/dock-tabs.tsx b/src/renderer/components/dock/dock-tabs.tsx
index 5733d1f161..87b4aa756b 100644
--- a/src/renderer/components/dock/dock-tabs.tsx
+++ b/src/renderer/components/dock/dock-tabs.tsx
@@ -11,8 +11,8 @@ import { DockTab } from "./dock-tab";
import type { DockTab as DockTabModel } from "./dock/store";
import { TabKind } from "./dock/store";
import { TerminalTab } from "./terminal/dock-tab";
-import { useResizeObserver } from "../../hooks";
import { cssVar } from "../../utils";
+import { useResizeObserver } from "../../hooks";
export interface DockTabsProps {
tabs: DockTabModel[];
diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx
index 2b3f24cf78..f4c937cba3 100644
--- a/src/renderer/components/dock/dock.tsx
+++ b/src/renderer/components/dock/dock.tsx
@@ -124,7 +124,10 @@ class NonInjectedDock extends React.Component {
if (!isOpen || !selectedTab) return null;
return (
-
+
{this.renderTab(selectedTab)}
);
diff --git a/src/renderer/components/dock/editor-panel.module.scss b/src/renderer/components/dock/editor-panel.module.scss
index 6dd8ae3eb4..4af56c6f42 100644
--- a/src/renderer/components/dock/editor-panel.module.scss
+++ b/src/renderer/components/dock/editor-panel.module.scss
@@ -8,3 +8,7 @@
flex: 1;
height: 100%;
}
+
+.hidden {
+ display: none;
+}
diff --git a/src/renderer/components/dock/editor-panel.tsx b/src/renderer/components/dock/editor-panel.tsx
index 1a5588b0a3..d9eca59159 100644
--- a/src/renderer/components/dock/editor-panel.tsx
+++ b/src/renderer/components/dock/editor-panel.tsx
@@ -22,6 +22,7 @@ export interface EditorPanelProps {
autoFocus?: boolean; // default: true
onChange: MonacoEditorProps["onChange"];
onError?: MonacoEditorProps["onError"];
+ hidden?: boolean;
}
interface Dependencies {
@@ -36,6 +37,7 @@ const NonInjectedEditorPanel = observer(({
autoFocus = true,
className,
onError,
+ hidden,
}: Dependencies & EditorPanelProps) => {
const editor = createRef
();
@@ -59,7 +61,7 @@ const NonInjectedEditorPanel = observer(({
autoFocus={autoFocus}
id={tabId}
value={value}
- className={cssNames(styles.EditorPanel, className)}
+ className={cssNames(styles.EditorPanel, className, { hidden })}
onChange={onChange}
onError={onError}
ref={editor}
diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx
index 2ebe465e50..22ecea94ec 100644
--- a/src/renderer/components/dock/info-panel.tsx
+++ b/src/renderer/components/dock/info-panel.tsx
@@ -14,9 +14,12 @@ import { Button } from "../button";
import { Icon } from "../icon";
import { Spinner } from "../spinner";
import type { DockStore, TabId } from "./dock/store";
-import { Notifications } from "../notifications";
+import type { ShowNotification } from "../notifications";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock/store.injectable";
+import type { ShowCheckedErrorNotification } from "../notifications/show-checked-error.injectable";
+import showSuccessNotificationInjectable from "../notifications/show-success-notification.injectable";
+import showCheckedErrorNotificationInjectable from "../notifications/show-checked-error.injectable";
export interface InfoPanelProps extends OptionalProps {
tabId: TabId;
@@ -35,10 +38,15 @@ export interface OptionalProps {
showInlineInfo?: boolean;
showNotifications?: boolean;
showStatusPanel?: boolean;
+ submitTestId?: string;
+ cancelTestId?: string;
+ submittingTestId?: string;
}
interface Dependencies {
dockStore: DockStore;
+ showSuccessNotification: ShowNotification;
+ showCheckedErrorNotification: ShowCheckedErrorNotification;
}
@observer
@@ -82,11 +90,11 @@ class NonInjectedInfoPanel extends Component {
const result = await this.props.submit?.();
if (showNotifications && result) {
- Notifications.ok(result);
+ this.props.showSuccessNotification(result);
}
} catch (error) {
if (showNotifications) {
- Notifications.checkedError(error, "Unknown error while submitting");
+ this.props.showCheckedErrorNotification(error, "Unknown error while submitting");
}
} finally {
this.waiting = false;
@@ -128,7 +136,7 @@ class NonInjectedInfoPanel extends Component {
{waiting ? (
<>
-
+
{" "}
{submittingMessage}
>
@@ -140,7 +148,8 @@ class NonInjectedInfoPanel extends Component
{
{
label={submitLabel}
onClick={submit}
disabled={isDisabled}
+ data-testid={this.props.submitTestId}
/>
{showSubmitClose && (
(
{
getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable),
+ showSuccessNotification: di.inject(showSuccessNotificationInjectable),
+ showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
...props,
}),
},
diff --git a/src/renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable.ts b/src/renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable.ts
new file mode 100644
index 0000000000..1c379b56e5
--- /dev/null
+++ b/src/renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable.ts
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { getChartValues } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
+
+export type CallForHelmChartValues = (
+ repo: string,
+ name: string,
+ version: string
+) => Promise;
+
+const callForHelmChartValuesInjectable = getInjectable({
+ id: "call-for-helm-chart-values",
+ instantiate: (): CallForHelmChartValues => getChartValues,
+ causesSideEffects: true,
+});
+
+export default callForHelmChartValuesInjectable;
diff --git a/src/renderer/components/dock/install-chart/create-install-chart-tab.injectable.ts b/src/renderer/components/dock/install-chart/create-install-chart-tab.injectable.ts
index 3f3e47891e..29e94ebfc6 100644
--- a/src/renderer/components/dock/install-chart/create-install-chart-tab.injectable.ts
+++ b/src/renderer/components/dock/install-chart/create-install-chart-tab.injectable.ts
@@ -5,52 +5,46 @@
import { getInjectable } from "@ogre-tools/injectable";
import installChartTabStoreInjectable from "./store.injectable";
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
-import type {
- DockTab,
- DockTabCreate,
- DockTabCreateSpecific } from "../dock/store";
+import type { DockTab, DockTabCreateSpecific } from "../dock/store";
import { TabKind } from "../dock/store";
-import type { InstallChartTabStore } from "./store";
import createDockTabInjectable from "../dock/create-dock-tab.injectable";
-
-interface Dependencies {
- createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab;
- installChartStore: InstallChartTabStore;
-}
+import getRandomInstallChartTabIdInjectable from "./get-random-install-chart-tab-id.injectable";
export type CreateInstallChartTab = (chart: HelmChart, tabParams?: DockTabCreateSpecific) => DockTab;
-const createInstallChartTab = ({ createDockTab, installChartStore }: Dependencies) => (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => {
- const { name, repo, version } = chart;
-
- const tab = createDockTab(
- {
- title: `Helm Install: ${repo}/${name}`,
- ...tabParams,
- kind: TabKind.INSTALL_CHART,
- },
- false,
- );
-
- installChartStore.setData(tab.id, {
- name,
- repo,
- version,
- namespace: "default",
- releaseName: "",
- description: "",
- });
-
- return tab;
-};
-
const createInstallChartTabInjectable = getInjectable({
id: "create-install-chart-tab",
- instantiate: (di) => createInstallChartTab({
- installChartStore: di.inject(installChartTabStoreInjectable),
- createDockTab: di.inject(createDockTabInjectable),
- }),
+ instantiate: (di) => {
+ const installChartStore = di.inject(installChartTabStoreInjectable);
+ const createDockTab = di.inject(createDockTabInjectable);
+ const getRandomId = di.inject(getRandomInstallChartTabIdInjectable);
+
+ return (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => {
+ const { name, repo, version } = chart;
+
+ const tab = createDockTab(
+ {
+ id: getRandomId(),
+ title: `Helm Install: ${repo}/${name}`,
+ ...tabParams,
+ kind: TabKind.INSTALL_CHART,
+ },
+ false,
+ );
+
+ installChartStore.setData(tab.id, {
+ name,
+ repo,
+ version,
+ namespace: "default",
+ releaseName: "",
+ description: "",
+ });
+
+ return tab;
+ };
+ },
});
export default createInstallChartTabInjectable;
diff --git a/src/renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable.ts b/src/renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable.ts
new file mode 100644
index 0000000000..c9169fff92
--- /dev/null
+++ b/src/renderer/components/dock/install-chart/get-random-install-chart-tab-id.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 getRandomInstallChartTabIdInjectable = getInjectable({
+ id: "get-random-install-chart-tab-id",
+ instantiate: (di) => di.inject(getRandomIdInjectable),
+});
+
+export default getRandomInstallChartTabIdInjectable;
diff --git a/src/renderer/components/dock/install-chart/install-chart-model.injectable.tsx b/src/renderer/components/dock/install-chart/install-chart-model.injectable.tsx
new file mode 100644
index 0000000000..64f2e27e12
--- /dev/null
+++ b/src/renderer/components/dock/install-chart/install-chart-model.injectable.tsx
@@ -0,0 +1,282 @@
+/**
+ * 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 installChartTabStoreInjectable from "./store.injectable";
+import { waitUntilDefined } from "../../../../common/utils";
+import type { CallForHelmChartValues } from "./chart-data/call-for-helm-chart-values.injectable";
+import callForHelmChartValuesInjectable from "./chart-data/call-for-helm-chart-values.injectable";
+import type { IChartInstallData, InstallChartTabStore } from "./store";
+import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
+import React from "react";
+import {
+ action,
+ computed,
+ observable,
+ runInAction,
+} from "mobx";
+import assert from "assert";
+import type { CallForCreateHelmRelease } from "../../+helm-releases/create-release/call-for-create-helm-release.injectable";
+import callForCreateHelmReleaseInjectable from "../../+helm-releases/create-release/call-for-create-helm-release.injectable";
+import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
+import dockStoreInjectable from "../dock/store.injectable";
+import type { NavigateToHelmReleases } from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
+import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
+import type { SingleValue } from "react-select";
+import type { CallForHelmChartVersions } from "../../+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
+import callForHelmChartVersionsInjectable from "../../+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
+
+const installChartModelInjectable = getInjectable({
+ id: "install-chart-model",
+
+ instantiate: async (di, tabId: string) => {
+ const store = di.inject(installChartTabStoreInjectable);
+ const callForHelmChartValues = di.inject(callForHelmChartValuesInjectable);
+ const callForHelmChartVersions = di.inject(callForHelmChartVersionsInjectable);
+ const callForCreateHelmRelease = di.inject(callForCreateHelmReleaseInjectable);
+ const dockStore = di.inject(dockStoreInjectable);
+ const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
+ const closeTab = () => dockStore.closeTab(tabId);
+
+ const waitForChart = async () => {
+ await waitUntilDefined(() => store.getData(tabId));
+ };
+
+ const model = new InstallChartModel({
+ tabId,
+ waitForChart,
+ callForCreateHelmRelease,
+ closeTab,
+ navigateToHelmReleases,
+ callForHelmChartValues,
+ callForHelmChartVersions,
+ store,
+ });
+
+ await model.load();
+
+ return model;
+ },
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (di, tabId: string) => tabId,
+ }),
+});
+
+export default installChartModelInjectable;
+
+interface Dependencies {
+ tabId: string;
+ closeTab: () => void;
+ navigateToHelmReleases: NavigateToHelmReleases;
+ waitForChart: () => Promise;
+ callForCreateHelmRelease: CallForCreateHelmRelease;
+ callForHelmChartValues: CallForHelmChartValues;
+ callForHelmChartVersions: CallForHelmChartVersions;
+ store: InstallChartTabStore;
+}
+
+export class InstallChartModel {
+ readonly namespace = {
+ value: computed(() => this.chart?.namespace || "default"),
+
+ onChange: action(
+ (option: SingleValue<{ label: string; value: string }>) => {
+ if (option) {
+ const namespace = option.value;
+
+ this.save({ namespace });
+ }
+ },
+ ),
+ };
+
+ readonly customName = {
+ value: computed(() => this.chart?.releaseName || ""),
+
+ onChange: action((customName: string) => {
+ this.save({ releaseName: customName });
+ }),
+ };
+
+ private readonly versions = observable.array([]);
+ readonly installed = observable.box();
+
+ private save = (data: Partial) => {
+ assert(this.chart);
+
+ const chart = { ...this.chart, ...data };
+
+ this.dependencies.store.setData(this.dependencies.tabId, chart);
+ };
+
+ readonly version = {
+ value: computed(() => this.chart?.version),
+
+ onChange: async (version: string | undefined) => {
+ assert(this.chart);
+
+ if (!version) {
+ return;
+ }
+
+ this.save({ version });
+
+ runInAction(() => {
+ this.configuration.isLoading.set(true);
+ });
+
+ const configuration = await this.dependencies.callForHelmChartValues(
+ this.chart.repo,
+ this.chart.name,
+ version,
+ );
+
+ runInAction(() => {
+ this.configuration.onChange(configuration);
+ this.configuration.isLoading.set(false);
+ });
+ },
+
+ options: computed(() =>
+ this.versions.map((chart) => ({
+ label: chart.version,
+ value: chart.version,
+ })),
+ ),
+ };
+
+ readonly configuration = {
+ value: computed(() => this.chart?.values || ""),
+ isLoading: observable.box(false),
+
+ onChange: action((configuration: string) => {
+ this.errorInConfiguration.value.set(undefined);
+
+ this.save({ values: configuration });
+ }),
+ };
+
+ readonly errorInConfiguration = {
+ value: observable.box(),
+
+ onChange: action((error: unknown) => {
+ this.errorInConfiguration.value.set(error as string);
+ }),
+ };
+
+ readonly executionOutput = {
+ isShown: observable.box(false),
+
+ show: action(() => {
+ this.executionOutput.isShown.set(true);
+ }),
+
+ close: action(() => {
+ this.executionOutput.isShown.set(false);
+ }),
+ };
+
+ constructor(private readonly dependencies: Dependencies) {}
+
+ @computed
+ private get chart() {
+ const chart = this.dependencies.store.getData(this.dependencies.tabId);
+
+ assert(chart);
+
+ return chart;
+ }
+
+ load = async () => {
+ await this.dependencies.waitForChart();
+
+ const [defaultConfiguration, versions] = await Promise.all([
+ this.dependencies.callForHelmChartValues(
+ this.chart.repo,
+ this.chart.name,
+ this.chart.version,
+ ),
+
+ this.dependencies.callForHelmChartVersions(
+ this.chart.repo,
+ this.chart.name,
+ ),
+ ]);
+
+ runInAction(() => {
+ // TODO: Make "default" not hard-coded
+ const namespace = this.chart.namespace || "default";
+
+ this.versions.replace(versions);
+
+ this.save({
+ version: this.chart.version,
+ namespace,
+ values: this.chart.values || defaultConfiguration,
+ releaseName: this.chart.releaseName,
+ });
+ });
+ };
+
+ @computed
+ get isValid() {
+ return !this.configuration.isLoading.get();
+ }
+
+ get chartName() {
+ return `${this.repository}/${this.name}`;
+ }
+
+ private get name() {
+ assert(this.chart);
+
+ return this.chart.name;
+ }
+
+ private get repository() {
+ assert(this.chart);
+
+ return this.chart.repo;
+ }
+
+ install = async () => {
+ const installed = await this.dependencies.callForCreateHelmRelease({
+ name: this.customName.value.get() || undefined,
+ chart: this.name,
+ repo: this.repository,
+ namespace: this.namespace.value.get() || "",
+ version: this.version.value.get() || "",
+ values: this.configuration.value.get() || "",
+ });
+
+ runInAction(() => {
+ this.installed.set(installed);
+ });
+
+ return (
+
+ {"Chart Release "}
+ {installed.release.name}
+ {" successfully created."}
+
+ );
+ };
+
+ navigateToInstalledRelease = () => {
+ const installed = this.installed.get();
+
+ assert(installed);
+
+ const release = installed.release;
+
+ this.dependencies.navigateToHelmReleases({
+ name: release.name,
+ namespace: release.namespace,
+ });
+
+ this.dependencies.closeTab();
+ };
+}
+
diff --git a/src/renderer/components/dock/install-chart/store.ts b/src/renderer/components/dock/install-chart/store.ts
index ab80e4350f..7bb7b3bfcc 100644
--- a/src/renderer/components/dock/install-chart/store.ts
+++ b/src/renderer/components/dock/install-chart/store.ts
@@ -3,13 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
-import { action, makeObservable } from "mobx";
-import type { TabId } from "../dock/store";
+import { makeObservable } from "mobx";
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
-import { getChartDetails, getChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
-import { waitUntilDefined } from "../../../../common/utils/wait";
export interface IChartInstallData {
name: string;
@@ -40,42 +37,4 @@ export class InstallChartTabStore extends DockTabStore {
get details() {
return this.dependencies.detailsStore;
}
-
- @action
- async loadData(tabId: string) {
- const promises = [];
- const data = await waitUntilDefined(() => this.getData(tabId));
-
- if (!this.getData(tabId)?.values) {
- promises.push(this.loadValues(tabId));
- }
-
- if (!this.versions.getData(tabId)) {
- promises.push(this.loadVersions(tabId, data));
- }
-
- await Promise.all(promises);
- }
-
- @action
- private async loadVersions(tabId: TabId, { repo, name, version }: IChartInstallData) {
- this.versions.clearData(tabId); // reset
- const charts = await getChartDetails(repo, name, { version });
- const versions = charts.versions.map(chartVersion => chartVersion.version);
-
- this.versions.setData(tabId, versions);
- }
-
- @action
- async loadValues(tabId: TabId, attempt = 0): Promise {
- const data = await waitUntilDefined(() => this.getData(tabId));
- const { repo, name, version } = data;
- const values = await getChartValues(repo, name, version);
-
- if (values) {
- this.setData(tabId, { ...data, values });
- } else if (attempt < 4) {
- return this.loadValues(tabId, attempt + 1);
- }
- }
}
diff --git a/src/renderer/components/dock/install-chart/view.tsx b/src/renderer/components/dock/install-chart/view.tsx
index 384bd4fb11..282acce6d8 100644
--- a/src/renderer/components/dock/install-chart/view.tsx
+++ b/src/renderer/components/dock/install-chart/view.tsx
@@ -5,154 +5,44 @@
import "./install-chart.scss";
-import React, { Component } from "react";
-import { action, makeObservable, observable } from "mobx";
+import React from "react";
import { observer } from "mobx-react";
-import type { DockStore, DockTab } from "../dock/store";
+import type { DockTab } from "../dock/store";
import { InfoPanel } from "../info-panel";
import { Badge } from "../../badge";
import { NamespaceSelect } from "../../+namespaces/namespace-select";
import { prevDefault } from "../../../utils";
-import type { IChartInstallData, InstallChartTabStore } from "./store";
-import { Spinner } from "../../spinner";
import { Icon } from "../../icon";
import { Button } from "../../button";
import { LogsDialog } from "../../dialog/logs-dialog";
-import type { SelectOption } from "../../select";
import { Select } from "../../select";
import { Input } from "../../input";
import { EditorPanel } from "../editor-panel";
-import type { HelmReleaseCreatePayload, HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { withInjectables } from "@ogre-tools/injectable-react";
-import installChartTabStoreInjectable from "./store.injectable";
-import dockStoreInjectable from "../dock/store.injectable";
-import createReleaseInjectable from "../../+helm-releases/create-release/create-release.injectable";
-import { Notifications } from "../../notifications";
-import type { NavigateToHelmReleases } from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
-import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
-import assert from "assert";
-import type { SingleValue } from "react-select";
+import type { InstallChartModel } from "./install-chart-model.injectable";
+import installChartModelInjectable from "./install-chart-model.injectable";
+import { Spinner } from "../../spinner";
export interface InstallCharProps {
tab: DockTab;
}
interface Dependencies {
- createRelease: (payload: HelmReleaseCreatePayload) => Promise;
- installChartStore: InstallChartTabStore;
- dockStore: DockStore;
- navigateToHelmReleases: NavigateToHelmReleases;
+ model: InstallChartModel;
}
-@observer
-class NonInjectedInstallChart extends Component {
- @observable error = "";
- @observable showNotes = false;
+const NonInjectedInstallChart = observer(
+ ({ model: model, tab: { id: tabId }}: InstallCharProps & Dependencies) => {
+ const installed = model.installed.get();
- constructor(props: InstallCharProps & Dependencies) {
- super(props);
- makeObservable(this);
- }
-
- componentDidMount(): void {
- this.props.installChartStore.loadData(this.tabId)
- .catch(err => Notifications.error(String(err)));
- }
-
- get chartData() {
- return this.props.installChartStore.getData(this.tabId);
- }
-
- get tabId() {
- return this.props.tab.id;
- }
-
- get versions() {
- return this.props.installChartStore.versions.getData(this.tabId);
- }
-
- get releaseDetails() {
- return this.props.installChartStore.details.getData(this.tabId);
- }
-
- viewRelease = ({ release }: HelmReleaseUpdateDetails) => {
- this.props.navigateToHelmReleases({
- name: release.name,
- namespace: release.namespace,
- });
- this.props.dockStore.closeTab(this.tabId);
- };
-
- save(data: Partial) {
- assert(this.chartData, "Cannot update data before data exists");
-
- this.props.installChartStore.setData(this.tabId, { ...this.chartData, ...data });
- }
-
- onVersionChange = (option: SingleValue>) => {
- if (option) {
- this.save({ ...option, values: "" });
- this.props.installChartStore.loadValues(this.tabId);
- }
- };
-
- onChange = action((values: string) => {
- this.error = "";
- this.save({ values });
- });
-
- onError = action((error: Error | string) => {
- this.error = error.toString();
- });
-
- onNamespaceChange = (option: SingleValue>) => {
- if (option) {
- this.save({ namespace: option.value });
- }
- };
-
- onReleaseNameChange = (name: string) => {
- this.save({ releaseName: name });
- };
-
- install = async ({ repo, name, version, namespace, values = "", releaseName }: IChartInstallData) => {
- const details = await this.props.createRelease({
- name: releaseName || undefined,
- chart: name,
- repo,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- namespace: namespace!,
- version,
- values,
- });
-
- this.props.installChartStore.details.setData(this.tabId, details);
-
- return (
-
- {"Chart Release "}
- {details.release.name}
- {" successfully created."}
-
- );
- };
-
- render() {
- const { tabId, chartData, versions, install, releaseDetails } = this;
-
- if (chartData?.values === undefined || !versions) {
- return ;
- }
-
- if (releaseDetails) {
+ if (installed) {
return (
+ sticker />
Installation complete!
@@ -160,30 +50,34 @@ class NonInjectedInstallChart extends Component
autoFocus
primary
label="View Helm Release"
- onClick={prevDefault(() => this.viewRelease(releaseDetails))}
+ onClick={prevDefault(model.navigateToInstalledRelease)}
+ data-testid={`show-release-${installed.release.name}-for-${tabId}`}
/>
this.showNotes = true}
+ onClick={model.executionOutput.show}
+ data-testid={`show-execution-output-for-${installed.release.name}-in-${tabId}`}
/>
this.showNotes = false}
- logs={releaseDetails.log}
+ isOpen={model.executionOutput.isShown.get()}
+ close={model.executionOutput.close}
+ logs={installed.log}
/>
);
}
- const { repo, name, version, namespace, releaseName } = chartData;
- const versionOptions = versions.map(version => ({
- value: version,
- label: version,
- }));
+ const {
+ configuration,
+ version,
+ namespace,
+ customName,
+ errorInConfiguration,
+ } = model;
return (
);
- }
-}
+ },
+);
export const InstallChart = withInjectables(
NonInjectedInstallChart,
{
- getProps: (di, props) => ({
- createRelease: di.inject(createReleaseInjectable),
- installChartStore: di.inject(installChartTabStoreInjectable),
- dockStore: di.inject(dockStoreInjectable),
- navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
+ getPlaceholder: () => (
+
+ ),
+
+ getProps: async (di, props) => ({
+ model: await di.inject(installChartModelInjectable, props.tab.id),
...props,
}),
},
diff --git a/src/renderer/components/monaco-editor/__mocks__/monaco-editor.tsx b/src/renderer/components/monaco-editor/__mocks__/monaco-editor.tsx
new file mode 100644
index 0000000000..e7b01cc876
--- /dev/null
+++ b/src/renderer/components/monaco-editor/__mocks__/monaco-editor.tsx
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { editor } from "monaco-editor";
+import React from "react";
+import type { MonacoEditorProps, MonacoEditorRef } from "../monaco-editor";
+import { monacoValidators } from "../monaco-validators";
+
+class FakeMonacoEditor extends React.Component {
+ render() {
+ const { id, value, onChange, onError, language = "yaml" } = this.props;
+
+ return (
+ {
+ const newValue = event.target.value;
+
+ onChange?.(
+ newValue,
+ {} as editor.IModelContentChangedEvent,
+ );
+
+ const validator = monacoValidators[language];
+
+ try {
+ validator(newValue);
+ } catch(e) {
+ onError?.(e);
+ }
+ }}
+ value={value}
+ />
+ );
+ }
+}
+
+export const MonacoEditor = React.forwardRef<
+ MonacoEditorRef,
+ MonacoEditorProps
+>((props, ref) => );
diff --git a/src/renderer/components/monaco-editor/monaco-editor.tsx b/src/renderer/components/monaco-editor/monaco-editor.tsx
index 4869cb4f48..7418187dde 100644
--- a/src/renderer/components/monaco-editor/monaco-editor.tsx
+++ b/src/renderer/components/monaco-editor/monaco-editor.tsx
@@ -12,10 +12,11 @@ import type { MonacoTheme } from "./monaco-themes";
import { type MonacoValidator, monacoValidators } from "./monaco-validators";
import { debounce, merge } from "lodash";
import { autoBind, cssNames, disposer } from "../../utils";
-import { UserStore } from "../../../common/user-store";
+import type { UserStore } from "../../../common/user-store";
import type { ThemeStore } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
+import userStoreInjectable from "../../../common/user-store/user-store.injectable";
export type MonacoEditorId = string;
@@ -39,6 +40,7 @@ export interface MonacoEditorProps {
interface Dependencies {
themeStore: ThemeStore;
+ userStore: UserStore;
}
export function createMonacoUri(id: MonacoEditorId): Uri {
@@ -99,7 +101,7 @@ class NonInjectedMonacoEditor extends React.Component ({
...props,
themeStore: di.inject(themeStoreInjectable),
+ userStore: di.inject(userStoreInjectable),
}),
},
);
diff --git a/src/renderer/components/table/table-row.tsx b/src/renderer/components/table/table-row.tsx
index 1cf498c5ec..9725c7112d 100644
--- a/src/renderer/components/table/table-row.tsx
+++ b/src/renderer/components/table/table-row.tsx
@@ -19,15 +19,19 @@ export interface TableRowProps- extends React.DOMAttributes
sortItem?: Item; // data for sorting callback in
searchItem?: Item; // data for searching filters in
disabled?: boolean;
+ testId?: string;
}
export class TableRow- extends React.Component
> {
render() {
- const { className, nowrap, selected, disabled, children, sortItem, searchItem, ...rowProps } = this.props;
+ const { className, nowrap, selected, disabled, children, sortItem, searchItem, testId, ...rowProps } = this.props;
const classNames = cssNames("TableRow", className, { selected, nowrap, disabled });
return (
-
+
{children}
);
diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx
index a83fd0d45d..995e1cb54e 100644
--- a/src/renderer/components/test-utils/get-application-builder.tsx
+++ b/src/renderer/components/test-utils/get-application-builder.tsx
@@ -12,7 +12,7 @@ import { Router } from "react-router";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import allowedResourcesInjectable from "../../cluster-frame-context/allowed-resources.injectable";
import type { RenderResult } from "@testing-library/react";
-import { getByText, fireEvent } from "@testing-library/react";
+import { queryByText, fireEvent } from "@testing-library/react";
import type { KubeResource } from "../../../common/rbac";
import type { DiContainer } from "@ogre-tools/injectable";
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
@@ -26,6 +26,7 @@ import type { MenuItemOpts } from "../../../main/menu/application-menu-items.inj
import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable";
import type { MenuItemConstructorOptions, MenuItem } from "electron";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
+import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import { ClusterFrameContext } from "../../cluster-frame-context/cluster-frame-context";
@@ -110,12 +111,13 @@ export interface ApplicationBuilder {
};
helmCharts: {
- navigate: () => void;
+ navigate: NavigateToHelmCharts;
};
select: {
- openMenu: (id: string) => void;
+ openMenu: (id: string) => ({ selectOption: (labelText: string) => void });
selectOption: (menuId: string, labelText: string) => void;
+ getValue: (menuId: string) => string;
};
}
@@ -244,6 +246,20 @@ export const getApplicationBuilder = () => {
const disableRendererExtension = disableExtensionsFor(rendererExtensionsState, rendererDi);
const disableMainExtension = disableExtensionsFor(mainExtensionsState, mainDi);
+ const selectOptionFor = (menuId: string) => (labelText: string) => {
+ const menuOptions = rendered.baseElement.querySelector
(
+ `.${menuId}-options`,
+ );
+
+ assert(menuOptions, `Could not find select options for menu with ID "${menuId}"`);
+
+ const option = queryByText(menuOptions, labelText);
+
+ assert(option, `Could not find select option with label "${labelText}" for menu with ID "${menuId}"`);
+
+ userEvent.click(option);
+ };
+
const builder: ApplicationBuilder = {
dis,
@@ -364,10 +380,10 @@ export const getApplicationBuilder = () => {
},
helmCharts: {
- navigate: () => {
+ navigate: (parameters) => {
const navigateToHelmCharts = rendererDi.inject(navigateToHelmChartsInjectable);
- navigateToHelmCharts();
+ navigateToHelmCharts(parameters);
},
},
@@ -391,6 +407,7 @@ export const getApplicationBuilder = () => {
const namespaceStoreStub = {
contextNamespaces: [],
items: [],
+ selectNamespaces: () => {},
} as unknown as NamespaceStore;
const clusterFrameContextFake = new ClusterFrameContext(
@@ -500,25 +517,33 @@ export const getApplicationBuilder = () => {
select: {
openMenu: (menuId) => {
- const selector = rendered.container.querySelector(
+ const select = rendered.baseElement.querySelector(
`#${menuId}`,
);
- assert(selector);
+ assert(select, `Could not find select with ID "${menuId}"`);
- openMenu(selector);
+ openMenu(select);
+
+ return {
+ selectOption: selectOptionFor(menuId),
+ };
},
- selectOption: (menuId, labelText) => {
- const menuOptions = rendered.baseElement.querySelector(
- `.${menuId}-options`,
+ selectOption: (menuId, labelText) => selectOptionFor(menuId)(labelText),
+
+ getValue: (menuId) => {
+ const select = rendered.baseElement.querySelector(
+ `#${menuId}`,
);
- assert(menuOptions);
+ assert(select, `Could not find select with ID "${menuId}"`);
- const option = getByText(menuOptions, labelText);
+ const controlElement = select.closest(".Select__control");
- userEvent.click(option);
+ assert(controlElement, `Could not find select value for menu with ID "${menuId}"`);
+
+ return controlElement.textContent || "";
},
},
};
diff --git a/src/renderer/frames/cluster-frame/__snapshots__/cluster-frame.test.tsx.snap b/src/renderer/frames/cluster-frame/__snapshots__/cluster-frame.test.tsx.snap
index 3dc152804a..0084489fc5 100644
--- a/src/renderer/frames/cluster-frame/__snapshots__/cluster-frame.test.tsx.snap
+++ b/src/renderer/frames/cluster-frame/__snapshots__/cluster-frame.test.tsx.snap
@@ -372,6 +372,7 @@ exports[` given cluster with list nodes and namespaces permissio
>
given cluster with list nodes and namespaces permissio
>
given cluster without list nodes, but with namespaces
>
{
const {
@@ -111,6 +112,9 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
di.override(historyInjectable, () => createMemoryHistory());
di.override(legacyOnChannelListenInjectable, () => () => noop);
+
+ di.override(storageSaveDelayInjectable, () => 0);
+
di.override(requestAnimationFrameInjectable, () => (callback) => callback());
di.override(lensResourcesDirInjectable, () => "/irrelevant");
diff --git a/src/renderer/utils/create-storage/create-storage.injectable.ts b/src/renderer/utils/create-storage/create-storage.injectable.ts
index f0bc54cd53..b40eef738c 100644
--- a/src/renderer/utils/create-storage/create-storage.injectable.ts
+++ b/src/renderer/utils/create-storage/create-storage.injectable.ts
@@ -11,6 +11,7 @@ import { observable } from "mobx";
import loggerInjectable from "../../../common/logger.injectable";
import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable";
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
+import storageSaveDelayInjectable from "./storage-save-delay.injectable";
const createStorageInjectable = getInjectable({
id: "create-storage",
@@ -27,6 +28,7 @@ const createStorageInjectable = getInjectable({
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
getAbsolutePath: di.inject(getAbsolutePathInjectable),
hostedClusterId: di.inject(hostedClusterIdInjectable),
+ saveDelay: di.inject(storageSaveDelayInjectable),
}),
});
diff --git a/src/renderer/utils/create-storage/create-storage.ts b/src/renderer/utils/create-storage/create-storage.ts
index 0bc3de06e4..501e425161 100755
--- a/src/renderer/utils/create-storage/create-storage.ts
+++ b/src/renderer/utils/create-storage/create-storage.ts
@@ -21,6 +21,7 @@ interface Dependencies {
writeJsonFile: (filePath: string, contentObject: JsonObject) => Promise;
getAbsolutePath: GetAbsolutePath;
hostedClusterId: string | undefined;
+ saveDelay: number;
}
export type CreateStorage = (key: string, defaultValue: T) => StorageLayer;
@@ -36,6 +37,7 @@ export const createStorage = ({
readJsonFile,
writeJsonFile,
hostedClusterId,
+ saveDelay,
}: Dependencies): CreateStorage => (key, defaultValue) => {
const { logPrefix } = StorageHelper;
@@ -59,7 +61,7 @@ export const createStorage = ({
// bind auto-saving data changes to %storage-file.json
reaction(() => toJS(storage.data), saveFile, {
- delay: 250, // lazy, avoid excessive writes to fs
+ delay: saveDelay, // lazy, avoid excessive writes to fs
equals: comparer.structural, // save only when something really changed
});
diff --git a/src/renderer/utils/create-storage/storage-save-delay.injectable.ts b/src/renderer/utils/create-storage/storage-save-delay.injectable.ts
new file mode 100644
index 0000000000..57a680befc
--- /dev/null
+++ b/src/renderer/utils/create-storage/storage-save-delay.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 { getInjectable } from "@ogre-tools/injectable";
+
+const storageSaveDelayInjectable = getInjectable({
+ id: "storage-save-delay",
+ instantiate: () => 250,
+});
+
+export default storageSaveDelayInjectable;
diff --git a/src/renderer/utils/create-storage/storages-are-ready.ts b/src/renderer/utils/create-storage/storages-are-ready.ts
new file mode 100644
index 0000000000..641c64cf8b
--- /dev/null
+++ b/src/renderer/utils/create-storage/storages-are-ready.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 type { DiContainer } from "@ogre-tools/injectable";
+import type { CreateStorage } from "./create-storage";
+import createStorageInjectable from "./create-storage.injectable";
+
+export const controlWhenStoragesAreReady = (di: DiContainer) => {
+ const storagesAreReady: Promise[] = [];
+
+ const decorated =
+ (toBeDecorated: CreateStorage) =>
+ (key: string, defaultValue: any) => {
+ const storage = toBeDecorated(key, defaultValue);
+
+ storagesAreReady.push(storage.whenReady);
+
+ return storage;
+ };
+
+ // TODO: Remove when typing is added to the library
+ (di as any).decorateFunction(createStorageInjectable, decorated);
+
+ return async () => void await Promise.all(storagesAreReady);
+};