Restructure to monorepo (#6907)

* wip: restructure to monorepo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor create-release-pr to a package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* github workflow fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix typo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* add webpack-env types to core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix github workflows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor/fix integration tests

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* lint fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* yarn run dev

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* eslint settings for vscode

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* move templates to right package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* open-lens build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* integration test fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix nx task dependencies

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* use bash shell for unit tests in test workflow

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix test:unit for windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix build:app on windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove ELECTRON_BUILDER_EXTRA_ARGS

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* sync src/ from master

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove Makefile from core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
1 parent 0066ca4ea2
commit 2657df2293
3321 files changed
+28699 -11447

No files matched your search

@@ -0,0 +1,393 @@
/**
* 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 { 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 { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { ExecFile } from "../../common/fs/exec-file.injectable";
import execFileInjectable from "../../common/fs/exec-file.injectable";
import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable";
import getActiveHelmRepositoriesInjectable from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import type { HelmRepo } from "../../common/helm/helm-repo";
import requestPublicHelmRepositoriesInjectable from "./child-features/preferences/renderer/adding-of-public-helm-repository/public-helm-repositories/request-public-helm-repositories.injectable";
import isPathInjectable from "../../renderer/components/input/validators/is-path.injectable";
import showSuccessNotificationInjectable from "../../renderer/components/notifications/show-success-notification.injectable";
import showErrorNotificationInjectable from "../../renderer/components/notifications/show-error-notification.injectable";
import type { AsyncResult } from "../../common/utils/async-result";
import { testUsingFakeTime } from "../../common/test-utils/use-fake-time";
describe("add custom helm repository in preferences", () => {
let builder: ApplicationBuilder;
let showSuccessNotificationMock: jest.Mock;
let showErrorNotificationMock: jest.Mock;
let rendered: RenderResult;
let execFileMock: AsyncFnMock<ExecFile>;
let getActiveHelmRepositoriesMock: AsyncFnMock<() => Promise<AsyncResult<HelmRepo[]>>>;
beforeEach(async () => {
jest.useFakeTimers();
builder = getApplicationBuilder();
testUsingFakeTime("2021-01-01 12:00:00");
execFileMock = asyncFn();
getActiveHelmRepositoriesMock = asyncFn();
showSuccessNotificationMock = jest.fn();
showErrorNotificationMock = jest.fn();
builder.beforeApplicationStart((mainDi) => {
mainDi.override(getActiveHelmRepositoriesInjectable, () => getActiveHelmRepositoriesMock);
mainDi.override(execFileInjectable, () => execFileMock);
mainDi.override(helmBinaryPathInjectable, () => "some-helm-binary-path");
});
builder.beforeWindowStart((windowDi) => {
windowDi.override(requestPublicHelmRepositoriesInjectable, () => async () => []);
windowDi.override(showSuccessNotificationInjectable, () => showSuccessNotificationMock);
windowDi.override(showErrorNotificationInjectable, () => showErrorNotificationMock);
// TODO: Figure out how to make async validators unit testable
windowDi.override(isPathInjectable, () => ({ debounce: 0, validate: async () => {} }));
});
rendered = await builder.render();
});
describe("when navigating to preferences containing helm repositories", () => {
beforeEach(async () => {
builder.preferences.navigate();
builder.preferences.navigation.click("kubernetes");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when active repositories resolve", () => {
beforeEach(async () => {
await Promise.all([
getActiveHelmRepositoriesMock.resolve({
callWasSuccessful: true,
response: [{
name: "Some active repository",
url: "some-url",
cacheFilePath: "/some-cache-file-for-active",
}],
}),
]);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when selecting to add custom repository", () => {
beforeEach(() => {
const button = rendered.getByTestId("add-custom-helm-repo-button");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows dialog", () => {
expect(
rendered.queryByTestId("add-custom-helm-repository-dialog"),
).toBeInTheDocument();
});
// TODO: Figure out how to close dialog by clicking outside of it
xdescribe("when closing the dialog by clicking outside", () => {
beforeEach(() => {});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show dialog anymore", () => {
expect(
rendered.queryByTestId("add-custom-helm-repository-dialog"),
).not.toBeInTheDocument();
});
});
describe("when closing the dialog by clicking cancel", () => {
beforeEach(() => {
const button = rendered.getByTestId("custom-helm-repository-cancel-button");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show dialog anymore", () => {
expect(
rendered.queryByTestId("add-custom-helm-repository-dialog"),
).not.toBeInTheDocument();
});
});
describe("when inputted minimal options for the repository", () => {
beforeEach(() => {
getActiveHelmRepositoriesMock.mockClear();
const nameInput = rendered.getByTestId("custom-helm-repository-name-input");
fireEvent.change(nameInput, { target: { value: "some-custom-repository" }});
const urlInput = rendered.getByTestId("custom-helm-repository-url-input");
fireEvent.change(urlInput, { target: { value: "http://some.url" }});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when submitted and some time passes", () => {
beforeEach(() => {
const submitButton = rendered.getByTestId("custom-helm-repository-submit-button");
fireEvent.click(submitButton);
// TODO: Remove when debounce is removed from WizardStep.submit
jest.runOnlyPendingTimers();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("adds the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "add", "some-custom-repository", "http://some.url"],
{
maxBuffer: 34359738368,
env: {},
},
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
it("does not show notification yet", () => {
expect(showSuccessNotificationMock).not.toHaveBeenCalled();
});
describe("when activation rejects", () => {
beforeEach(async () => {
await execFileMock.resolve({
callWasSuccessful: false,
error: Object.assign(new Error("Some error"), {
stderr: "",
}),
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Some error",
);
});
it("does not show success notification", () => {
expect(showSuccessNotificationMock).not.toHaveBeenCalled();
});
it("does not show dialog anymore", () => {
expect(rendered.queryByTestId("add-custom-helm-repository-dialog")).not.toBeInTheDocument();
});
it("does not reload active repositories", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
});
describe("when activation resolves with success", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "add", "some-custom-repository", "http://some.url"],
],
{
callWasSuccessful: true,
response: "",
},
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show dialog anymore", () => {
expect(rendered.queryByTestId("add-custom-helm-repository-dialog")).not.toBeInTheDocument();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
it("shows success notification", () => {
expect(showSuccessNotificationMock).toHaveBeenCalledWith(
"Helm repository some-custom-repository has been added.",
);
});
describe("when adding custom repository again", () => {
beforeEach(() => {
const button = rendered.getByTestId("add-custom-helm-repo-button");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("repository name is empty", () => {
const input = rendered.getByTestId("custom-helm-repository-name-input") as HTMLInputElement;
expect(input.value).toBe("");
});
it("repository url is empty", () => {
const input = rendered.getByTestId("custom-helm-repository-url-input") as HTMLInputElement;
expect(input.value).toBe("");
});
});
});
});
describe("when showing the maximal options", () => {
beforeEach(() => {
const button = rendered.getByTestId("toggle-maximal-options-for-custom-helm-repository-button");
fireEvent.click(button);
});
it("shows maximal options", () => {
const maximalOptions = rendered.getByTestId("maximal-options-for-custom-helm-repository-dialog");
expect(maximalOptions).toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("given closing the dialog, when reopening the dialog, still shows maximal options", () => {
const cancelButton = rendered.getByTestId("custom-helm-repository-cancel-button");
fireEvent.click(cancelButton);
const openButton = rendered.getByTestId("add-custom-helm-repo-button");
fireEvent.click(openButton);
const maximalOptions = rendered.getByTestId("maximal-options-for-custom-helm-repository-dialog");
expect(maximalOptions).toBeInTheDocument();
});
describe("when hiding maximal options", () => {
beforeEach(() => {
const button = rendered.getByTestId("toggle-maximal-options-for-custom-helm-repository-button");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show maximal options anymore", () => {
const maximalOptions = rendered.queryByTestId("maximal-options-for-custom-helm-repository-dialog");
expect(maximalOptions).not.toBeInTheDocument();
});
});
describe("when inputted maximal options", () => {
beforeEach(async () => {
[
{ selector: "username-input", value: "some-username" },
{ selector: "password-input", value: "some-password" },
{ selector: "ca-cert-file-input", value: "some-ca-cert-file" },
{ selector: "cert-file-input", value: "some-cert-file" },
{ selector: "key-file-input", value: "some-key-file" },
].forEach(({ selector, value }) => {
const input = rendered.getByTestId(`custom-helm-repository-${selector}`);
fireEvent.change(input, { target: { value }});
});
const checkbox = rendered.getByTestId(`custom-helm-repository-verify-tls-input`);
fireEvent.click(checkbox);
jest.runOnlyPendingTimers();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("when submitted and some time passes, adds the repository with maximal options", () => {
const submitButton = rendered.getByTestId("custom-helm-repository-submit-button");
fireEvent.click(submitButton);
// TODO: Remove when debounce is removed from WizardStep.submit
jest.runOnlyPendingTimers();
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
[
"repo",
"add",
"some-custom-repository",
"http://some.url",
"--insecure-skip-tls-verify",
"--username",
"some-username",
"--password",
"some-password",
"--ca-file",
"some-ca-cert-file",
"--key-file",
"some-key-file",
"--cert-file",
"some-cert-file",
],
{
maxBuffer: 34359738368,
env: {},
},
);
});
});
});
});
});
});
});
});
@@ -0,0 +1,297 @@
/**
* 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";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { ExecFile } from "../../common/fs/exec-file.injectable";
import execFileInjectable from "../../common/fs/exec-file.injectable";
import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable";
import getActiveHelmRepositoriesInjectable from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import type { HelmRepo } from "../../common/helm/helm-repo";
import requestPublicHelmRepositoriesInjectable from "./child-features/preferences/renderer/adding-of-public-helm-repository/public-helm-repositories/request-public-helm-repositories.injectable";
import showSuccessNotificationInjectable from "../../renderer/components/notifications/show-success-notification.injectable";
import showErrorNotificationInjectable from "../../renderer/components/notifications/show-error-notification.injectable";
import type { AsyncResult } from "../../common/utils/async-result";
describe("add helm repository from list in preferences", () => {
let builder: ApplicationBuilder;
let showSuccessNotificationMock: jest.Mock;
let showErrorNotificationMock: jest.Mock;
let rendered: RenderResult;
let execFileMock: AsyncFnMock<ExecFile>;
let getActiveHelmRepositoriesMock: AsyncFnMock<() => Promise<AsyncResult<HelmRepo[]>>>;
let callForPublicHelmRepositoriesMock: AsyncFnMock<() => Promise<HelmRepo[]>>;
beforeEach(async () => {
builder = getApplicationBuilder();
execFileMock = asyncFn();
getActiveHelmRepositoriesMock = asyncFn();
callForPublicHelmRepositoriesMock = asyncFn();
showSuccessNotificationMock = jest.fn();
showErrorNotificationMock = jest.fn();
builder.beforeApplicationStart((mainDi) => {
mainDi.override(getActiveHelmRepositoriesInjectable, () => getActiveHelmRepositoriesMock);
mainDi.override(execFileInjectable, () => execFileMock);
mainDi.override(helmBinaryPathInjectable, () => "some-helm-binary-path");
});
builder.beforeWindowStart((windowDi) => {
windowDi.override(showSuccessNotificationInjectable, () => showSuccessNotificationMock);
windowDi.override(showErrorNotificationInjectable, () => showErrorNotificationMock);
windowDi.override(requestPublicHelmRepositoriesInjectable, () => callForPublicHelmRepositoriesMock);
});
rendered = await builder.render();
});
describe("when navigating to preferences containing helm repositories", () => {
beforeEach(() => {
builder.preferences.navigate();
builder.preferences.navigation.click("kubernetes");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("calls for public repositories", () => {
expect(callForPublicHelmRepositoriesMock).toHaveBeenCalled();
});
it("calls for active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
describe("when both active and public repositories resolve", () => {
beforeEach(async () => {
await Promise.all([
callForPublicHelmRepositoriesMock.resolve([
{
name: "Some already active repository",
url: "some-url",
cacheFilePath: "/some-cache-file-for-active",
},
{
name: "Some to be added repository",
url: "some-other-url",
cacheFilePath: "/some-cache-file-for-non-active",
},
]),
getActiveHelmRepositoriesMock.resolve({
callWasSuccessful: true,
response: [{
name: "Some already active repository",
url: "some-url",
cacheFilePath: "/some-cache-file-for-active",
}],
}),
]);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when select for adding public repositories is clicked", () => {
beforeEach(() => {
builder.select.openMenu(
"selection-of-active-public-helm-repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when deactive public repository is selected", () => {
beforeEach(async () => {
getActiveHelmRepositoriesMock.mockClear();
builder.select.selectOption(
"selection-of-active-public-helm-repository",
"Some to be added repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("adds the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "add", "Some to be added repository", "some-other-url"],
{
maxBuffer: 34359738368,
env: {},
},
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
describe("when adding rejects", () => {
beforeEach(async () => {
await execFileMock.resolve({
callWasSuccessful: false,
error: Object.assign(new Error("Some error"), {
stderr: "",
}),
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Some error",
);
});
it("does not show success notification", () => {
expect(showSuccessNotificationMock).not.toHaveBeenCalled();
});
it("does not show dialog anymore", () => {
expect(rendered.queryByTestId("add-custom-helm-repository-dialog")).not.toBeInTheDocument();
});
it("does not reload active repositories", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
});
describe("when adding resolves", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "add", "Some to be added repository", "some-other-url"],
],
{
callWasSuccessful: true,
response: "",
},
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
it("shows success notification", () => {
expect(showSuccessNotificationMock).toHaveBeenCalledWith(
"Helm repository Some to be added repository has been added.",
);
});
describe("when active repositories resolve again", () => {
beforeEach(async () => {
await getActiveHelmRepositoriesMock.resolve({
callWasSuccessful: true,
response: [
{
name: "Some already active repository",
url: "some-url",
cacheFilePath: "/some-cache-file-for-active",
},
{
name: "Some to be added repository",
url: "some-other-url",
cacheFilePath: "/some-cache-file-for-non-active",
},
],
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when select for selecting active repositories is clicked", () => {
beforeEach(() => {
builder.select.openMenu(
"selection-of-active-public-helm-repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when active repository is selected", () => {
beforeEach(() => {
execFileMock.mockClear();
getActiveHelmRepositoriesMock.mockClear();
builder.select.selectOption(
"selection-of-active-public-helm-repository",
"Some already active repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("removes the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "remove", "Some already active repository"],
{
maxBuffer: 34359738368,
env: {},
},
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
describe("when removing resolves", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "remove", "Some already active repository"],
],
{
callWasSuccessful: true,
response: "",
},
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
});
});
});
});
});
});
});
});
});
});
@@ -0,0 +1,50 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import { getActiveHelmRepositoriesChannel } from "../../../../../common/helm/get-active-helm-repositories-channel";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import showErrorNotificationInjectable from "../../../../../renderer/components/notifications/show-error-notification.injectable";
import helmRepositoriesErrorStateInjectable from "./helm-repositories-error-state.injectable";
import { runInAction } from "mobx";
const activeHelmRepositoriesInjectable = getInjectable({
id: "active-helm-repositories",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const helmRepositoriesErrorState = di.inject(
helmRepositoriesErrorStateInjectable,
);
return asyncComputed({
getValueFromObservedPromise: async () => {
const result = await requestFromChannel(
getActiveHelmRepositoriesChannel,
);
if (result.callWasSuccessful) {
return result.response;
} else {
showErrorNotification(result.error);
runInAction(() =>
helmRepositoriesErrorState.set({
controlsAreShown: false,
errorMessage: result.error,
}),
);
return [];
}
},
valueWhenPending: [],
});
},
});
export default activeHelmRepositoriesInjectable;
@@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.AddHelmRepoDialog {
--flex-gap: #{$margin * 1.5};
.accordion {
font-weight: bold;
align-self: flex-start;
padding: 0;
&:focus:not(:active) {
box-shadow: none;
}
}
.Icon {
--color-active: black;
}
.fields-title {
display: grid;
grid-template-columns: min-content auto;
align-items: center;
gap: $margin * 0.5;
}
}
@@ -0,0 +1,154 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./add-helm-repo-dialog.scss";
import React from "react";
import { Wizard, WizardStep } from "../../../../../../renderer/components/wizard";
import { Input } from "../../../../../../renderer/components/input";
import { isUrl, systemName } from "../../../../../../renderer/components/input/input_validators";
import { withInjectables } from "@ogre-tools/injectable-react";
import customHelmRepoInjectable from "./custom-helm-repo.injectable";
import type { HelmRepo } from "../../../../../../common/helm/helm-repo";
import { observer } from "mobx-react";
import type { IObservableValue } from "mobx";
import { action } from "mobx";
import submitCustomHelmRepositoryInjectable from "./submit-custom-helm-repository.injectable";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
import { Button } from "../../../../../../renderer/components/button";
import { Icon } from "../../../../../../renderer/components/icon";
import maximalCustomHelmRepoOptionsAreShownInjectable from "./maximal-custom-helm-repo-options-are-shown.injectable";
import { SubTitle } from "../../../../../../renderer/components/layout/sub-title";
import { Checkbox } from "../../../../../../renderer/components/checkbox";
import { HelmFileInput } from "./helm-file-input/helm-file-input";
import { toJS } from "../../../../../../common/utils";
interface Dependencies {
helmRepo: HelmRepo;
hideDialog: () => void;
submitCustomRepository: (repository: HelmRepo) => Promise<void>;
maximalOptionsAreShown: IObservableValue<boolean>;
}
const NonInjectedActivationOfCustomHelmRepositoryDialogContent = observer(({
helmRepo,
submitCustomRepository,
maximalOptionsAreShown,
hideDialog,
} : Dependencies) => (
<Wizard header={<h5>Add custom Helm Repo</h5>} done={hideDialog}>
<WizardStep
contentClass="flow column"
nextLabel="Add"
next={() => submitCustomRepository(toJS(helmRepo))}
testIdForNext="custom-helm-repository-submit-button"
testIdForPrev="custom-helm-repository-cancel-button"
>
<div className="flex column gaps" data-testid="add-custom-helm-repository-dialog">
<Input
autoFocus
required
placeholder="Helm repo name"
trim
validators={systemName}
value={helmRepo.name}
onChange={action(v => helmRepo.name = v)}
data-testid="custom-helm-repository-name-input"
/>
<Input
required
placeholder="URL"
validators={isUrl}
value={helmRepo.url}
onChange={action(v => helmRepo.url = v)}
data-testid="custom-helm-repository-url-input"
/>
<Button
plain
className="accordion"
data-testid="toggle-maximal-options-for-custom-helm-repository-button"
onClick={action(() => maximalOptionsAreShown.set(!maximalOptionsAreShown.get()))}
>
More
<Icon
small
tooltip="More"
material={maximalOptionsAreShown.get() ? "remove" : "add"}
/>
</Button>
{maximalOptionsAreShown.get() && (
<div data-testid="maximal-options-for-custom-helm-repository-dialog">
<SubTitle title="Security settings" />
<Checkbox
label="Skip TLS certificate checks for the repository"
value={helmRepo.insecureSkipTlsVerify}
onChange={action(v => {
helmRepo.insecureSkipTlsVerify = v;
})}
data-testid="custom-helm-repository-verify-tls-input"
/>
<HelmFileInput
placeholder="Key file"
value={helmRepo.keyFile || ""}
setValue={action((value) => helmRepo.keyFile = value)}
fileExtensions={keyExtensions}
data-testid="custom-helm-repository-key-file-input"
/>
<HelmFileInput
placeholder="Ca file"
value={helmRepo.caFile || ""}
setValue={action((value) => helmRepo.caFile = value)}
fileExtensions={certExtensions}
data-testid="custom-helm-repository-ca-cert-file-input"
/>
<HelmFileInput
placeholder="Certificate file"
value={helmRepo.certFile || ""}
setValue={action((value) => helmRepo.certFile = value)}
fileExtensions={certExtensions}
data-testid="custom-helm-repository-cert-file-input"
/>
<SubTitle title="Chart Repository Credentials" />
<Input
placeholder="Username"
value={helmRepo.username}
onChange= {action(v => helmRepo.username = v)}
data-testid="custom-helm-repository-username-input"
/>
<Input
type="password"
placeholder="Password"
value={helmRepo.password}
onChange={action(v => helmRepo.password = v)}
data-testid="custom-helm-repository-password-input"
/>
</div>
)}
</div>
</WizardStep>
</Wizard>
));
export const AddingOfCustomHelmRepositoryDialogContent = withInjectables<Dependencies>(NonInjectedActivationOfCustomHelmRepositoryDialogContent, {
getProps: (di) => ({
helmRepo: di.inject(customHelmRepoInjectable),
hideDialog: di.inject(hideDialogForAddingCustomHelmRepositoryInjectable),
submitCustomRepository: di.inject(submitCustomHelmRepositoryInjectable),
maximalOptionsAreShown: di.inject(maximalCustomHelmRepoOptionsAreShownInjectable),
}),
});
const keyExtensions = ["key", "keystore", "jks", "p12", "pfx", "pem"];
const certExtensions = ["crt", "cer", "ca-bundle", "p7b", "p7c", "p7s", "p12", "pfx", "pem"];
@@ -0,0 +1,47 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./add-helm-repo-dialog.scss";
import React from "react";
import { Dialog } from "../../../../../../renderer/components/dialog";
import { withInjectables } from "@ogre-tools/injectable-react";
import { AddingOfCustomHelmRepositoryDialogContent } from "./adding-of-custom-helm-repository-dialog-content";
import addingOfCustomHelmRepositoryDialogIsVisibleInjectable from "./dialog-visibility/adding-of-custom-helm-repository-dialog-is-visible.injectable";
import type { IObservableValue } from "mobx";
import { observer } from "mobx-react";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
interface Dependencies {
contentIsVisible: IObservableValue<boolean>;
hideDialog: () => void;
}
const NonInjectedActivationOfCustomHelmRepositoryDialog = observer(({
contentIsVisible,
hideDialog,
}: Dependencies) => (
<div>
<Dialog
className="AddHelmRepoDialog"
isOpen={contentIsVisible.get()}
close={hideDialog}
>
{contentIsVisible.get() && <AddingOfCustomHelmRepositoryDialogContent />}
</Dialog>
</div>
));
export const AddingOfCustomHelmRepositoryDialog = withInjectables<Dependencies>(
NonInjectedActivationOfCustomHelmRepositoryDialog,
{
getProps: (di) => ({
contentIsVisible: di.inject(addingOfCustomHelmRepositoryDialogIsVisibleInjectable),
hideDialog: di.inject(hideDialogForAddingCustomHelmRepositoryInjectable),
}),
},
);
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import { Button } from "../../../../../../renderer/components/button";
import showDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/show-dialog-for-adding-custom-helm-repository.injectable";
interface Dependencies {
showDialog: () => void;
}
const NonInjectedActivationOfCustomHelmRepositoryOpenButton = ({ showDialog }: Dependencies) => (
<Button
primary
label="Add Custom Helm Repo"
onClick={showDialog}
data-testid="add-custom-helm-repo-button"
/>
);
export const AddingOfCustomHelmRepositoryOpenButton = withInjectables<Dependencies>(
NonInjectedActivationOfCustomHelmRepositoryOpenButton,
{
getProps: (di) => ({
showDialog: di.inject(showDialogForAddingCustomHelmRepositoryInjectable),
}),
},
);
@@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { observable } from "mobx";
import type { HelmRepo } from "../../../../../../common/helm/helm-repo";
const customHelmRepoInjectable = getInjectable({
id: "custom-helm-repo",
instantiate: () => observable.object<HelmRepo>({
name: "",
url: "",
username: "",
password: "",
insecureSkipTlsVerify: false,
caFile: "",
keyFile: "",
certFile: "",
cacheFilePath: "",
}),
lifecycle: lifecycleEnum.transient,
});
export default customHelmRepoInjectable;
@@ -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 { observable } from "mobx";
const addingOfCustomHelmRepositoryDialogIsVisibleInjectable = getInjectable({
id: "adding-of-custom-helm-repository-dialog-is-visible",
instantiate: () => observable.box(false),
});
export default addingOfCustomHelmRepositoryDialogIsVisibleInjectable;
@@ -0,0 +1,21 @@
/**
* 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 { action } from "mobx";
import addingOfCustomHelmRepositoryDialogIsVisibleInjectable from "./adding-of-custom-helm-repository-dialog-is-visible.injectable";
const hideDialogForAddingCustomHelmRepositoryInjectable = getInjectable({
id: "hide-dialog-for-adding-custom-helm-repository",
instantiate: (di) => {
const state = di.inject(addingOfCustomHelmRepositoryDialogIsVisibleInjectable);
return action(() => {
state.set(false);
});
},
});
export default hideDialogForAddingCustomHelmRepositoryInjectable;
@@ -0,0 +1,21 @@
/**
* 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 { action } from "mobx";
import addingOfCustomHelmRepositoryDialogIsVisibleInjectable from "./adding-of-custom-helm-repository-dialog-is-visible.injectable";
const showDialogForAddingCustomHelmRepositoryInjectable = getInjectable({
id: "show-dialog-for-adding-custom-helm-repository",
instantiate: (di) => {
const state = di.inject(addingOfCustomHelmRepositoryDialogIsVisibleInjectable);
return action(() => {
state.set(true);
});
},
});
export default showDialogForAddingCustomHelmRepositoryInjectable;
@@ -0,0 +1,32 @@
/**
* 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 { FileFilter } from "electron";
import type { PathPickOpts } from "../../../../../../../renderer/components/path-picker";
import openPathPickingDialogInjectable from "../../../../../../path-picking-dialog/renderer/pick-paths.injectable";
export interface RequestFilePathOptions extends Pick<PathPickOpts, "onCancel" | "onPick"> {
filter: FileFilter;
}
export type RequestFilePaths = (options: RequestFilePathOptions) => Promise<void>;
const requestFilePathsInjectable = getInjectable({
id: "request-file-paths",
instantiate: (di): RequestFilePaths => {
const openPathPickingDialog = di.inject(openPathPickingDialogInjectable);
return async ({ filter, ...opts }) => await openPathPickingDialog({
properties: ["openFile", "showHiddenFiles"],
message: "Select file",
buttonLabel: "Use file",
filters: [filter, { name: "Any", extensions: ["*"] }],
...opts,
});
},
});
export default requestFilePathsInjectable;
@@ -0,0 +1,69 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { InputValidator } from "../../../../../../../renderer/components/input";
import { Input } from "../../../../../../../renderer/components/input";
import { Icon } from "../../../../../../../renderer/components/icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import type { RequestFilePaths } from "./get-file-paths.injectable";
import requestFilePathsInjectable from "./get-file-paths.injectable";
import isPathInjectable from "../../../../../../../renderer/components/input/validators/is-path.injectable";
interface HelmFileInputProps {
placeholder: string;
fileExtensions: string[];
value: string;
setValue: (value: string) => void;
"data-testid"?: string;
}
interface Dependencies {
requestFilePaths: RequestFilePaths;
isPath: InputValidator<true>;
}
const NonInjectedHelmFileInput = ({
placeholder,
value,
setValue,
fileExtensions,
requestFilePaths,
isPath,
"data-testid": testId,
}: Dependencies & HelmFileInputProps) => (
<div className="flex gaps align-center">
<Input
placeholder={placeholder}
validators={isPath}
className="box grow"
value={value}
onChange={(v) => setValue(v)}
data-testid={testId}
/>
<Icon
material="folder"
onClick={() => void requestFilePaths({
filter: {
name: placeholder,
extensions: fileExtensions,
},
onPick: (filePaths) => {
if (filePaths.length) {
setValue(filePaths[0]);
}
},
})}
tooltip="Browse"
/>
</div>
);
export const HelmFileInput = withInjectables<Dependencies, HelmFileInputProps>(NonInjectedHelmFileInput, {
getProps: (di, props) => ({
...props,
requestFilePaths: di.inject(requestFilePathsInjectable),
isPath: di.inject(isPathInjectable),
}),
});
@@ -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 { observable } from "mobx";
const maximalCustomHelmRepoOptionsAreShownInjectable = getInjectable({
id: "maximal-custom-helm-repo-options-are-shown",
instantiate: () => observable.box(false),
});
export default maximalCustomHelmRepoOptionsAreShownInjectable;
@@ -0,0 +1,25 @@
/**
* 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 { HelmRepo } from "../../../../../../common/helm/helm-repo";
import addHelmRepositoryInjectable from "../adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
const submitCustomHelmRepositoryInjectable = getInjectable({
id: "submit-custom-helm-repository",
instantiate: (di) => {
const addHelmRepository = di.inject(addHelmRepositoryInjectable);
const hideDialog = di.inject(hideDialogForAddingCustomHelmRepositoryInjectable);
return async (repository: HelmRepo) => {
await addHelmRepository(repository);
hideDialog();
};
},
});
export default submitCustomHelmRepositoryInjectable;
@@ -0,0 +1,80 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import publicHelmRepositoriesInjectable from "./public-helm-repositories/public-helm-repositories.injectable";
import type { HelmRepo } from "../../../../../../common/helm/helm-repo";
import type { SelectOption } from "../../../../../../renderer/components/select";
import { Select } from "../../../../../../renderer/components/select";
import { Icon } from "../../../../../../renderer/components/icon";
import { observer } from "mobx-react";
import type { SingleValue } from "react-select";
import selectHelmRepositoryInjectable from "./select-helm-repository/select-helm-repository.injectable";
import { matches } from "lodash/fp";
import activeHelmRepositoriesInjectable from "../active-helm-repositories.injectable";
interface Dependencies {
publicRepositories: IAsyncComputed<HelmRepo[]>;
activeRepositories: IAsyncComputed<HelmRepo[]>;
selectRepository: (value: SingleValue<SelectOption<HelmRepo>>) => void;
}
const NonInjectedAddingOfPublicHelmRepository = observer(({
publicRepositories,
activeRepositories,
selectRepository,
}: Dependencies) => {
const dereferencesPublicRepositories = publicRepositories.value.get();
const dereferencesActiveRepositories = activeRepositories.value.get();
const valuesAreLoading = publicRepositories.pending.get() || activeRepositories.pending.get();
const repositoryOptions = dereferencesPublicRepositories.map(repository => ({
value: repository,
label: repository.name,
isSelected: !!dereferencesActiveRepositories.find(matches({ name: repository.name })),
}));
return (
<Select
id="selection-of-active-public-helm-repository"
placeholder="Repositories"
isLoading={valuesAreLoading}
isDisabled={valuesAreLoading}
options={repositoryOptions}
onChange={selectRepository}
value={dereferencesPublicRepositories}
formatOptionLabel={formatOptionLabel}
controlShouldRenderValue={false}
className="box grow"
themeName="lens"
/>
);
});
export const AddingOfPublicHelmRepository = withInjectables<Dependencies>(
NonInjectedAddingOfPublicHelmRepository,
{
getProps: (di) => ({
publicRepositories: di.inject(publicHelmRepositoriesInjectable),
activeRepositories: di.inject(activeHelmRepositoriesInjectable),
selectRepository: di.inject(selectHelmRepositoryInjectable),
}),
},
);
const formatOptionLabel = ({ value, isSelected }: SelectOption<HelmRepo>) => (
<div className="flex gaps">
<span>{value.name}</span>
{isSelected && (
<Icon
small
material="check"
className="box right" />
)}
</div>
);
@@ -0,0 +1,18 @@
/**
* 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 requestPublicHelmRepositoriesInjectable from "./request-public-helm-repositories.injectable";
const publicHelmRepositoriesInjectable = getInjectable({
id: "public-helm-repositories",
instantiate: (di) => asyncComputed({
getValueFromObservedPromise: di.inject(requestPublicHelmRepositoriesInjectable),
valueWhenPending: [],
}),
});
export default publicHelmRepositoriesInjectable;
@@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverrideForFunction } from "../../../../../../../common/test-utils/get-global-override-for-function";
import requestPublicHelmRepositoriesInjectable from "./request-public-helm-repositories.injectable";
export default getGlobalOverrideForFunction(requestPublicHelmRepositoriesInjectable);
@@ -0,0 +1,40 @@
/**
* 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 { sortBy } from "lodash/fp";
import proxyDownloadJsonInjectable from "../../../../../../../common/fetch/download-json/proxy.injectable";
import { withTimeout } from "../../../../../../../common/fetch/timeout-controller";
import type { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import loggerInjectable from "../../../../../../../common/logger.injectable";
const publicHelmReposUrl = "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json";
const requestPublicHelmRepositoriesInjectable = getInjectable({
id: "request-public-helm-repositories",
instantiate: (di) => {
const downloadJson = di.inject(proxyDownloadJsonInjectable);
const logger = di.inject(loggerInjectable);
return async (): Promise<HelmRepo[]> => {
const controller = withTimeout(10_000);
const result = await downloadJson(publicHelmReposUrl, {
signal: controller.signal,
});
if (!result.callWasSuccessful) {
logger.warn(`Failed to download public helm repos: ${result.error}`);
return [];
}
return sortBy(repo => repo.name, result.response as HelmRepo[]);
};
},
causesSideEffects: true,
});
export default requestPublicHelmRepositoriesInjectable;
@@ -0,0 +1,41 @@
/**
* 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 { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "../../active-helm-repositories.injectable";
import showErrorNotificationInjectable from "../../../../../../../renderer/components/notifications/show-error-notification.injectable";
import showSuccessNotificationInjectable from "../../../../../../../renderer/components/notifications/show-success-notification.injectable";
import { addHelmRepositoryChannel } from "../../../../../../../common/helm/add-helm-repository-channel";
const addHelmRepositoryInjectable = getInjectable({
id: "add-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
return async (repository: HelmRepo) => {
const result = await requestFromChannel(
addHelmRepositoryChannel,
repository,
);
if (result.callWasSuccessful) {
showSuccessNotification(
`Helm repository ${repository.name} has been added.`,
);
activeHelmRepositories.invalidate();
} else {
showErrorNotification(result.error);
}
};
},
});
export default addHelmRepositoryInjectable;
@@ -0,0 +1,33 @@
/**
* 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 addHelmRepositoryInjectable from "./add-helm-repository.injectable";
import type { SelectOption } from "../../../../../../../renderer/components/select";
import type { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import type { SingleValue } from "react-select";
import removeHelmRepositoryInjectable from "../../remove-helm-repository.injectable";
const selectHelmRepositoryInjectable = getInjectable({
id: "select-helm-repository",
instantiate: (di) => {
const addHelmRepository = di.inject(addHelmRepositoryInjectable);
const removeHelmRepository = di.inject(removeHelmRepositoryInjectable);
return (selected: SingleValue<SelectOption<HelmRepo>>) => {
if (!selected) {
return;
}
if (!selected.isSelected) {
addHelmRepository(selected.value);
} else {
removeHelmRepository(selected.value);
}
};
},
});
export default selectHelmRepositoryInjectable;
@@ -0,0 +1,23 @@
/**
* 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 { HelmCharts } from "./helm-charts";
import { preferenceItemInjectionToken } from "../../../../preferences/renderer/preference-items/preference-item-injection-token";
const helmChartsPreferenceBlockInjectable = getInjectable({
id: "helm-charts-preference-block",
instantiate: () => ({
kind: "block" as const,
id: "helm-charts",
parentId: "kubernetes-page",
orderNumber: 30,
Component: HelmCharts,
}),
injectionToken: preferenceItemInjectionToken,
});
export default helmChartsPreferenceBlockInjectable;
@@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.repos {
display: flex;
flex-direction: column;
margin-top: calc(var(--margin) * 2);
}
.repos .repo + .repo {
margin-top: calc(var(--margin) * 1.5);
}
.contents {
display: flex;
gap: var(--padding);
}
.repoName {
font-weight: 500;
}
.repoUrl {
color: var(--textColorDimmed);
}
@@ -0,0 +1,65 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { HelmRepositories } from "./helm-repositories";
import { AddingOfPublicHelmRepository } from "./adding-of-public-helm-repository/adding-of-public-helm-repository";
import { AddingOfCustomHelmRepositoryOpenButton } from "./adding-of-custom-helm-repository/adding-of-custom-helm-repository-open-button";
import { AddingOfCustomHelmRepositoryDialog } from "./adding-of-custom-helm-repository/adding-of-custom-helm-repository-dialog";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { HelmRepositoriesErrorState } from "./helm-repositories-error-state.injectable";
import helmRepositoriesErrorStateInjectable from "./helm-repositories-error-state.injectable";
import type { IObservableValue } from "mobx";
import { observer } from "mobx-react";
import { Notice } from "../../../../../renderer/components/+extensions/notice";
interface Dependencies {
helmRepositoriesErrorState: IObservableValue<HelmRepositoriesErrorState>;
}
const NonInjectedHelmCharts = observer(
({ helmRepositoriesErrorState }: Dependencies) => {
const state = helmRepositoriesErrorState.get();
return (
<section id="helm">
<h2>Helm Charts</h2>
<div>
{!state.controlsAreShown && (
<Notice>
<div className="flex-grow text-center">{state.errorMessage}</div>
</Notice>
)}
{state.controlsAreShown && (
<div data-testid="helm-controls">
<div className="flex gaps">
<AddingOfPublicHelmRepository />
<AddingOfCustomHelmRepositoryOpenButton />
</div>
<HelmRepositories />
<AddingOfCustomHelmRepositoryDialog />
</div>
)}
</div>
</section>
);
},
);
export const HelmCharts = withInjectables<Dependencies>(
NonInjectedHelmCharts,
{
getProps: (di) => ({
helmRepositoriesErrorState: di.inject(helmRepositoriesErrorStateInjectable),
}),
},
);
@@ -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 { observable } from "mobx";
export type HelmRepositoriesErrorState =
| { controlsAreShown: true }
| { controlsAreShown: false; errorMessage: string };
const helmRepositoriesErrorStateInjectable = getInjectable({
id: "helm-repositories-error-state",
instantiate: () =>
observable.box<HelmRepositoriesErrorState>({ controlsAreShown: true }),
});
export default helmRepositoriesErrorStateInjectable;
@@ -0,0 +1,70 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./helm-charts.module.scss";
import React from "react";
import { observer } from "mobx-react";
import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable";
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import { Spinner } from "../../../../../renderer/components/spinner";
import type { HelmRepo } from "../../../../../common/helm/helm-repo";
import { RemovableItem } from "../../../../preferences/renderer/removable-item/removable-item";
import removeHelmRepositoryInjectable from "./remove-helm-repository.injectable";
interface Dependencies {
activeHelmRepositories: IAsyncComputed<HelmRepo[]>;
removeRepository: (repository: HelmRepo) => Promise<void>;
}
const NonInjectedActiveHelmRepositories = observer(({ activeHelmRepositories, removeRepository }: Dependencies) => {
if (activeHelmRepositories.pending.get()) {
return (
<div className={styles.repos}>
<div className="pt-5 relative">
<Spinner center data-testid="helm-repositories-are-loading" />
</div>
</div>
);
}
const repositories = activeHelmRepositories.value.get();
return (
<div className={styles.repos}>
{repositories.map((repository) => (
<RemovableItem
key={repository.name}
onRemove={() => removeRepository(repository)}
className={styles.repo}
data-testid={`remove-helm-repository-${repository.name}`}
>
<div>
<div data-testid={`helm-repository-${repository.name}`} className={styles.repoName}>
{repository.name}
</div>
<div className={styles.repoUrl}>{repository.url}</div>
</div>
</RemovableItem>
))}
</div>
);
});
export const HelmRepositories = withInjectables<Dependencies>(
NonInjectedActiveHelmRepositories,
{
getProps: (di) => ({
activeHelmRepositories: di.inject(activeHelmRepositoriesInjectable),
removeRepository: di.inject(removeHelmRepositoryInjectable),
}),
},
);
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmRepo } from "../../../../../common/helm/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable";
import { removeHelmRepositoryChannel } from "../../../../../common/helm/remove-helm-repository-channel";
const removePublicHelmRepositoryInjectable = getInjectable({
id: "remove-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
return async (repository: HelmRepo) => {
await requestFromChannel(removeHelmRepositoryChannel, repository);
activeHelmRepositories.invalidate();
};
},
});
export default removePublicHelmRepositoryInjectable;
@@ -0,0 +1,991 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { 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 { 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 { RequestCreateHelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
import requestCreateHelmReleaseInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
import currentPathInjectable from "../../../renderer/routes/current-path.injectable";
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 dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import type { RequestHelmCharts } from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import type { RequestHelmChartReadme } from "../../../common/k8s-api/endpoints/helm-charts.api/request-readme.injectable";
import type { RequestHelmChartValues } from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
import requestHelmChartsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import requestHelmChartReadmeInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-readme.injectable";
import requestHelmChartValuesInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
import type { RequestDetailedHelmRelease } from "../../../renderer/components/+helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable";
import requestDetailedHelmReleaseInjectable from "../../../renderer/components/+helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable";
describe("installing helm chart from new tab", () => {
let builder: ApplicationBuilder;
let requestDetailedHelmReleaseMock: AsyncFnMock<RequestDetailedHelmRelease>;
let requestHelmChartsMock: AsyncFnMock<RequestHelmCharts>;
let requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
let requestHelmChartReadmeMock: AsyncFnMock<RequestHelmChartReadme>;
let requestHelmChartValuesMock: AsyncFnMock<RequestHelmChartValues>;
let requestCreateHelmReleaseMock: AsyncFnMock<RequestCreateHelmRelease>;
beforeEach(() => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
requestDetailedHelmReleaseMock = asyncFn();
requestHelmChartsMock = asyncFn();
requestHelmChartVersionsMock = asyncFn();
requestHelmChartReadmeMock = asyncFn();
requestHelmChartValuesMock = asyncFn();
requestCreateHelmReleaseMock = asyncFn();
builder.beforeWindowStart((windowDi) => {
windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage");
windowDi.override(requestDetailedHelmReleaseInjectable, () => requestDetailedHelmReleaseMock);
windowDi.override(requestHelmChartsInjectable, () => requestHelmChartsMock);
windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock);
windowDi.override(requestHelmChartReadmeInjectable, () => requestHelmChartReadmeMock);
windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock);
windowDi.override(requestCreateHelmReleaseInjectable, () => requestCreateHelmReleaseMock);
windowDi.override(getRandomInstallChartTabIdInjectable, () =>
jest
.fn(() => "some-irrelevant-tab-id")
.mockReturnValueOnce("some-first-tab-id")
.mockReturnValueOnce("some-second-tab-id"),
);
});
builder.namespaces.add("default");
builder.namespaces.add("some-other-namespace");
});
describe("given tab for installing chart was not previously opened and application is started", () => {
let rendered: RenderResult;
let windowDi: DiContainer;
beforeEach(async () => {
rendered = await builder.render();
windowDi = builder.applicationWindow.only.di;
const writeJsonFile = windowDi.inject(writeJsonFileInjectable);
await writeJsonFile(
"/some-directory-for-lens-local-storage/some-cluster-id.json",
{
dock: {
height: 300,
tabs: [],
isOpen: false,
},
},
);
const dockStore = windowDi.inject(dockStoreInjectable);
// TODO: Make TerminalWindow unit testable to allow realistic behaviour
dockStore.closeTab("terminal");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when navigating to helm charts", () => {
beforeEach(async () => {
builder.helmCharts.navigate({
chartName: "some-name",
repo: "some-repository",
});
await requestHelmChartsMock.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 requestHelmChartVersionsMock.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 requestHelmChartReadmeMock.resolve({
callWasSuccessful: true,
response: "some-readme",
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when selecting to install the chart", () => {
beforeEach(() => {
requestHelmChartVersionsMock.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(requestHelmChartValuesMock).toHaveBeenCalledWith(
"some-repository",
"some-name",
"some-version",
);
});
it("calls for available versions", () => {
expect(requestHelmChartVersionsMock).toHaveBeenCalledWith(
"some-repository",
"some-name",
);
});
it("shows spinner in dock tab", () => {
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 requestHelmChartVersionsMock.resolve([]);
expect(
rendered.getByTestId("install-chart-tab-spinner"),
).toBeInTheDocument();
});
describe("when default configuration and versions resolve", () => {
beforeEach(async () => {
await requestHelmChartValuesMock.resolve({
callWasSuccessful: true,
response: "some-default-configuration",
});
await requestHelmChartVersionsMock.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(requestCreateHelmReleaseMock).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 requestCreateHelmReleaseMock.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 = windowDi
.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 () => {
requestHelmChartReadmeMock.mockClear();
requestHelmChartVersionsMock.mockClear();
const row = rendered.getByTestId(
"helm-chart-row-for-some-repository-some-other-name",
);
fireEvent.click(row);
await requestHelmChartVersionsMock.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 requestHelmChartReadmeMock.resolve({
callWasSuccessful: true,
response: "some-readme",
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when selecting to install second chart", () => {
beforeEach(() => {
requestHelmChartVersionsMock.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(requestHelmChartValuesMock).toHaveBeenCalledWith(
"some-repository",
"some-other-name",
"some-version",
);
});
it("calls for available versions for the second chart", () => {
expect(requestHelmChartVersionsMock).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 requestHelmChartValuesMock.resolve({
callWasSuccessful: true,
response: "some-other-default-configuration",
});
await requestHelmChartVersionsMock.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(
requestCreateHelmReleaseMock,
).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(() => {
requestHelmChartValuesMock.mockClear();
requestHelmChartVersionsMock.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(
requestHelmChartValuesMock,
).not.toHaveBeenCalled();
});
it("does not call for available versions", () => {
expect(
requestHelmChartVersionsMock,
).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(
requestCreateHelmReleaseMock,
).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(() => {
requestHelmChartVersionsMock.mockClear();
requestHelmChartValuesMock.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(requestHelmChartValuesMock).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(
requestHelmChartVersionsMock,
).not.toHaveBeenCalled();
});
it("install button is disabled", () => {
expect(installButton).toHaveAttribute("disabled");
});
it("stores the selected version", async () => {
const readJsonFile = windowDi.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 requestHelmChartValuesMock.resolve({
callWasSuccessful: true,
response: "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(
requestCreateHelmReleaseMock,
).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 = windowDi.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(requestCreateHelmReleaseMock).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 requestHelmChartValuesMock.resolve({
callWasSuccessful: true,
response: "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 = windowDi.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(requestCreateHelmReleaseMock).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 requestHelmChartValuesMock.resolve({
callWasSuccessful: true,
response: "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 = windowDi.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(requestCreateHelmReleaseMock).toHaveBeenCalledWith({
chart: "some-name",
name: "some-custom-name",
namespace: "default",
repo: "some-repository",
values: "some-default-configuration",
version: "some-version",
});
});
});
});
});
});
});
});
@@ -0,0 +1,209 @@
/**
* 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 writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import { TabKind } from "../../../renderer/components/dock/dock/store";
import requestCreateHelmReleaseInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import type { RequestHelmChartValues } from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
import requestHelmChartValuesInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
describe("installing helm chart from previously opened tab", () => {
let builder: ApplicationBuilder;
let requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
let requestHelmChartValuesMock: AsyncFnMock<RequestHelmChartValues>;
beforeEach(() => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
requestHelmChartVersionsMock = asyncFn();
requestHelmChartValuesMock = asyncFn();
builder.namespaces.add("default");
builder.namespaces.add("some-other-namespace");
builder.beforeWindowStart((windowDi) => {
windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage");
windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock);
windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock);
windowDi.override(requestCreateHelmReleaseInjectable, () => jest.fn());
windowDi.override(getRandomInstallChartTabIdInjectable, () =>
jest
.fn(() => "some-irrelevant-tab-id")
.mockReturnValueOnce("some-first-tab-id"),
);
});
});
describe("given tab for installing chart was previously opened, when application is started", () => {
let rendered: RenderResult;
beforeEach(async () => {
builder.beforeWindowStart(async (windowDi) => {
const writeJsonFile = windowDi.inject(writeJsonFileInjectable);
await writeJsonFile(
"/some-directory-for-lens-local-storage/some-cluster-id.json",
{
dock: {
height: 300,
tabs: [
{
id: "some-first-tab-id",
kind: TabKind.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();
});
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(requestHelmChartValuesMock).toHaveBeenCalledWith(
"some-repository",
"some-name",
"some-other-version",
);
});
it("calls for available versions", () => {
expect(requestHelmChartVersionsMock).toHaveBeenCalledWith(
"some-repository",
"some-name",
);
});
describe("when configuration and version resolves", () => {
beforeEach(async () => {
await requestHelmChartValuesMock.resolve({
callWasSuccessful: true,
response: "some-default-configuration",
});
await requestHelmChartVersionsMock.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");
});
});
});
});
@@ -0,0 +1,357 @@
/**
* 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 { 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 requestCreateHelmReleaseInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
import { flushPromises } from "../../../common/test-utils/flush-promises";
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 { RequestHelmCharts } from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import type { RequestHelmChartReadme } from "../../../common/k8s-api/endpoints/helm-charts.api/request-readme.injectable";
import requestHelmChartsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import requestHelmChartReadmeInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-readme.injectable";
import requestHelmChartValuesInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
describe("opening dock tab for installing helm chart", () => {
let builder: ApplicationBuilder;
let requestHelmChartsMock: AsyncFnMock<RequestHelmCharts>;
let requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
let requestHelmChartReadmeMock: AsyncFnMock<RequestHelmChartReadme>;
let requestHelmChartValuesMock: jest.Mock;
beforeEach(() => {
builder = getApplicationBuilder();
requestHelmChartsMock = asyncFn();
requestHelmChartVersionsMock = asyncFn();
requestHelmChartReadmeMock = asyncFn();
requestHelmChartValuesMock = jest.fn();
builder.beforeWindowStart((windowDi) => {
windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage");
windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
windowDi.override(requestHelmChartsInjectable, () => requestHelmChartsMock);
windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock);
windowDi.override(requestHelmChartReadmeInjectable, () => requestHelmChartReadmeMock);
windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock);
windowDi.override(requestCreateHelmReleaseInjectable, () => jest.fn());
windowDi.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 windowDi = builder.applicationWindow.only.di;
const dockStore = windowDi.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(requestHelmChartsMock).toHaveBeenCalled();
});
describe("when charts resolve", () => {
beforeEach(async () => {
await requestHelmChartsMock.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: [{
email: "some@foo.com",
name: "Some Foo",
}],
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: [{
email: "other@bar.com",
name: "Other Bar",
}],
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(requestHelmChartVersionsMock).toHaveBeenCalledWith(
"some-repository",
"some-name",
);
});
it("shows spinner", () => {
expect(
rendered.getByTestId("spinner-for-chart-details"),
).toBeInTheDocument();
});
describe("when chart versions resolve", () => {
beforeEach(async () => {
await requestHelmChartVersionsMock.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: [{
email: "some@foo.com",
name: "Some Foo",
}],
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: [{
email: "other@bar.com",
name: "Other Bar",
}],
deprecated: false,
}),
]);
});
it("calls for chart readme for the version", () => {
expect(requestHelmChartReadmeMock).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 requestHelmChartReadmeMock.resolve({
callWasSuccessful: true,
response: "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(() => {
requestHelmChartReadmeMock.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(requestHelmChartReadmeMock).toHaveBeenCalledWith(
"some-repository",
"some-name",
"some-other-version",
);
});
describe("when readme resolves", () => {
beforeEach(async () => {
await requestHelmChartReadmeMock.resolve({
callWasSuccessful: true,
response: "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(requestHelmChartValuesMock).toHaveBeenCalledWith(
"some-repository",
"some-name",
"some-other-version",
);
});
});
describe("when readme rejects", () => {
beforeEach(async () => {
await requestHelmChartReadmeMock.resolve({
callWasSuccessful: false,
error: "some-error",
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
});
describe("when selecting to install the chart", () => {
beforeEach(() => {
requestHelmChartVersionsMock.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();
});
});
});
});
});
});
});
});
@@ -0,0 +1,512 @@
/**
* 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";
import type { ReadYamlFile } from "../../common/fs/read-yaml-file.injectable";
import readYamlFileInjectable from "../../common/fs/read-yaml-file.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { HelmRepositoriesFromYaml } from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import execFileInjectable, { type ExecFile } from "../../common/fs/exec-file.injectable";
import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable";
import loggerInjectable from "../../common/logger.injectable";
import type { Logger } from "../../common/logger";
import requestPublicHelmRepositoriesInjectable from "./child-features/preferences/renderer/adding-of-public-helm-repository/public-helm-repositories/request-public-helm-repositories.injectable";
import showErrorNotificationInjectable from "../../renderer/components/notifications/show-error-notification.injectable";
import { noop } from "../../common/utils";
describe("listing active helm repositories in preferences", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let readYamlFileMock: AsyncFnMock<ReadYamlFile>;
let execFileMock: AsyncFnMock<ExecFile>;
let loggerStub: Logger;
let showErrorNotificationMock: jest.Mock;
beforeEach(async () => {
builder = getApplicationBuilder();
readYamlFileMock = asyncFn();
execFileMock = asyncFn();
showErrorNotificationMock = jest.fn();
loggerStub = {
error: jest.fn(),
debug: noop,
info: noop,
silly: noop,
warn: noop,
};
builder.beforeApplicationStart((mainDi) => {
mainDi.override(readYamlFileInjectable, () => readYamlFileMock);
mainDi.override(execFileInjectable, () => execFileMock);
mainDi.override(helmBinaryPathInjectable, () => "some-helm-binary-path");
mainDi.override(loggerInjectable, () => loggerStub);
});
builder.beforeWindowStart((windowDi) => {
windowDi.override(showErrorNotificationInjectable, () => showErrorNotificationMock);
windowDi.override(requestPublicHelmRepositoriesInjectable, () => async () => []);
});
rendered = await builder.render();
});
describe("when navigating to preferences containing helm repositories", () => {
beforeEach(async () => {
builder.preferences.navigate();
builder.preferences.navigation.click("kubernetes");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows loader for repositories", () => {
expect(
rendered.getByTestId("helm-repositories-are-loading"),
).toBeInTheDocument();
});
it("calls for helm configuration", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["env"],
{
maxBuffer: 34359738368,
env: {},
},
);
});
it("does not call for updating of repositories yet", () => {
expect(execFileMock).not.toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "update"],
{
maxBuffer: 34359738368,
env: {},
},
);
});
describe("when getting configuration rejects", () => {
beforeEach(async () => {
await execFileMock.resolve({
callWasSuccessful: false,
error: Object.assign(new Error("Some error"), {
stderr: "some-error",
}),
});
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error getting Helm configuration: some-error",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when configuration resolves without path to repository config file", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolveSpecific(
["some-helm-binary-path", ["env"]],
{
callWasSuccessful: true,
response: "HELM_REPOSITORY_CACHE=some-helm-repository-cache-path",
},
);
});
it("logs error", () => {
expect(loggerStub.error).toHaveBeenCalledWith(
"Tried to get Helm repositories, but HELM_REPOSITORY_CONFIG was not present in `$ helm env`.",
);
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error getting Helm configuration: Tried to get Helm repositories, but HELM_REPOSITORY_CONFIG was not present in `$ helm env`.",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when configuration resolves without path to repository cache directory", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolveSpecific(
["some-helm-binary-path", ["env"]],
{
callWasSuccessful: true,
response: "HELM_REPOSITORY_CONFIG=some-helm-repository-config-file.yaml",
},
);
});
it("logs error", () => {
expect(loggerStub.error).toHaveBeenCalledWith(
"Tried to get Helm repositories, but HELM_REPOSITORY_CACHE was not present in `$ helm env`.",
);
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error getting Helm configuration: Tried to get Helm repositories, but HELM_REPOSITORY_CACHE was not present in `$ helm env`.",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when configuration resolves", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolveSpecific(
["some-helm-binary-path", ["env"]],
{
callWasSuccessful: true,
response: [
"HELM_REPOSITORY_CONFIG=some-helm-repository-config-file.yaml",
"HELM_REPOSITORY_CACHE=some-helm-repository-cache-path",
].join("\n"),
},
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("calls for update of repositories", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "update"],
{
maxBuffer: 34359738368,
env: {},
},
);
});
it("does not call for repositories yet", () => {
expect(readYamlFileMock).not.toHaveBeenCalled();
});
describe("when updating repositories reject with any other error", () => {
beforeEach(async () => {
await execFileMock.resolve({
callWasSuccessful: false,
error: Object.assign(new Error("Some error"), {
stderr: "Some error",
}),
});
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error updating Helm repositories: Some error",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when updating repositories reject with error about no existing repositories", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolve({
callWasSuccessful: false,
error: Object.assign(new Error("no repositories found. You must add one before updating"), {
stderr: "no repositories found. You must add one before updating",
}),
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("still shows the loader for repositories", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).toBeInTheDocument();
});
it('adds "bitnami" as default repository', () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"],
{
maxBuffer: 34359738368,
env: {},
},
);
});
describe("when adding default repository reject", () => {
beforeEach(async () => {
await execFileMock.resolve({
callWasSuccessful: false,
error: Object.assign(new Error("Some error"), {
stderr: "Some error",
}),
});
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error when adding default Helm repository: Some error",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when adding of default repository resolves", () => {
beforeEach(async () => {
readYamlFileMock.mockClear();
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
[
"repo",
"add",
"bitnami",
"https://charts.bitnami.com/bitnami",
],
],
{
callWasSuccessful: true,
response: "",
},
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("still shows the loader for repositories", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).toBeInTheDocument();
});
it("calls for repositories again", () => {
expect(readYamlFileMock).toHaveBeenCalledWith(
"some-helm-repository-config-file.yaml",
);
});
describe("when another call for repositories resolve", () => {
beforeEach(async () => {
await readYamlFileMock.resolveSpecific(
["some-helm-repository-config-file.yaml"],
{
repositories: [
{
name: "bitnami",
url: "https://charts.bitnami.com/bitnami",
caFile: "irrelevant",
certFile: "irrelevant",
insecure_skip_tls_verify: false,
keyFile: "irrelevant",
pass_credentials_all: false,
password: "irrelevant",
username: "irrelevant",
},
],
},
);
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("shows the added repository", () => {
const actual = rendered.getByTestId("helm-repository-bitnami");
expect(actual).toBeInTheDocument();
});
});
});
});
describe("when updating repositories resolve", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolveSpecific(
["some-helm-binary-path", ["repo", "update"]],
{
callWasSuccessful: true,
response: "",
},
);
});
it("loads repositories from file system", () => {
expect(readYamlFileMock).toHaveBeenCalledWith(
"some-helm-repository-config-file.yaml",
);
});
describe("when repositories resolves", () => {
beforeEach(async () => {
execFileMock.mockClear();
await readYamlFileMock.resolveSpecific(
["some-helm-repository-config-file.yaml"],
repositoryConfigStub,
);
});
it("does not add default repository", () => {
expect(execFileMock).not.toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"],
{
maxBuffer: 34359738368,
env: {},
},
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("shows repositories in use", () => {
const actual = rendered.getAllByTestId(
/^helm-repository-(some-repository|some-other-repository)$/,
);
expect(actual).toHaveLength(2);
});
});
});
});
});
});
const repositoryConfigStub: HelmRepositoriesFromYaml = {
repositories: [
{
name: "some-repository",
url: "some-repository-url",
caFile: "irrelevant",
certFile: "irrelevant",
insecure_skip_tls_verify: false,
keyFile: "irrelevant",
pass_credentials_all: false,
password: "irrelevant",
username: "irrelevant",
},
{
name: "some-other-repository",
url: "some-other-repository-url",
caFile: "irrelevant",
certFile: "irrelevant",
insecure_skip_tls_verify: false,
keyFile: "irrelevant",
pass_credentials_all: false,
password: "irrelevant",
username: "irrelevant",
},
],
};
@@ -0,0 +1,126 @@
/**
* 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 { 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 { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { ExecFile } from "../../common/fs/exec-file.injectable";
import execFileInjectable from "../../common/fs/exec-file.injectable";
import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable";
import getActiveHelmRepositoriesInjectable from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import type { HelmRepo } from "../../common/helm/helm-repo";
import requestPublicHelmRepositoriesInjectable from "./child-features/preferences/renderer/adding-of-public-helm-repository/public-helm-repositories/request-public-helm-repositories.injectable";
import type { AsyncResult } from "../../common/utils/async-result";
describe("remove helm repository from list of active repositories in preferences", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let getActiveHelmRepositoriesMock: AsyncFnMock<() => Promise<AsyncResult<HelmRepo[]>>>;
let execFileMock: AsyncFnMock<ExecFile>;
beforeEach(async () => {
builder = getApplicationBuilder();
execFileMock = asyncFn();
getActiveHelmRepositoriesMock = asyncFn();
builder.beforeApplicationStart((mainDi) => {
mainDi.override(getActiveHelmRepositoriesInjectable, () => getActiveHelmRepositoriesMock);
mainDi.override(execFileInjectable, () => execFileMock);
mainDi.override(helmBinaryPathInjectable, () => "some-helm-binary-path");
});
builder.beforeWindowStart((windowDi) => {
windowDi.override(requestPublicHelmRepositoriesInjectable, () => async () => []);
});
rendered = await builder.render();
});
describe("when navigating to preferences containing helm repositories", () => {
beforeEach(async () => {
builder.preferences.navigate();
builder.preferences.navigation.click("kubernetes");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when active repositories resolve", () => {
beforeEach(async () => {
getActiveHelmRepositoriesMock.resolve({
callWasSuccessful: true,
response: [{
name: "some-active-repository",
url: "some-url",
cacheFilePath: "/some-cache-file",
}],
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when removing repository", () => {
beforeEach(() => {
execFileMock.mockClear();
getActiveHelmRepositoriesMock.mockClear();
const removeButton = rendered.getByTestId(
"remove-helm-repository-some-active-repository",
);
fireEvent.click(removeButton);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("removes the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "remove", "some-active-repository"],
{
maxBuffer: 34359738368,
env: {},
},
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
describe("when removing resolves", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "remove", "some-active-repository"],
],
{
callWasSuccessful: true,
response: "",
},
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
});
});
});
});
});
@@ -0,0 +1,268 @@
/**
* 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 { 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 { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
import type { RequestHelmCharts } from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import requestHelmChartsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import type { RequestHelmReleaseConfiguration } from "../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import requestHelmReleaseConfigurationInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import type { RequestHelmReleases } from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import requestHelmReleasesInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../../common/test-utils/use-fake-time";
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
describe("New Upgrade Helm Chart Dock Tab", () => {
let builder: ApplicationBuilder;
let renderResult: RenderResult;
let requestHelmReleaseConfigurationMock: AsyncFnMock<RequestHelmReleaseConfiguration>;
let requestHelmReleasesMock: AsyncFnMock<RequestHelmReleases>;
let requestHelmChartsMock: AsyncFnMock<RequestHelmCharts>;
let requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
let navigateToHelmReleases: NavigateToHelmReleases;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
requestHelmReleaseConfigurationMock = asyncFn();
windowDi.override(requestHelmReleaseConfigurationInjectable, () => requestHelmReleaseConfigurationMock);
requestHelmReleasesMock = asyncFn();
windowDi.override(requestHelmReleasesInjectable, () => requestHelmReleasesMock);
requestHelmChartsMock = asyncFn();
windowDi.override(requestHelmChartsInjectable, () => requestHelmChartsMock);
requestHelmChartVersionsMock = asyncFn();
windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock);
navigateToHelmReleases = windowDi.inject(navigateToHelmReleasesInjectable);
});
testUsingFakeTime("2020-01-12 12:00:00");
builder.namespaces.add("my-first-namespace");
builder.namespaces.add("my-second-namespace");
renderResult = await builder.render();
const dockStore = builder.applicationWindow.only.di.inject(dockStoreInjectable);
// TODO: Make TerminalWindow unit testable to allow realistic behaviour
dockStore.closeTab("terminal");
});
describe("given a namespace is selected", () => {
beforeEach(() => {
builder.namespaces.select("my-second-namespace");
});
describe("when navigating to the helm releases view", () => {
beforeEach(() => {
navigateToHelmReleases();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("requests helm releases for the selected namespace", () => {
expect(requestHelmReleasesMock).toBeCalledWith("my-second-namespace");
});
describe("when helm releases resolves", () => {
beforeEach(async () => {
await requestHelmReleasesMock.resolve([
{
appVersion: "some-app-version",
name: "some-name",
namespace: "my-second-namespace",
chart: "some-chart-1.0.0",
status: "some-status",
updated: "some-updated",
revision: "some-revision",
},
]);
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
describe("when clicking the menu for a helm release", () => {
beforeEach(() => {
const helmReleaseMenu = renderResult.getByTestId("menu-actions-icon-for-release-menu-for-my-second-namespace/some-name");
helmReleaseMenu.click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
describe("when clicking the upgrade chart menu item", () => {
beforeEach(() => {
const upgradeHelmChartMenuItem = renderResult.getByTestId("upgrade-chart-menu-item-for-my-second-namespace/some-name");
upgradeHelmChartMenuItem.click();
advanceFakeTime(100);
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("requests all helm charts", () => {
expect(requestHelmChartsMock).toBeCalled();
});
describe("when request for all helm charts resolves", () => {
beforeEach(async () => {
await requestHelmChartsMock.resolve([
HelmChart.create({
apiVersion: "1.2.3",
version: "1.0.0",
created: "at-some-time",
name: "some-chart",
repo: "some-repo",
}),
HelmChart.create({
apiVersion: "1.2.3",
version: "1.0.0",
created: "at-some-third-time",
name: "some-chart",
repo: "some-third-repo",
}),
HelmChart.create({
apiVersion: "1.2.3",
version: "0.9.0",
created: "at-some-other-time",
name: "some-other-chart",
repo: "some-repo",
}),
]);
});
it("requests versions of the helm charts", () => {
expect(requestHelmChartVersionsMock).toBeCalledWith(
"some-repo",
"some-chart",
);
expect(requestHelmChartVersionsMock).toBeCalledWith(
"some-third-repo",
"some-chart",
);
});
it("shows the dock tab of the upgrade chart tab", () => {
expect(renderResult.queryByTestId("dock-tab-content-for-some-irrelevant-random-id")).toBeInTheDocument();
});
it("does not yet show the dock contents of the upgrade chart tab", () => {
expect(renderResult.queryByTestId("upgrade-chart-dock-tab-contents-for-my-second-namespace/some-name")).not.toBeInTheDocument();
});
describe("when the requests of versions of the helm charts resolves", () => {
beforeEach(async () => {
await requestHelmChartVersionsMock.resolveSpecific(
["some-repo", "some-chart"],
[
HelmChart.create({
apiVersion: "1.2.3",
version: "1.0.0",
created: "at-some-time",
name: "some-chart",
repo: "some-repo",
}),
HelmChart.create({
apiVersion: "1.2.3",
version: "1.0.1",
created: "at-some-time",
name: "some-chart",
repo: "some-repo",
}),
],
);
await requestHelmChartVersionsMock.resolveSpecific(
["some-third-repo", "some-chart"],
[
HelmChart.create({
apiVersion: "1.2.3",
version: "0.9.0",
created: "at-some-other-time",
name: "some-other-chart",
repo: "some-repo",
}),
HelmChart.create({
apiVersion: "1.2.3",
version: "0.9.1",
created: "at-some-other-time",
name: "some-other-chart",
repo: "some-repo",
}),
],
);
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows the dock contents of the upgrade chart tab", () => {
expect(renderResult.queryByTestId("upgrade-chart-dock-tab-contents-for-my-second-namespace/some-name")).toBeInTheDocument();
});
it("requests the configuration for the specific helm release", () => {
expect(requestHelmReleaseConfigurationMock).toBeCalledWith(
"some-name",
"my-second-namespace",
true,
);
});
describe("when the configuration request resolves", () => {
beforeEach(async () => {
await requestHelmReleaseConfigurationMock.resolve("some confiration for the helm release");
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
describe("when closing the upgrade chart tab", () => {
beforeEach(() => {
const closeDockTab = renderResult.getByTestId("dock-tab-close-for-some-irrelevant-random-id");
closeDockTab.click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("does so", () => {
expect(renderResult.queryByTestId("dock-tab-content-for-some-irrelevant-random-id")).not.toBeInTheDocument();
});
});
});
});
});
});
});
});
});
});
});