Stop using HelmCli from Renderer (#4861)

* Introduce way for execute file

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make typing of HelmRepo shared

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce way to get Helm environment values

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce function to read YAML file

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for listing active helm repositories in preferences

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make sense in name of injectable

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce helper for opening and selecting values of select

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* 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>

* Introduce competition for deactivating helm repository from list of active repositories

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global overrides

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make some tests more deterministic by mocking tooltips

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for activating custom helm repository in preferences

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove old implementation made redundant with competition for preferences of helm repositories

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add success notification for activating custom helm repository

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce way to get single active helm repository

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract responsibilities from god-class and switch to getting helm repositories using competition instead of another god class

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add TODO

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak position of spinner

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Start handling errors when accessing helm repositories

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Handle error about no helm repositories when updating repositories

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove unwarranted function configuration

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global overrides

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove duplication how to acquire binary path for helm

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant comment

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate naming to match Helm's internal

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Relocate file closer to it's relatives

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne SavolainenandMikko Aspiala authored and GitHub committed 2022-06-13 11:42:53 +03:00
1 parent 5420780ae0
commit 1393cc303d
99 files changed
+21409 -1039

No files matched your search

-3
View File
@@ -15,7 +15,6 @@ import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsRendererApi from "../extensions/renderer-api";
import { delay } from "../common/utils";
import { isMac, isDevelopment } from "../common/vars";
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
import { DefaultProps } from "./mui-base-theme";
import configurePackages from "../common/configure-packages";
import * as initializers from "./initializers";
@@ -151,8 +150,6 @@ export async function bootstrap(di: DiContainer) {
extensionInstallationStateStore.bindIpcListeners();
HelmRepoManager.createInstance(); // initialize the manager
// Register additional store listeners
clusterStore.registerIpcListener();
@@ -1,220 +0,0 @@
/**
* 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 type { FileFilter } from "electron";
import { observable, makeObservable, action } from "mobx";
import { observer } from "mobx-react";
import type { DialogProps } from "../dialog";
import { Dialog } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { Input } from "../input";
import { Checkbox } from "../checkbox";
import { Button } from "../button";
import { systemName, isUrl, isPath } from "../input/input_validators";
import { SubTitle } from "../layout/sub-title";
import { Icon } from "../icon";
import { Notifications } from "../notifications";
import { type HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
import { requestOpenFilePickingDialog } from "../../ipc";
export interface AddHelmRepoDialogProps extends Partial<DialogProps> {
onAddRepo: () => void;
}
enum FileType {
CaFile = "caFile",
KeyFile = "keyFile",
CertFile = "certFile",
}
const dialogState = observable.object({
isOpen: false,
});
function getEmptyRepo(): HelmRepo {
return { name: "", url: "", username: "", password: "", insecureSkipTlsVerify: false, caFile: "", keyFile: "", certFile: "" };
}
@observer
export class AddHelmRepoDialog extends React.Component<AddHelmRepoDialogProps> {
private static keyExtensions = ["key", "keystore", "jks", "p12", "pfx", "pem"];
private static certExtensions = ["crt", "cer", "ca-bundle", "p7b", "p7c", "p7s", "p12", "pfx", "pem"];
constructor(props: AddHelmRepoDialogProps) {
super(props);
makeObservable(this);
}
static open() {
dialogState.isOpen = true;
}
static close() {
dialogState.isOpen = false;
}
@observable helmRepo = getEmptyRepo();
@observable showOptions = false;
@action
close = () => {
AddHelmRepoDialog.close();
this.helmRepo = getEmptyRepo();
this.showOptions = false;
};
setFilepath(type: FileType, value: string) {
this.helmRepo[type] = value;
}
getFilePath(type: FileType) {
return this.helmRepo[type];
}
async selectFileDialog(type: FileType, fileFilter: FileFilter) {
const { canceled, filePaths } = await requestOpenFilePickingDialog({
defaultPath: this.getFilePath(type),
properties: ["openFile", "showHiddenFiles"],
message: `Select file`,
buttonLabel: `Use file`,
filters: [
fileFilter,
{ name: "Any", extensions: ["*"] },
],
});
if (!canceled && filePaths.length) {
this.setFilepath(type, filePaths[0]);
}
}
async addCustomRepo() {
try {
await HelmRepoManager.getInstance().addRepo(this.helmRepo);
Notifications.ok((
<>
{"Helm repository "}
<b>{this.helmRepo.name}</b>
{" has been added."}
</>
));
this.props.onAddRepo();
this.close();
} catch (err) {
Notifications.error((
<>
{"Adding helm branch "}
<b>{this.helmRepo.name}</b>
{" has failed: "}
{String(err)}
</>
));
}
}
renderFileInput(placeholder:string, fileType:FileType, fileExtensions:string[]){
return (
<div className="flex gaps align-center">
<Input
placeholder={placeholder}
validators={isPath}
className="box grow"
value={this.getFilePath(fileType)}
onChange={v => this.setFilepath(fileType, v)}
/>
<Icon
material="folder"
onClick={() => this.selectFileDialog(fileType, { name: placeholder, extensions: fileExtensions })}
tooltip="Browse"
/>
</div>
);
}
renderOptions() {
return (
<>
<SubTitle title="Security settings" />
<Checkbox
label="Skip TLS certificate checks for the repository"
value={this.helmRepo.insecureSkipTlsVerify}
onChange={v => this.helmRepo.insecureSkipTlsVerify = v}
/>
{this.renderFileInput("Key file", FileType.KeyFile, AddHelmRepoDialog.keyExtensions)}
{this.renderFileInput("Ca file", FileType.CaFile, AddHelmRepoDialog.certExtensions)}
{this.renderFileInput("Certificate file", FileType.CertFile, AddHelmRepoDialog.certExtensions)}
<SubTitle title="Chart Repository Credentials" />
<Input
placeholder="Username"
value={this.helmRepo.username}
onChange= {v => this.helmRepo.username = v}
/>
<Input
type="password"
placeholder="Password"
value={this.helmRepo.password}
onChange={v => this.helmRepo.password = v}
/>
</>
);
}
render() {
const { ...dialogProps } = this.props;
const header = <h5>Add custom Helm Repo</h5>;
return (
<Dialog
{...dialogProps}
className="AddHelmRepoDialog"
isOpen={dialogState.isOpen}
close={this.close}
>
<Wizard header={header} done={this.close}>
<WizardStep
contentClass="flow column"
nextLabel="Add"
next={() => this.addCustomRepo()}
>
<div className="flex column gaps">
<Input
autoFocus
required
placeholder="Helm repo name"
trim
validators={systemName}
value={this.helmRepo.name}
onChange={v => this.helmRepo.name = v}
/>
<Input
required
placeholder="URL"
validators={isUrl}
value={this.helmRepo.url}
onChange={v => this.helmRepo.url = v}
/>
<Button
plain
className="accordion"
onClick={() => this.showOptions = !this.showOptions}
>
More
<Icon
small
tooltip="More"
material={this.showOptions ? "remove" : "add"}
/>
</Button>
{this.showOptions && this.renderOptions()}
</div>
</WizardStep>
</Wizard>
</Dialog>
);
}
}
@@ -1,202 +0,0 @@
/**
* 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 { observable, makeObservable, computed } from "mobx";
import type { HelmRepo } from "../../../main/helm/helm-repo-manager";
import { HelmRepoManager } from "../../../main/helm/helm-repo-manager";
import { Button } from "../button";
import { Icon } from "../icon";
import { Notifications } from "../notifications";
import type { SelectOption } from "../select";
import { Select } from "../select";
import { AddHelmRepoDialog } from "./add-helm-repo-dialog";
import { observer } from "mobx-react";
import { RemovableItem } from "./removable-item";
import { Notice } from "../+extensions/notice";
import { Spinner } from "../spinner";
import { noop } from "../../utils";
import type { SingleValue } from "react-select";
@observer
export class HelmCharts extends React.Component {
@observable loadingRepos = false;
@observable loadingAvailableRepos = false;
@observable repos: HelmRepo[] = [];
@observable addedRepos = observable.map<string, HelmRepo>();
constructor(props: {}) {
super(props);
makeObservable(this);
}
@computed get repoOptions() {
return this.repos.map(repo => ({
value: repo,
label: repo.name,
isSelected: this.addedRepos.has(repo.name),
}));
}
componentDidMount() {
this.loadAvailableRepos().catch(noop);
this.loadRepos().catch(noop);
}
async loadAvailableRepos() {
this.loadingAvailableRepos = true;
try {
if (!this.repos.length) {
this.repos = await HelmRepoManager.getInstance().loadAvailableRepos();
}
} catch (err) {
Notifications.error(String(err));
}
this.loadingAvailableRepos = false;
}
async loadRepos() {
this.loadingRepos = true;
try {
const repos = await HelmRepoManager.getInstance().repositories(); // via helm-cli
this.addedRepos.replace(repos.map(repo => [repo.name, repo]));
} catch (err) {
Notifications.error(String(err));
}
this.loadingRepos = false;
}
async addRepo(repo: HelmRepo) {
try {
await HelmRepoManager.getInstance().addRepo(repo);
this.addedRepos.set(repo.name, repo);
} catch (err) {
Notifications.error((
<>
{"Adding helm branch "}
<b>{repo.name}</b>
{" has failed: "}
{String(err)}
</>
));
}
}
async removeRepo(repo: HelmRepo) {
try {
await HelmRepoManager.getInstance().removeRepo(repo);
this.addedRepos.delete(repo.name);
} catch (err) {
Notifications.error(
<>
{"Removing helm branch "}
<b>{repo.name}</b>
{" has failed: "}
{String(err)}
</>,
);
}
}
onRepoSelect = async (option: SingleValue<{ value: HelmRepo }>): Promise<void> => {
if (!option) {
return;
}
if (this.addedRepos.has(option.value.name)) {
return void Notifications.ok((
<>
{"Helm repo "}
<b>{option.value.name}</b>
{" already in use."}
</>
));
}
await this.addRepo(option.value);
};
formatOptionLabel = ({ value, isSelected }: SelectOption<HelmRepo>) => (
<div className="flex gaps">
<span>{value.name}</span>
{isSelected && (
<Icon
small
material="check"
className="box right" />
)}
</div>
);
renderRepositories() {
const repos = Array.from(this.addedRepos);
if (this.loadingRepos) {
return <div className="pt-5 relative"><Spinner center/></div>;
}
if (!repos.length) {
return (
<Notice>
<div className="flex-grow text-center">The repositories have not been added yet</div>
</Notice>
);
}
return repos.map(([name, repo]) => {
return (
<RemovableItem
key={name}
onRemove={() => this.removeRepo(repo)}
className="mt-3"
>
<div>
<div data-testid="repository-name" className={styles.repoName}>{name}</div>
<div className={styles.repoUrl}>{repo.url}</div>
</div>
</RemovableItem>
);
});
}
render() {
return (
<div>
<div className="flex gaps">
<Select
id="HelmRepoSelect"
placeholder="Repositories"
isLoading={this.loadingAvailableRepos}
isDisabled={this.loadingAvailableRepos}
options={this.repoOptions}
onChange={this.onRepoSelect}
value={this.repos}
formatOptionLabel={this.formatOptionLabel}
controlShouldRenderValue={false}
className="box grow"
themeName="lens"
/>
<Button
primary
label="Add Custom Helm Repo"
onClick={AddHelmRepoDialog.open}
/>
</div>
<AddHelmRepoDialog onAddRepo={() => this.loadRepos()}/>
<div className={styles.repos}>
{this.renderRepositories()}
</div>
</div>
);
}
}
@@ -4,8 +4,7 @@
*/
import { observer } from "mobx-react";
import React from "react";
import { HelmCharts } from "./helm-charts";
import { HelmCharts } from "./kubernetes/helm-charts/helm-charts";
import { KubeconfigSyncs } from "./kubeconfig-syncs";
import { KubectlBinaries } from "./kubectl-binaries";
import { Preferences } from "./preferences";
@@ -0,0 +1,43 @@
/**
* 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 getActiveHelmRepositoriesChannelInjectable from "../../../../../common/helm/get-active-helm-repositories-channel.injectable";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import showErrorNotificationInjectable from "../../../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 getHelmRepositoriesChannel = di.inject(getActiveHelmRepositoriesChannelInjectable);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const helmRepositoriesErrorState = di.inject(helmRepositoriesErrorStateInjectable);
return asyncComputed(async () => {
const result = await requestFromChannel(getHelmRepositoriesChannel);
if (result.callWasSuccessful) {
return result.response;
} else {
showErrorNotification(result.error);
runInAction(() =>
helmRepositoriesErrorState.set({
controlsAreShown: false,
errorMessage: result.error,
}),
);
return [];
}
}, []);
},
});
export default activeHelmRepositoriesInjectable;
@@ -0,0 +1,152 @@
/**
* 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 "../../../../wizard";
import { Input } from "../../../../input";
import { systemName, isUrl } from "../../../../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 "../../../../button";
import { Icon } from "../../../../icon";
import maximalCustomHelmRepoOptionsAreShownInjectable from "./maximal-custom-helm-repo-options-are-shown.injectable";
import { SubTitle } from "../../../../layout/sub-title";
import { Checkbox } from "../../../../checkbox";
import { HelmFileInput } from "./helm-file-input/helm-file-input";
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(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 "../../../../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 "../../../../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,25 @@
/**
* 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";
const customHelmRepoInjectable = getInjectable({
id: "custom-helm-repo",
instantiate: () => observable({
name: "",
url: "",
username: "",
password: "",
insecureSkipTlsVerify: false,
caFile: "",
keyFile: "",
certFile: "",
}),
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,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 type { FileFilter } from "electron";
import { requestOpenFilePickingDialog } from "../../../../../../ipc";
const getFilePathsInjectable = getInjectable({
id: "get-file-paths",
instantiate: () => async (fileFilter: FileFilter) =>
await requestOpenFilePickingDialog({
properties: ["openFile", "showHiddenFiles"],
message: `Select file`,
buttonLabel: `Use file`,
filters: [fileFilter, { name: "Any", extensions: ["*"] }],
}),
causesSideEffects: true,
});
export default getFilePathsInjectable;
@@ -0,0 +1,74 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { InputValidator } from "../../../../../input";
import { Input } from "../../../../../input";
import { Icon } from "../../../../../icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import getFilePathsInjectable from "./get-file-paths.injectable";
import type { FileFilter } from "electron";
import isPathInjectable from "../../../../../input/validators/is-path.injectable";
interface HelmFileInputProps {
placeholder: string;
fileExtensions: string[];
value: string;
setValue: (value: string) => void;
"data-testid"?: string;
}
interface Dependencies {
getFilePaths: (fileFilter: FileFilter) => Promise<{ canceled: boolean; filePaths: string[] }>;
isPath: InputValidator<true>;
}
const NonInjectedHelmFileInput = ({
placeholder,
value,
setValue,
fileExtensions,
getFilePaths,
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={async () => {
const { canceled, filePaths } = await getFilePaths({
name: placeholder,
extensions: fileExtensions,
});
if (!canceled && filePaths.length) {
setValue(filePaths[0]);
}
}}
tooltip="Browse"
/>
</div>
);
export const HelmFileInput = withInjectables<Dependencies, HelmFileInputProps>(
NonInjectedHelmFileInput,
{
getProps: (di, props) => ({
getFilePaths: di.inject(getFilePathsInjectable),
isPath: di.inject(isPathInjectable),
...props,
}),
},
);
@@ -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 "../../../../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 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,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/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;
@@ -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;
@@ -0,0 +1,42 @@
/**
* 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 addHelmRepositoryChannelInjectable from "../../../../../../../common/helm/add-helm-repository-channel.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 "../../../../../notifications/show-error-notification.injectable";
import showSuccessNotificationInjectable from "../../../../../notifications/show-success-notification.injectable";
const addHelmRepositoryInjectable = getInjectable({
id: "add-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const addHelmRepositoryChannel = di.inject(addHelmRepositoryChannelInjectable);
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 "../../../../../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,61 @@
/**
* 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 "../../../+extensions/notice";
interface Dependencies {
helmRepositoriesErrorState: IObservableValue<HelmRepositoriesErrorState>;
}
const NonInjectedHelmCharts = observer(
({ helmRepositoriesErrorState }: Dependencies) => {
const state = helmRepositoriesErrorState.get();
return (
<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>
);
},
);
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,68 @@
/**
* 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 "../../../spinner";
import type { HelmRepo } from "../../../../../common/helm/helm-repo";
import { RemovableItem } from "../../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="mt-3"
data-testid={`remove-helm-repository-${repository.name}`}
>
<div data-testid={`helm-repository-${repository.name}`} className={styles.repoName}>
{repository.name}
</div>
<div className={styles.repoUrl}>{repository.url}</div>
</RemovableItem>
))}
</div>
);
});
export const HelmRepositories = withInjectables<Dependencies>(
NonInjectedActiveHelmRepositories,
{
getProps: (di) => ({
activeHelmRepositories: di.inject(activeHelmRepositoriesInjectable),
removeRepository: di.inject(removeHelmRepositoryInjectable),
}),
},
);
@@ -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/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable";
import removeHelmRepositoryChannelInjectable from "../../../../../common/helm/remove-helm-repository-channel.injectable";
const removePublicHelmRepositoryInjectable = getInjectable({
id: "remove-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const removeHelmRepositoryChannel = di.inject(removeHelmRepositoryChannelInjectable);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
return async (repository: HelmRepo) => {
await requestFromChannel(removeHelmRepositoryChannel, repository);
activeHelmRepositories.invalidate();
};
},
});
export default removePublicHelmRepositoryInjectable;
@@ -14,9 +14,10 @@ export interface RemovableItemProps extends DOMAttributes<any>{
icon?: string;
onRemove: () => void;
className?: string;
"data-testid"?: string;
}
export function RemovableItem({ icon, onRemove, children, className, ...rest }: RemovableItemProps) {
export function RemovableItem({ icon, onRemove, children, className, "data-testid": testId, ...rest }: RemovableItemProps) {
return (
<div className={cssNames(styles.item, "flex gaps align-center justify-space-between", className)} {...rest}>
{icon && (
@@ -27,6 +28,7 @@ export function RemovableItem({ icon, onRemove, children, className, ...rest }:
material="delete"
onClick={onRemove}
tooltip="Remove"
data-testid={testId}
/>
</div>
);
@@ -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 { AsyncInputValidationError, inputValidator } from "../input_validators";
import pathExistsInjectable from "../../../../common/fs/path-exists.injectable";
const isPathInjectable = getInjectable({
id: "is-path",
instantiate: (di) => {
const pathExists = di.inject(pathExistsInjectable);
return inputValidator<true>({
debounce: 100,
condition: ({ type }) => type === "text",
validate: async (value) => {
try {
await pathExists(value);
} catch {
throw new AsyncInputValidationError(
`${value} is not a valid file path`,
);
}
},
});
},
});
export default isPathInjectable;
@@ -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 { NotificationMessage, Notification } from "./notifications.store";
import { NotificationStatus } from "./notifications.store";
import notificationsStoreInjectable from "./notifications-store.injectable";
const showErrorNotificationInjectable = getInjectable({
id: "show-error-notification",
instantiate: (di) => {
const notificationsStore = di.inject(notificationsStoreInjectable);
return (message: NotificationMessage, customOpts: Partial<Omit<Notification, "message">> = {}) =>
notificationsStore.add({
status: NotificationStatus.ERROR,
timeout: 5000,
message,
...customOpts,
});
},
});
export default showErrorNotificationInjectable;
@@ -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 { NotificationMessage, Notification } from "./notifications.store";
import { NotificationStatus } from "./notifications.store";
import notificationsStoreInjectable from "./notifications-store.injectable";
const showSuccessNotificationInjectable = getInjectable({
id: "show-success-notification",
instantiate: (di) => {
const notificationsStore = di.inject(notificationsStoreInjectable);
return (message: NotificationMessage, customOpts: Partial<Omit<Notification, "message">> = {}) =>
notificationsStore.add({
status: NotificationStatus.OK,
timeout: 5000,
message,
...customOpts,
});
},
});
export default showSuccessNotificationInjectable;
+3 -1
View File
@@ -233,7 +233,9 @@ class NonInjectedSelect<
Menu: ({ className, ...props }) => (
<WrappedMenu
{...props}
className={cssNames(menuClass, this.themeClass, className)}
className={cssNames(menuClass, this.themeClass, className, {
[`${inputId}-options`]: !!inputId,
})}
/>
),
}}
@@ -15,7 +15,7 @@ import { Observer } from "mobx-react";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import allowedResourcesInjectable from "../../../common/cluster-store/allowed-resources.injectable";
import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import { getByText, fireEvent } from "@testing-library/react";
import type { KubeResource } from "../../../common/rbac";
import { Sidebar } from "../layout/sidebar";
import type { DiContainer } from "@ogre-tools/injectable";
@@ -52,6 +52,9 @@ import { getDiForUnitTesting as getMainDi } from "../../../main/getDiForUnitTest
import { overrideChannels } from "../../../test-utils/channel-fakes/override-channels";
import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token";
import trayIconPathsInjectable from "../../../main/tray/tray-icon-path.injectable";
import assert from "assert";
import { openMenu } from "react-select-event";
import userEvent from "@testing-library/user-event";
type Callback = (dis: DiContainers) => void | Promise<void>;
@@ -85,6 +88,11 @@ export interface ApplicationBuilder {
helmCharts: {
navigate: () => void;
};
select: {
openMenu: (id: string) => void;
selectOption: (menuId: string, labelText: string) => void;
};
}
interface DiContainers {
@@ -433,6 +441,30 @@ export const getApplicationBuilder = () => {
return rendered;
},
select: {
openMenu: (menuId) => {
const selector = rendered.container.querySelector<HTMLElement>(
`#${menuId}`,
);
assert(selector);
openMenu(selector);
},
selectOption: (menuId, labelText) => {
const menuOptions = rendered.baseElement.querySelector<HTMLElement>(
`.${menuId}-options`,
);
assert(menuOptions);
const option = getByText(menuOptions, labelText);
userEvent.click(option);
},
},
};
return builder;
+5 -1
View File
@@ -129,6 +129,8 @@ export interface WizardStepProps<D> extends WizardCommonProps<D> {
skip?: boolean; // don't render the step
scrollable?: boolean;
children?: React.ReactNode | React.ReactNode[];
testIdForNext?: string;
testIdForPrev?: string;
}
interface WizardStepState {
@@ -214,7 +216,7 @@ export class WizardStep<D> extends React.Component<WizardStepProps<D>, WizardSte
step, isFirst, isLast, children,
loading, customButtons, disabledNext, scrollable,
hideNextBtn, hideBackBtn, beforeContent, afterContent, noValidate, skip, moreButtons,
waiting, className, contentClass, prevLabel, nextLabel,
waiting, className, contentClass, prevLabel, nextLabel, testIdForNext, testIdForPrev,
} = this.props;
if (skip) {
@@ -242,6 +244,7 @@ export class WizardStep<D> extends React.Component<WizardStepProps<D>, WizardSte
label={prevLabel || (isFirst?.() ? "Cancel" : "Back")}
hidden={hideBackBtn}
onClick={this.prev}
data-testid={testIdForPrev}
/>
<Button
primary
@@ -250,6 +253,7 @@ export class WizardStep<D> extends React.Component<WizardStepProps<D>, WizardSte
hidden={hideNextBtn}
waiting={waiting ?? this.state.waiting}
disabled={disabledNext}
data-testid={testIdForNext}
/>
</div>
)}
+21 -4
View File
@@ -5,7 +5,12 @@
import glob from "glob";
import { memoize, noop } from "lodash/fp";
import { createContainer } from "@ogre-tools/injectable";
import type {
DiContainer,
Injectable } from "@ogre-tools/injectable";
import {
createContainer,
} from "@ogre-tools/injectable";
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import requestFromChannelInjectable from "./utils/channel/request-from-channel.injectable";
import loggerInjectable from "../common/logger.injectable";
@@ -45,6 +50,8 @@ import appVersionInjectable from "../common/get-configuration-file-model/app-ver
import provideInitialValuesForSyncBoxesInjectable from "./utils/sync-box/provide-initial-values-for-sync-boxes.injectable";
import requestAnimationFrameInjectable from "./components/animate/request-animation-frame.injectable";
import getRandomIdInjectable from "../common/utils/get-random-id.injectable";
import getFilePathsInjectable from "./components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/helm-file-input/get-file-paths.injectable";
import callForPublicHelmRepositoriesInjectable from "./components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable";
export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => {
const {
@@ -91,9 +98,11 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
on: () => {},
}) as unknown as IpcRenderer);
di.override(broadcastMessageInjectable, () => () => {
throw new Error("Tried to broadcast message over IPC without explicit override.");
});
overrideFunctionalInjectables(di, [
broadcastMessageInjectable,
getFilePathsInjectable,
callForPublicHelmRepositoriesInjectable,
]);
// eslint-disable-next-line unused-imports/no-unused-vars-ts
di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore);
@@ -147,3 +156,11 @@ const getInjectableFilePaths = memoize(() => [
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);
const overrideFunctionalInjectables = (di: DiContainer, injectables: Injectable<any, any, any>[]) => {
injectables.forEach(injectable => {
di.override(injectable, () => () => {
throw new Error(`Tried to run "${injectable.id}" without explicit override.`);
});
});
};