1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introduce competition for activating, deactivating public helm repositories in preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-06-06 10:24:30 +03:00
parent 7a83cd0329
commit bee90e2d83
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
15 changed files with 5839 additions and 1 deletions

View File

@ -0,0 +1,218 @@
/**
* 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 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-repo";
import callForPublicHelmRepositoriesInjectable from "../../renderer/components/+preferences/kubernetes/helm-charts/activation-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable";
describe("activate helm repository from list in preferences", () => {
let applicationBuilder: ApplicationBuilder;
let rendered: RenderResult;
let execFileMock: AsyncFnMock<
ReturnType<typeof execFileInjectable["instantiate"]>
>;
let getActiveHelmRepositoriesMock: AsyncFnMock<() => Promise<HelmRepo[]>>;
let callForPublicHelmRepositoriesMock: AsyncFnMock<() => Promise<HelmRepo[]>>;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder();
execFileMock = asyncFn();
getActiveHelmRepositoriesMock = asyncFn();
callForPublicHelmRepositoriesMock = asyncFn();
applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
rendererDi.override(
callForPublicHelmRepositoriesInjectable,
() => callForPublicHelmRepositoriesMock,
);
mainDi.override(
getActiveHelmRepositoriesInjectable,
() => getActiveHelmRepositoriesMock,
);
mainDi.override(execFileInjectable, () => execFileMock);
mainDi.override(helmBinaryPathInjectable, () => "some-helm-binary-path");
});
rendered = await applicationBuilder.render();
});
describe("when navigating to preferences containing helm repositories", () => {
beforeEach(async () => {
applicationBuilder.preferences.navigate();
applicationBuilder.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" },
{ name: "Some to be activated repository", url: "some-other-url" },
]),
getActiveHelmRepositoriesMock.resolve([
{ name: "Some already active repository", url: "some-url" },
]),
]);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when select for selecting active repositories is clicked", () => {
beforeEach(() => {
applicationBuilder.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();
applicationBuilder.select.selectOption(
"selection-of-active-public-helm-repository",
"Some to be activated repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("activates the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "add", "Some to be activated repository", "some-other-url"],
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
describe("when activating resolves", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "add", "Some to be activated repository", "some-other-url"],
],
"",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
describe("when active repositories resolve again", () => {
beforeEach(async () => {
await getActiveHelmRepositoriesMock.resolve([
{ name: "Some already active repository", url: "some-url" },
{ name: "Some to be activated repository", url: "some-other-url" },
]);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when select for selecting active repositories is clicked", () => {
beforeEach(() => {
applicationBuilder.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();
applicationBuilder.select.selectOption(
"selection-of-active-public-helm-repository",
"Some already active repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("deactivates the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "remove", "Some already active repository"],
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
describe("when deactivating resolves", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "remove", "Some already active repository"],
],
"",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
});
});
});
});
});
});
});
});
});
});

View File

@ -0,0 +1,22 @@
/**
* 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 "../helm-repo";
import type { RequestChannel } from "../utils/channel/request-channel-injection-token";
import { requestChannelInjectionToken } from "../utils/channel/request-channel-injection-token";
export type ActivateHelmRepositoryChannel = RequestChannel<HelmRepo>;
const activateHelmRepositoryChannelInjectable = getInjectable({
id: "activate-helm-repository-channel",
instantiate: (): ActivateHelmRepositoryChannel => ({
id: "activate-helm-repository-channel",
}),
injectionToken: requestChannelInjectionToken,
});
export default activateHelmRepositoryChannelInjectable;

View File

@ -0,0 +1,22 @@
/**
* 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 "../helm-repo";
import type { RequestChannel } from "../utils/channel/request-channel-injection-token";
import { requestChannelInjectionToken } from "../utils/channel/request-channel-injection-token";
export type DeactivateHelmRepositoryChannel = RequestChannel<HelmRepo>;
const deactivateHelmRepositoryChannelInjectable = getInjectable({
id: "deactivate-helm-repository-channel",
instantiate: (): DeactivateHelmRepositoryChannel => ({
id: "deactivate-helm-repository-channel",
}),
injectionToken: requestChannelInjectionToken,
});
export default deactivateHelmRepositoryChannelInjectable;

View File

@ -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 activateHelmRepositoryChannelInjectable from "../../../../common/helm/activate-helm-repository-channel.injectable";
import activateHelmRepositoryInjectable from "./activate-helm-repository.injectable";
import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token";
const activateHelmRepositoryChannelListenerInjectable = getInjectable({
id: "activate-helm-repository-channel-listener",
instantiate: (di) => {
const activateHelmRepository = di.inject(activateHelmRepositoryInjectable);
const channel = di.inject(activateHelmRepositoryChannelInjectable);
return {
channel,
handler: activateHelmRepository,
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default activateHelmRepositoryChannelListenerInjectable;

View File

@ -0,0 +1,62 @@
/**
* 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 execHelmInjectable from "../../exec-helm/exec-helm.injectable";
import type { HelmRepo } from "../../../../common/helm-repo";
import loggerInjectable from "../../../../common/logger.injectable";
const activateHelmRepositoryInjectable = getInjectable({
id: "activate-helm-repository",
instantiate: (di) => {
const execHelm = di.inject(execHelmInjectable);
const logger = di.inject(loggerInjectable);
return async (repo: HelmRepo) => {
const {
name,
url,
insecureSkipTlsVerify,
username,
password,
caFile,
keyFile,
certFile,
} = repo;
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = ["repo", "add", name, url];
if (insecureSkipTlsVerify) {
args.push("--insecure-skip-tls-verify");
}
if (username) {
args.push("--username", username);
}
if (password) {
args.push("--password", password);
}
if (caFile) {
args.push("--ca-file", caFile);
}
if (keyFile) {
args.push("--key-file", keyFile);
}
if (certFile) {
args.push("--cert-file", certFile);
}
await execHelm(...args);
};
},
});
export default activateHelmRepositoryInjectable;

View File

@ -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 { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import deactivateHelmRepositoryInjectable from "./deactivate-helm-repository.injectable";
import deactivateHelmRepositoryChannelInjectable from "../../../../common/helm/deactivate-helm-repository-channel.injectable";
const deactivateHelmRepositoryChannelListenerInjectable = getInjectable({
id: "deactivate-helm-repository-channel-listener",
instantiate: (di) => {
const deactivateHelmRepository = di.inject(deactivateHelmRepositoryInjectable);
const channel = di.inject(deactivateHelmRepositoryChannelInjectable);
return {
channel,
handler: deactivateHelmRepository,
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default deactivateHelmRepositoryChannelListenerInjectable;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import execHelmInjectable from "../../exec-helm/exec-helm.injectable";
import type { HelmRepo } from "../../../../common/helm-repo";
import loggerInjectable from "../../../../common/logger.injectable";
const deactivateHelmRepositoryInjectable = getInjectable({
id: "deactive-helm-repository",
instantiate: (di) => {
const execHelm = di.inject(execHelmInjectable);
const logger = di.inject(loggerInjectable);
return async (repo: HelmRepo) => {
logger.info(`[HELM]: removing repo ${repo.name} (${repo.url})`);
return execHelm(
"repo",
"remove",
repo.name,
);
};
},
});
export default deactivateHelmRepositoryInjectable;

View File

@ -0,0 +1,82 @@
/**
* 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-repo";
import type { SelectOption } from "../../../../select";
import { Select } from "../../../../select";
import { Icon } from "../../../../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 NonInjectedActivationOfPublicHelmRepository = 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 (
<div>
<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"
/>
</div>
);
});
export const ActivationOfPublicHelmRepository = withInjectables<Dependencies>(
NonInjectedActivationOfPublicHelmRepository,
{
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>
);

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { sortBy } from "lodash/fp";
import type { HelmRepo } from "../../../../../../../common/helm-repo";
import { customRequestPromise } from "../../../../../../../common/request";
const callForPublicHelmRepositoriesInjectable = getInjectable({
id: "call-for-public-helm-repositories",
instantiate: () => async (): Promise<HelmRepo[]> => {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
const repositories = res.body as HelmRepo[];
return sortBy(repo => repo.name, repositories);
},
causesSideEffects: true,
});
export default callForPublicHelmRepositoriesInjectable;

View File

@ -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 { asyncComputed } from "@ogre-tools/injectable-react";
import callForPublicHelmRepositoriesInjectable from "./call-for-public-helm-repositories.injectable";
const publicHelmRepositoriesInjectable = getInjectable({
id: "public-helm-repositories",
instantiate: (di) => {
const callForPublicHelmRepositories = di.inject(callForPublicHelmRepositoriesInjectable);
return asyncComputed(async () => {
return await callForPublicHelmRepositories();
}, []);
},
});
export default publicHelmRepositoriesInjectable;

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import activateHelmRepositoryChannelInjectable from "../../../../../../../common/helm/activate-helm-repository-channel.injectable";
import type { HelmRepo } from "../../../../../../../common/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "../../active-helm-repositories.injectable";
const activateHelmRepositoryInjectable = getInjectable({
id: "activate-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const activateHelmRepositoryChannel = di.inject(activateHelmRepositoryChannelInjectable);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
return async (repository: HelmRepo) => {
await requestFromChannel(activateHelmRepositoryChannel, repository);
activeHelmRepositories.invalidate();
};
},
});
export default activateHelmRepositoryInjectable;

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmRepo } from "../../../../../../../common/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "../../active-helm-repositories.injectable";
import deactivateHelmRepositoryChannelInjectable from "../../../../../../../common/helm/deactivate-helm-repository-channel.injectable";
const activatePublicHelmRepositoryInjectable = getInjectable({
id: "deactivate-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const deactivateHelmRepositoryChannel = di.inject(deactivateHelmRepositoryChannelInjectable);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
return async (repository: HelmRepo) => {
await requestFromChannel(deactivateHelmRepositoryChannel, repository);
activeHelmRepositories.invalidate();
};
},
});
export default activatePublicHelmRepositoryInjectable;

View File

@ -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 activateHelmRepositoryInjectable from "./activate-helm-repository.injectable";
import type { SelectOption } from "../../../../../select";
import type { HelmRepo } from "../../../../../../../common/helm-repo";
import type { SingleValue } from "react-select";
import deactivateHelmRepositoryInjectable from "./deactivate-helm-repository.injectable";
const selectHelmRepositoryInjectable = getInjectable({
id: "select-helm-repository",
instantiate: (di) => {
const activateHelmRepository = di.inject(activateHelmRepositoryInjectable);
const deactivateHelmRepository = di.inject(deactivateHelmRepositoryInjectable);
return (selected: SingleValue<SelectOption<HelmRepo>>) => {
if (!selected) {
return;
}
if (!selected.isSelected) {
activateHelmRepository(selected.value);
} else {
deactivateHelmRepository(selected.value);
}
};
},
});
export default selectHelmRepositoryInjectable;

View File

@ -7,12 +7,15 @@
import React from "react";
import { HelmRepositories } from "./helm-repositories";
import { ActivationOfPublicHelmRepository } from "./activation-of-public-helm-repository/activation-of-public-helm-repository";
export const HelmCharts = () => (
<div>
<div className="flex gaps">
<HelmRepositories />
<ActivationOfPublicHelmRepository />
</div>
<HelmRepositories />
</div>
);