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

Rework installation of helm charts to get rid of the majority of bugs

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-07-18 15:40:27 +03:00
parent 5dbb968d91
commit 4a61fffe45
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
29 changed files with 26772 additions and 359 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import callForHelmChartReadmeInjectable from "./readme/call-for-helm-chart-readme.injectable";
import helmChartDetailsVersionSelectionInjectable from "./versions/helm-chart-details-version-selection.injectable";
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
const readmeOfSelectedHelmChartInjectable = getInjectable({
id: "readme-of-selected-helm-chart",
instantiate: (di, chart: HelmChart) => {
const selection = di.inject(helmChartDetailsVersionSelectionInjectable, chart);
const callForHelmChartReadme = di.inject(callForHelmChartReadmeInjectable);
return asyncComputed(async () => {
const chartVersion = selection.value.get();
if (!chartVersion) {
return "";
}
return await callForHelmChartReadme(
chartVersion.getRepository(),
chartVersion.getName(),
chartVersion.getVersion(),
);
}, "");
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, chart: HelmChart) => chart.getId(),
}),
});
export default readmeOfSelectedHelmChartInjectable;

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 { getChartDetails } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
export type CallForHelmChartReadme = (
repo: string,
name: string,
version: string,
) => Promise<string>;
const callForHelmChartReadmeInjectable = getInjectable({
id: "call-for-helm-chart-readme",
instantiate:
(): CallForHelmChartReadme =>
async (repository: string, name: string, version: string) => {
// TODO: Dismantle wrong abstraction
const details = await getChartDetails(repository, name, { version });
return details.readme;
},
});
export default callForHelmChartReadmeInjectable;

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import callForHelmChartVersionsInjectable from "./versions/call-for-helm-chart-versions.injectable";
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
const versionsOfSelectedHelmChartInjectable = getInjectable({
id: "versions-of-selected-helm-chart",
instantiate: (di, chart: HelmChart) => {
const callForHelmChartVersions = di.inject(callForHelmChartVersionsInjectable);
return asyncComputed(
async () =>
await callForHelmChartVersions(chart.getRepository(), chart.getName()),
[],
);
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, chart: HelmChart) => chart.getId(),
}),
});
export default versionsOfSelectedHelmChartInjectable;

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 type { HelmChart } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
import { getChartDetails } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
export type CallForHelmChartVersions = (
repo: string,
name: string
) => Promise<HelmChart[]>;
const callForHelmChartVersionsInjectable = getInjectable({
id: "call-for-helm-chart-versions",
instantiate:
(): CallForHelmChartVersions => async (repository: string, name: string) => {
// TODO: Dismantle wrong abstraction
const details = await getChartDetails(repository, name);
return details.versions;
},
});
export default callForHelmChartVersionsInjectable;

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import { computed, observable } from "mobx";
import versionsOfSelectedHelmChartInjectable from "../versions-of-selected-helm-chart.injectable";
import type { HelmChart } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
import type { SingleValue } from "react-select";
interface VersionSelectionOption {
label: string;
value: HelmChart;
}
export interface HelmChartDetailsVersionSelection {
value: IComputedValue<HelmChart | undefined>;
options: IComputedValue<VersionSelectionOption[]>;
onChange: (option: SingleValue<VersionSelectionOption>) => void;
}
const helmChartDetailsVersionSelectionInjectable = getInjectable({
id: "helm-chart-details-version-selection",
instantiate: (di, chart: HelmChart): HelmChartDetailsVersionSelection => {
const versionsOfSelectedHelmChart = di.inject(
versionsOfSelectedHelmChartInjectable,
chart,
);
const state = observable.box<HelmChart>();
return {
value: computed(
() => state.get() || versionsOfSelectedHelmChart.value.get()[0],
),
options: computed(() =>
versionsOfSelectedHelmChart.value.get().map((chartVersion) => ({
label: chartVersion.version,
value: chartVersion,
})),
),
onChange: (option) => {
if (option) {
state.set(option.value);
}
},
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, chart: HelmChart) => chart.getId(),
}),
});
export default helmChartDetailsVersionSelectionInjectable;

View File

@ -7,8 +7,7 @@ import "./helm-chart-details.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api"; import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
import { computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react";
import { disposeOnUnmount, observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer"; import { Drawer, DrawerItem } from "../drawer";
import { autoBind, stopPropagation } from "../../utils"; import { autoBind, stopPropagation } from "../../utils";
import { MarkdownViewer } from "../markdown-viewer"; import { MarkdownViewer } from "../markdown-viewer";
@ -17,19 +16,21 @@ import { Button } from "../button";
import { Select } from "../select"; import { Select } from "../select";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { Tooltip, withStyles } from "@material-ui/core"; import { Tooltip, withStyles } from "@material-ui/core";
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import createInstallChartTabInjectable from "../dock/install-chart/create-install-chart-tab.injectable"; import createInstallChartTabInjectable from "../dock/install-chart/create-install-chart-tab.injectable";
import type { ShowCheckedErrorNotification } from "../notifications/show-checked-error.injectable"; import type { ShowCheckedErrorNotification } from "../notifications/show-checked-error.injectable";
import type { SingleValue } from "react-select";
import AbortController from "abort-controller";
import showCheckedErrorNotificationInjectable from "../notifications/show-checked-error.injectable"; import showCheckedErrorNotificationInjectable from "../notifications/show-checked-error.injectable";
import type { GetChartDetails } from "./get-char-details.injectable";
import getChartDetailsInjectable from "./get-char-details.injectable";
import { HelmChartIcon } from "./icon"; import { HelmChartIcon } from "./icon";
import readmeOfSelectHelmChartInjectable from "./details/readme-of-selected-helm-chart.injectable";
import versionsOfSelectedHelmChartInjectable from "./details/versions-of-selected-helm-chart.injectable";
import type { HelmChartDetailsVersionSelection } from "./details/versions/helm-chart-details-version-selection.injectable";
import helmChartDetailsVersionSelectionInjectable from "./details/versions/helm-chart-details-version-selection.injectable";
import assert from "assert";
export interface HelmChartDetailsProps { export interface HelmChartDetailsProps {
chart: HelmChart;
hideDetails(): void; hideDetails(): void;
chart: HelmChart;
} }
const LargeTooltip = withStyles({ const LargeTooltip = withStyles({
@ -41,84 +42,34 @@ const LargeTooltip = withStyles({
interface Dependencies { interface Dependencies {
createInstallChartTab: (helmChart: HelmChart) => void; createInstallChartTab: (helmChart: HelmChart) => void;
showCheckedErrorNotification: ShowCheckedErrorNotification; showCheckedErrorNotification: ShowCheckedErrorNotification;
getChartDetails: GetChartDetails; versions: IAsyncComputed<HelmChart[]>;
readme: IAsyncComputed<string>;
versionSelection: HelmChartDetailsVersionSelection;
} }
@observer @observer
class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Dependencies> { class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Dependencies> {
readonly chartVersions = observable.array<HelmChart>();
readonly selectedChart = observable.box<HelmChart | undefined>();
readonly readme = observable.box<string | undefined>(undefined);
readonly chartVerionOptions = computed(() => (
this.chartVersions.map(chart => ({
value: chart,
label: chart.version,
}))
));
private abortController = new AbortController();
constructor(props: HelmChartDetailsProps & Dependencies) { constructor(props: HelmChartDetailsProps & Dependencies) {
super(props); super(props);
autoBind(this); autoBind(this);
} }
componentWillUnmount() { get chart() {
this.abortController.abort(); return this.props.chart;
} }
componentDidMount() { install() {
disposeOnUnmount(this, [ const chart = this.props.versionSelection.value.get();
reaction(() => this.props.chart, async ({ name, repo, version }) => {
runInAction(() => {
this.selectedChart.set(undefined);
this.chartVersions.clear();
this.readme.set("");
});
try { assert(chart);
const { readme, versions } = await this.props.getChartDetails(repo, name, { version });
runInAction(() => { this.props.createInstallChartTab(chart);
this.readme.set(readme);
this.chartVersions.replace(versions);
this.selectedChart.set(versions[0]);
});
} catch (error) {
this.props.showCheckedErrorNotification(error, "Unknown error occured while getting chart details");
}
}, {
fireImmediately: true,
}),
]);
}
async onVersionChange(option: SingleValue<{ value: HelmChart }>) {
const chart = option?.value ?? this.chartVersions[0];
runInAction(() => {
this.selectedChart.set(chart ?? undefined);
this.readme.set(undefined);
});
try {
this.abortController.abort();
this.abortController = new AbortController();
const { chart: { name, repo }} = this.props;
const { readme } = await this.props.getChartDetails(repo, name, { version: chart.version, reqInit: { signal: this.abortController.signal }});
this.readme.set(readme);
} catch (error) {
this.props.showCheckedErrorNotification(error, "Unknown error occured while getting chart details");
}
}
install(selectedChart: HelmChart) {
this.props.createInstallChartTab(selectedChart);
this.props.hideDetails(); this.props.hideDetails();
} }
renderIntroduction(selectedChart: HelmChart) { renderIntroduction(selectedChart: HelmChart) {
const testId = selectedChart.getFullName("-");
return ( return (
<div className="introduction flex align-flex-start"> <div className="introduction flex align-flex-start">
<HelmChartIcon <HelmChartIcon
@ -131,7 +82,8 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
<Button <Button
primary primary
label="Install" label="Install"
onClick={() => this.install(selectedChart)} onClick={this.install}
data-testid={`install-chart-for-${testId}`}
/> />
</div> </div>
<DrawerItem <DrawerItem
@ -140,10 +92,10 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
onClick={stopPropagation} onClick={stopPropagation}
> >
<Select <Select
id="chart-version-input" id={`helm-chart-version-selector-${testId}`}
themeName="outlined" themeName="outlined"
menuPortalTarget={null} menuPortalTarget={null}
options={this.chartVerionOptions.get()} options={this.props.versionSelection.options.get()}
formatOptionLabel={({ value: chart }) => ( formatOptionLabel={({ value: chart }) => (
chart.deprecated chart.deprecated
? ( ? (
@ -154,8 +106,8 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
: chart.version : chart.version
)} )}
isOptionDisabled={({ value: chart }) => chart.deprecated} isOptionDisabled={({ value: chart }) => chart.deprecated}
value={selectedChart} value={this.props.versionSelection.value.get()}
onChange={this.onVersionChange} onChange={this.props.versionSelection.onChange}
/> />
</DrawerItem> </DrawerItem>
<DrawerItem name="Home"> <DrawerItem name="Home">
@ -190,44 +142,42 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
} }
renderReadme() { renderReadme() {
const readme = this.readme.get();
if (readme === undefined) {
return <Spinner center />;
}
return ( return (
<div className="chart-description" data-testid="helmchart-readme"> <div className="chart-description" data-testid="helmchart-readme">
<MarkdownViewer markdown={readme} /> <MarkdownViewer markdown={this.props.readme.value.get()} />
</div> </div>
); );
} }
renderContent() { renderContent() {
const selectedChart = this.selectedChart.get(); const readmeIsLoading = this.props.readme.pending.get();
const versionsAreLoading = this.props.versions.pending.get();
if (!selectedChart) { if (!this.chart || versionsAreLoading) {
return <Spinner center />; return <Spinner center data-testid="spinner-for-chart-details" />;
} }
return ( return (
<div className="box grow"> <div className="box grow">
{this.renderIntroduction(selectedChart)} {this.renderIntroduction(this.chart)}
{this.renderReadme()}
{readmeIsLoading ? (
<Spinner center data-testid="spinner-for-chart-readme" />
) : (
this.renderReadme()
)}
</div> </div>
); );
} }
render() { render() {
const { chart, hideDetails } = this.props;
return ( return (
<Drawer <Drawer
className="HelmChartDetails" className="HelmChartDetails"
usePortal={true} usePortal={true}
open={!!chart} open={!!this.chart}
title={chart ? `Chart: ${chart.getFullName()}` : ""} title={this.chart ? `Chart: ${this.chart.getFullName()}` : ""}
onClose={hideDetails} onClose={this.props.hideDetails}
> >
{this.renderContent()} {this.renderContent()}
</Drawer> </Drawer>
@ -240,6 +190,8 @@ export const HelmChartDetails = withInjectables<Dependencies, HelmChartDetailsPr
...props, ...props,
createInstallChartTab: di.inject(createInstallChartTabInjectable), createInstallChartTab: di.inject(createInstallChartTabInjectable),
showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable), showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
getChartDetails: di.inject(getChartDetailsInjectable), readme: di.inject(readmeOfSelectHelmChartInjectable, props.chart),
versions: di.inject(versionsOfSelectedHelmChartInjectable, props.chart),
versionSelection: di.inject(helmChartDetailsVersionSelectionInjectable, props.chart),
}), }),
}); });

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { HelmChartStore } from "./helm-chart.store";
const helmChartStoreInjectable = getInjectable({
id: "helm-chart-store",
instantiate: () => new HelmChartStore(),
});
export default helmChartStoreInjectable;

View File

@ -12,12 +12,15 @@ import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.ap
import { HelmChartDetails } from "./helm-chart-details"; import { HelmChartDetails } from "./helm-chart-details";
import { ItemListLayout } from "../item-object-list/list-layout"; import { ItemListLayout } from "../item-object-list/list-layout";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
import helmChartsRouteParametersInjectable from "./helm-charts-route-parameters.injectable"; import helmChartsRouteParametersInjectable from "./helm-charts-route-parameters.injectable";
import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable"; import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable"; import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import { HelmChartIcon } from "./icon"; import { HelmChartIcon } from "./icon";
import helmChartsInjectable from "./helm-charts/helm-charts.injectable";
import selectedHelmChartInjectable from "./helm-charts/selected-helm-chart.injectable";
enum columnId { enum columnId {
name = "name", name = "name",
@ -34,27 +37,15 @@ interface Dependencies {
}; };
navigateToHelmCharts: NavigateToHelmCharts; navigateToHelmCharts: NavigateToHelmCharts;
charts: IAsyncComputed<HelmChart[]>;
selectedChart: IComputedValue<HelmChart | undefined>;
} }
@observer @observer
class NonInjectedHelmCharts extends Component<Dependencies> { class NonInjectedHelmCharts extends Component<Dependencies> {
componentDidMount() {
helmChartStore.loadAll();
}
get selectedChart() {
const chartName = this.props.routeParameters.chartName.get();
const repo = this.props.routeParameters.repo.get();
if (!chartName || !repo) {
return undefined;
}
return helmChartStore.getByName(chartName, repo);
}
onDetails = (chart: HelmChart) => { onDetails = (chart: HelmChart) => {
if (chart === this.selectedChart) { if (chart === this.props.selectedChart.get()) {
this.hideDetails(); this.hideDetails();
} else { } else {
this.showDetails(chart); this.showDetails(chart);
@ -78,6 +69,8 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
}; };
render() { render() {
const selectedChart = this.props.selectedChart.get();
return ( return (
<SiblingsInTabLayout> <SiblingsInTabLayout>
<div data-testid="page-for-helm-charts" style={{ display: "none" }}/> <div data-testid="page-for-helm-charts" style={{ display: "none" }}/>
@ -87,7 +80,7 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
tableId="helm_charts" tableId="helm_charts"
className="HelmCharts" className="HelmCharts"
store={helmChartStore} store={helmChartStore}
getItems={() => helmChartStore.items} getItems={() => this.props.charts.value.get()}
isSelectable={false} isSelectable={false}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: chart => chart.getName(), [columnId.name]: chart => chart.getName(),
@ -105,6 +98,7 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
placeholder: "Search Helm Charts...", placeholder: "Search Helm Charts...",
}, },
})} })}
customizeTableRowProps={(item) => ({ testId: `helm-chart-row-for-${item.getFullName("-")}` })}
renderTableHeader={[ renderTableHeader={[
{ className: "icon", showWithColumn: columnId.name }, { className: "icon", showWithColumn: columnId.name },
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
@ -124,12 +118,12 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
{ title: chart.getRepository(), className: chart.getRepository().toLowerCase() }, { title: chart.getRepository(), className: chart.getRepository().toLowerCase() },
{ className: "menu" }, { className: "menu" },
]} ]}
detailsItem={this.selectedChart} detailsItem={selectedChart}
onDetails={this.onDetails} onDetails={this.onDetails}
/> />
{this.selectedChart && ( {selectedChart && (
<HelmChartDetails <HelmChartDetails
chart={this.selectedChart} chart={selectedChart}
hideDetails={this.hideDetails} hideDetails={this.hideDetails}
/> />
)} )}
@ -145,6 +139,8 @@ export const HelmCharts = withInjectables<Dependencies>(
getProps: (di) => ({ getProps: (di) => ({
routeParameters: di.inject(helmChartsRouteParametersInjectable), routeParameters: di.inject(helmChartsRouteParametersInjectable),
navigateToHelmCharts: di.inject(navigateToHelmChartsInjectable), navigateToHelmCharts: di.inject(navigateToHelmChartsInjectable),
charts: di.inject(helmChartsInjectable),
selectedChart: di.inject(selectedHelmChartInjectable),
}), }),
}, },
); );

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import { listCharts } from "../../../../common/k8s-api/endpoints/helm-charts.api";
export type CallForHelmCharts = () => Promise<HelmChart[]>;
const callForHelmChartsInjectable = getInjectable({
id: "call-for-helm-charts",
instantiate: (): CallForHelmCharts => async () => await listCharts(),
});
export default callForHelmChartsInjectable;

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import callForHelmChartsInjectable from "./call-for-helm-charts.injectable";
const helmChartsInjectable = getInjectable({
id: "helm-charts",
instantiate: (di) => {
const callForHelmCharts = di.inject(callForHelmChartsInjectable);
return asyncComputed(async () => await callForHelmCharts(), []);
},
});
export default helmChartsInjectable;

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import helmChartsRouteParametersInjectable from "../helm-charts-route-parameters.injectable";
import helmChartsInjectable from "./helm-charts.injectable";
const selectedHelmChartInjectable = getInjectable({
id: "selected-helm-chart",
instantiate: (di) => {
const { chartName, repo } = di.inject(helmChartsRouteParametersInjectable);
const helmCharts = di.inject(helmChartsInjectable);
return computed(() => {
const dereferencedChartName = chartName.get();
const deferencedRepository = repo.get();
if (!dereferencedChartName || !deferencedRepository) {
return undefined;
}
return helmCharts.value
.get()
.find(
(chart) =>
chart.getName() === dereferencedChartName &&
chart.getRepository() === deferencedRepository,
);
});
},
});
export default selectedHelmChartInjectable;

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmReleaseCreatePayload, HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { createRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
export type CallForCreateHelmRelease = (
payload: HelmReleaseCreatePayload
) => Promise<HelmReleaseUpdateDetails>;
const callForCreateHelmReleaseInjectable = getInjectable({
id: "call-for-create-helm-release",
instantiate: (): CallForCreateHelmRelease => createRelease,
causesSideEffects: true,
});
export default callForCreateHelmReleaseInjectable;

View File

@ -6,19 +6,18 @@ import { getInjectable } from "@ogre-tools/injectable";
import type { import type {
HelmReleaseCreatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api"; HelmReleaseCreatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import {
createRelease,
} from "../../../../common/k8s-api/endpoints/helm-releases.api";
import releasesInjectable from "../releases.injectable"; import releasesInjectable from "../releases.injectable";
import callForCreateHelmReleaseInjectable from "./call-for-create-helm-release.injectable";
const createReleaseInjectable = getInjectable({ const createReleaseInjectable = getInjectable({
id: "create-release", id: "create-release",
instantiate: (di) => { instantiate: (di) => {
const releases = di.inject(releasesInjectable); const releases = di.inject(releasesInjectable);
const callForCreateRelease = di.inject(callForCreateHelmReleaseInjectable);
return async (payload: HelmReleaseCreatePayload) => { return async (payload: HelmReleaseCreatePayload) => {
const release = await createRelease(payload); const release = await callForCreateRelease(payload);
releases.invalidate(); releases.invalidate();

View File

@ -13,8 +13,7 @@ import { Notifications } from "../notifications";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { clipboard } from "electron"; import { clipboard } from "electron";
import { kebabCase } from "lodash/fp";
// todo: make as external BrowserWindow (?)
export interface LogsDialogProps extends DialogProps { export interface LogsDialogProps extends DialogProps {
title: string; title: string;
@ -26,6 +25,7 @@ export function LogsDialog({ title, logs, ...dialogProps }: LogsDialogProps) {
<Dialog <Dialog
{...dialogProps} {...dialogProps}
className="LogsDialog" className="LogsDialog"
data-testid={`logs-dialog-for-${kebabCase(title)}`}
> >
<Wizard <Wizard
header={<h5>{title}</h5>} header={<h5>{title}</h5>}

View File

@ -115,6 +115,7 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
</Tooltip> </Tooltip>
</div> </div>
)} )}
data-testid={`dock-tab-for-${id}`}
/> />
{this.renderMenu(id)} {this.renderMenu(id)}
</> </>

View File

@ -11,8 +11,8 @@ import { DockTab } from "./dock-tab";
import type { DockTab as DockTabModel } from "./dock/store"; import type { DockTab as DockTabModel } from "./dock/store";
import { TabKind } from "./dock/store"; import { TabKind } from "./dock/store";
import { TerminalTab } from "./terminal/dock-tab"; import { TerminalTab } from "./terminal/dock-tab";
import { useResizeObserver } from "../../hooks";
import { cssVar } from "../../utils"; import { cssVar } from "../../utils";
import { useResizeObserver } from "../../hooks";
export interface DockTabsProps { export interface DockTabsProps {
tabs: DockTabModel[]; tabs: DockTabModel[];

View File

@ -124,7 +124,10 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
if (!isOpen || !selectedTab) return null; if (!isOpen || !selectedTab) return null;
return ( return (
<div className={`tab-content ${selectedTab.kind}`} style={{ flexBasis: height }}> <div
className={`tab-content ${selectedTab.kind}`}
style={{ flexBasis: height }}
data-testid={`dock-tab-content-for-${selectedTab.id}`}>
{this.renderTab(selectedTab)} {this.renderTab(selectedTab)}
</div> </div>
); );

View File

@ -8,3 +8,7 @@
flex: 1; flex: 1;
height: 100%; height: 100%;
} }
.hidden {
display: none;
}

View File

@ -22,6 +22,7 @@ export interface EditorPanelProps {
autoFocus?: boolean; // default: true autoFocus?: boolean; // default: true
onChange: MonacoEditorProps["onChange"]; onChange: MonacoEditorProps["onChange"];
onError?: MonacoEditorProps["onError"]; onError?: MonacoEditorProps["onError"];
hidden?: boolean;
} }
interface Dependencies { interface Dependencies {
@ -36,6 +37,7 @@ const NonInjectedEditorPanel = observer(({
autoFocus = true, autoFocus = true,
className, className,
onError, onError,
hidden,
}: Dependencies & EditorPanelProps) => { }: Dependencies & EditorPanelProps) => {
const editor = createRef<MonacoEditorRef>(); const editor = createRef<MonacoEditorRef>();
@ -59,7 +61,7 @@ const NonInjectedEditorPanel = observer(({
autoFocus={autoFocus} autoFocus={autoFocus}
id={tabId} id={tabId}
value={value} value={value}
className={cssNames(styles.EditorPanel, className)} className={cssNames(styles.EditorPanel, className, { hidden })}
onChange={onChange} onChange={onChange}
onError={onError} onError={onError}
ref={editor} ref={editor}

View File

@ -14,9 +14,12 @@ import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import type { DockStore, TabId } from "./dock/store"; import type { DockStore, TabId } from "./dock/store";
import { Notifications } from "../notifications"; import type { ShowNotification } from "../notifications";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock/store.injectable"; import dockStoreInjectable from "./dock/store.injectable";
import type { ShowCheckedErrorNotification } from "../notifications/show-checked-error.injectable";
import showSuccessNotificationInjectable from "../notifications/show-success-notification.injectable";
import showCheckedErrorNotificationInjectable from "../notifications/show-checked-error.injectable";
export interface InfoPanelProps extends OptionalProps { export interface InfoPanelProps extends OptionalProps {
tabId: TabId; tabId: TabId;
@ -35,10 +38,15 @@ export interface OptionalProps {
showInlineInfo?: boolean; showInlineInfo?: boolean;
showNotifications?: boolean; showNotifications?: boolean;
showStatusPanel?: boolean; showStatusPanel?: boolean;
submitTestId?: string;
cancelTestId?: string;
submittingTestId?: string;
} }
interface Dependencies { interface Dependencies {
dockStore: DockStore; dockStore: DockStore;
showSuccessNotification: ShowNotification;
showCheckedErrorNotification: ShowCheckedErrorNotification;
} }
@observer @observer
@ -82,11 +90,11 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
const result = await this.props.submit?.(); const result = await this.props.submit?.();
if (showNotifications && result) { if (showNotifications && result) {
Notifications.ok(result); this.props.showSuccessNotification(result);
} }
} catch (error) { } catch (error) {
if (showNotifications) { if (showNotifications) {
Notifications.checkedError(error, "Unknown error while submitting"); this.props.showCheckedErrorNotification(error, "Unknown error while submitting");
} }
} finally { } finally {
this.waiting = false; this.waiting = false;
@ -128,7 +136,7 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
<div className="flex gaps align-center"> <div className="flex gaps align-center">
{waiting ? ( {waiting ? (
<> <>
<Spinner /> <Spinner data-testid={this.props.submittingTestId} />
{" "} {" "}
{submittingMessage} {submittingMessage}
</> </>
@ -141,6 +149,7 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
plain plain
label="Cancel" label="Cancel"
onClick={close} onClick={close}
data-testid={this.props.cancelTestId}
/> />
<Button <Button
active active
@ -149,6 +158,7 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
label={submitLabel} label={submitLabel}
onClick={submit} onClick={submit}
disabled={isDisabled} disabled={isDisabled}
data-testid={this.props.submitTestId}
/> />
{showSubmitClose && ( {showSubmitClose && (
<Button <Button
@ -172,6 +182,8 @@ export const InfoPanel = withInjectables<Dependencies, InfoPanelProps>(
{ {
getProps: (di, props) => ({ getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable), dockStore: di.inject(dockStoreInjectable),
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
...props, ...props,
}), }),
}, },

View File

@ -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 { getChartValues } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
export type CallForHelmChartValues = (
repo: string,
name: string,
version: string
) => Promise<string>;
const callForHelmChartValuesInjectable = getInjectable({
id: "call-for-helm-chart-values",
instantiate: (): CallForHelmChartValues => getChartValues,
});
export default callForHelmChartValuesInjectable;

View File

@ -5,26 +5,27 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import installChartTabStoreInjectable from "./store.injectable"; import installChartTabStoreInjectable from "./store.injectable";
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api"; import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import type { import type { DockTab, DockTabCreateSpecific } from "../dock/store";
DockTab,
DockTabCreate,
DockTabCreateSpecific } from "../dock/store";
import { TabKind } from "../dock/store"; import { TabKind } from "../dock/store";
import type { InstallChartTabStore } from "./store";
import createDockTabInjectable from "../dock/create-dock-tab.injectable"; import createDockTabInjectable from "../dock/create-dock-tab.injectable";
import getRandomInstallChartTabIdInjectable from "./get-random-install-chart-tab-id.injectable";
interface Dependencies {
createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab;
installChartStore: InstallChartTabStore;
}
export type CreateInstallChartTab = (chart: HelmChart, tabParams?: DockTabCreateSpecific) => DockTab; export type CreateInstallChartTab = (chart: HelmChart, tabParams?: DockTabCreateSpecific) => DockTab;
const createInstallChartTab = ({ createDockTab, installChartStore }: Dependencies) => (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => { const createInstallChartTabInjectable = getInjectable({
id: "create-install-chart-tab",
instantiate: (di) => {
const installChartStore = di.inject(installChartTabStoreInjectable);
const createDockTab = di.inject(createDockTabInjectable);
const getRandomId = di.inject(getRandomInstallChartTabIdInjectable);
return (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => {
const { name, repo, version } = chart; const { name, repo, version } = chart;
const tab = createDockTab( const tab = createDockTab(
{ {
id: getRandomId(),
title: `Helm Install: ${repo}/${name}`, title: `Helm Install: ${repo}/${name}`,
...tabParams, ...tabParams,
kind: TabKind.INSTALL_CHART, kind: TabKind.INSTALL_CHART,
@ -42,15 +43,8 @@ const createInstallChartTab = ({ createDockTab, installChartStore }: Dependencie
}); });
return tab; return tab;
}; };
},
const createInstallChartTabInjectable = getInjectable({
id: "create-install-chart-tab",
instantiate: (di) => createInstallChartTab({
installChartStore: di.inject(installChartTabStoreInjectable),
createDockTab: di.inject(createDockTabInjectable),
}),
}); });
export default createInstallChartTabInjectable; export default createInstallChartTabInjectable;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import getRandomIdInjectable from "../../../../common/utils/get-random-id.injectable";
const getRandomInstallChartTabIdInjectable = getInjectable({
id: "get-random-install-chart-tab-id",
instantiate: (di) => di.inject(getRandomIdInjectable),
});
export default getRandomInstallChartTabIdInjectable;

View File

@ -0,0 +1,281 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import installChartTabStoreInjectable from "./store.injectable";
import { waitUntilDefined } from "../../../../common/utils";
import type { CallForHelmChartValues } from "./chart-data/call-for-helm-chart-values.injectable";
import callForHelmChartValuesInjectable from "./chart-data/call-for-helm-chart-values.injectable";
import type { IChartInstallData, InstallChartTabStore } from "./store";
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import React from "react";
import {
action,
computed,
observable,
runInAction,
} from "mobx";
import assert from "assert";
import type { CallForCreateHelmRelease } from "../../+helm-releases/create-release/call-for-create-helm-release.injectable";
import callForCreateHelmReleaseInjectable from "../../+helm-releases/create-release/call-for-create-helm-release.injectable";
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import dockStoreInjectable from "../dock/store.injectable";
import type { NavigateToHelmReleases } from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import type { SingleValue } from "react-select";
import type { CallForHelmChartVersions } from "../../+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
import callForHelmChartVersionsInjectable from "../../+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
const installChartModelInjectable = getInjectable({
id: "install-chart-model",
instantiate: async (di, tabId: string) => {
const store = di.inject(installChartTabStoreInjectable);
const callForHelmChartValues = di.inject(callForHelmChartValuesInjectable);
const callForHelmChartVersions = di.inject(callForHelmChartVersionsInjectable);
const callForCreateHelmRelease = di.inject(callForCreateHelmReleaseInjectable);
const dockStore = di.inject(dockStoreInjectable);
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
const closeTab = () => dockStore.closeTab(tabId);
const callChartByTab = async () => await waitUntilDefined(() => store.getData(tabId));
const model = new InstallChartModel({
tabId,
callChartByTab,
callForCreateHelmRelease,
closeTab,
navigateToHelmReleases,
callForHelmChartValues,
callForHelmChartVersions,
store,
});
await model.load();
return model;
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, tabId: string) => tabId,
}),
});
export default installChartModelInjectable;
interface Dependencies {
tabId: string;
closeTab: () => void;
navigateToHelmReleases: NavigateToHelmReleases;
callChartByTab: (tabId: string) => Promise<IChartInstallData>;
callForCreateHelmRelease: CallForCreateHelmRelease;
callForHelmChartValues: CallForHelmChartValues;
callForHelmChartVersions: CallForHelmChartVersions;
store: InstallChartTabStore;
}
export class InstallChartModel {
readonly namespace = {
value: computed(() => this.chart?.namespace || "default"),
onChange: action(
(option: SingleValue<{ label: string; value: string }>) => {
if (option) {
const namespace = option.value;
this.save({ namespace });
}
},
),
};
readonly customName = {
value: computed(() => this.chart?.releaseName || ""),
onChange: action((customName: string) => {
this.save({ releaseName: customName });
}),
};
private readonly versions = observable.array<HelmChart>([]);
readonly installed = observable.box<HelmReleaseUpdateDetails | undefined>();
private save = (data: Partial<IChartInstallData>) => {
assert(this.chart);
const chart = { ...this.chart, ...data };
this.dependencies.store.setData(this.dependencies.tabId, chart);
};
readonly version = {
value: computed(() => this.chart?.version),
onChange: async (version: string | undefined) => {
assert(this.chart);
if (!version) {
return;
}
this.save({ version });
runInAction(() => {
this.configuration.isLoading.set(true);
});
const configuration = await this.dependencies.callForHelmChartValues(
this.chart.repo,
this.chart.name,
version,
);
runInAction(() => {
this.configuration.onChange(configuration);
this.configuration.isLoading.set(false);
});
},
options: computed(() =>
this.versions.map((chart) => ({
label: chart.version,
value: chart.version,
})),
),
};
readonly configuration = {
value: computed(() => this.chart?.values || ""),
isLoading: observable.box(false),
onChange: action((configuration: string) => {
this.errorInConfiguration.value.set(undefined);
this.save({ values: configuration });
}),
};
readonly errorInConfiguration = {
value: observable.box<string | undefined>(),
onChange: action((error: unknown) => {
this.errorInConfiguration.value.set(error as string);
}),
};
readonly executionOutput = {
isShown: observable.box(false),
show: action(() => {
this.executionOutput.isShown.set(true);
}),
close: action(() => {
this.executionOutput.isShown.set(false);
}),
};
constructor(private readonly dependencies: Dependencies) {}
@computed
private get chart() {
const chart = this.dependencies.store.getData(this.dependencies.tabId);
assert(chart);
return chart;
}
load = async () => {
const tabId = this.dependencies.tabId;
await this.dependencies.callChartByTab(tabId);
const [defaultConfiguration, versions] = await Promise.all([
this.dependencies.callForHelmChartValues(
this.chart.repo,
this.chart.name,
this.chart.version,
),
this.dependencies.callForHelmChartVersions(
this.chart.repo,
this.chart.name,
),
]);
runInAction(() => {
// TODO: Make "default" not hard-coded
const namespace = this.chart.namespace || "default";
this.versions.replace(versions);
this.save({
version: this.chart.version,
namespace,
values: this.chart.values || defaultConfiguration,
releaseName: this.chart.releaseName,
});
});
};
@computed
get isValid() {
return !this.configuration.isLoading.get();
}
get chartName() {
return `${this.repository}/${this.name}`;
}
private get name() {
assert(this.chart);
return this.chart.name;
}
private get repository() {
assert(this.chart);
return this.chart.repo;
}
install = async () => {
const installed = await this.dependencies.callForCreateHelmRelease({
name: this.customName.value.get() || undefined,
chart: this.name,
repo: this.repository,
namespace: this.namespace.value.get() || "",
version: this.version.value.get() || "",
values: this.configuration.value.get() || "",
});
runInAction(() => {
this.installed.set(installed);
});
return (
<p>
{"Chart Release "}
<b>{installed.release.name}</b>
{" successfully created."}
</p>
);
};
navigateToInstalledRelease = () => {
const installed = this.installed.get();
assert(installed);
const release = installed.release;
this.dependencies.navigateToHelmReleases({
name: release.name,
namespace: release.namespace,
});
this.dependencies.closeTab();
};
}

View File

@ -3,13 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { action, makeObservable } from "mobx"; import { makeObservable } from "mobx";
import type { TabId } from "../dock/store";
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store"; import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
import { DockTabStore } from "../dock-tab-store/dock-tab.store"; import { DockTabStore } from "../dock-tab-store/dock-tab.store";
import { getChartDetails, getChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api"; import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { waitUntilDefined } from "../../../../common/utils/wait";
export interface IChartInstallData { export interface IChartInstallData {
name: string; name: string;
@ -40,42 +37,4 @@ export class InstallChartTabStore extends DockTabStore<IChartInstallData> {
get details() { get details() {
return this.dependencies.detailsStore; return this.dependencies.detailsStore;
} }
@action
async loadData(tabId: string) {
const promises = [];
const data = await waitUntilDefined(() => this.getData(tabId));
if (!this.getData(tabId)?.values) {
promises.push(this.loadValues(tabId));
}
if (!this.versions.getData(tabId)) {
promises.push(this.loadVersions(tabId, data));
}
await Promise.all(promises);
}
@action
private async loadVersions(tabId: TabId, { repo, name, version }: IChartInstallData) {
this.versions.clearData(tabId); // reset
const charts = await getChartDetails(repo, name, { version });
const versions = charts.versions.map(chartVersion => chartVersion.version);
this.versions.setData(tabId, versions);
}
@action
async loadValues(tabId: TabId, attempt = 0): Promise<void> {
const data = await waitUntilDefined(() => this.getData(tabId));
const { repo, name, version } = data;
const values = await getChartValues(repo, name, version);
if (values) {
this.setData(tabId, { ...data, values });
} else if (attempt < 4) {
return this.loadValues(tabId, attempt + 1);
}
}
} }

View File

@ -5,154 +5,44 @@
import "./install-chart.scss"; import "./install-chart.scss";
import React, { Component } from "react"; import React from "react";
import { action, makeObservable, observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { DockStore, DockTab } from "../dock/store"; import type { DockTab } from "../dock/store";
import { InfoPanel } from "../info-panel"; import { InfoPanel } from "../info-panel";
import { Badge } from "../../badge"; import { Badge } from "../../badge";
import { NamespaceSelect } from "../../+namespaces/namespace-select"; import { NamespaceSelect } from "../../+namespaces/namespace-select";
import { prevDefault } from "../../../utils"; import { prevDefault } from "../../../utils";
import type { IChartInstallData, InstallChartTabStore } from "./store";
import { Spinner } from "../../spinner";
import { Icon } from "../../icon"; import { Icon } from "../../icon";
import { Button } from "../../button"; import { Button } from "../../button";
import { LogsDialog } from "../../dialog/logs-dialog"; import { LogsDialog } from "../../dialog/logs-dialog";
import type { SelectOption } from "../../select";
import { Select } from "../../select"; import { Select } from "../../select";
import { Input } from "../../input"; import { Input } from "../../input";
import { EditorPanel } from "../editor-panel"; import { EditorPanel } from "../editor-panel";
import type { HelmReleaseCreatePayload, HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import installChartTabStoreInjectable from "./store.injectable"; import type { InstallChartModel } from "./install-chart-model.injectable";
import dockStoreInjectable from "../dock/store.injectable"; import installChartModelInjectable from "./install-chart-model.injectable";
import createReleaseInjectable from "../../+helm-releases/create-release/create-release.injectable"; import { Spinner } from "../../spinner";
import { Notifications } from "../../notifications";
import type { NavigateToHelmReleases } from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import assert from "assert";
import type { SingleValue } from "react-select";
export interface InstallCharProps { export interface InstallCharProps {
tab: DockTab; tab: DockTab;
} }
interface Dependencies { interface Dependencies {
createRelease: (payload: HelmReleaseCreatePayload) => Promise<HelmReleaseUpdateDetails>; model: InstallChartModel;
installChartStore: InstallChartTabStore;
dockStore: DockStore;
navigateToHelmReleases: NavigateToHelmReleases;
} }
@observer const NonInjectedInstallChart = observer(
class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies> { ({ model: model, tab: { id: tabId }}: InstallCharProps & Dependencies) => {
@observable error = ""; const installed = model.installed.get();
@observable showNotes = false;
constructor(props: InstallCharProps & Dependencies) { if (installed) {
super(props);
makeObservable(this);
}
componentDidMount(): void {
this.props.installChartStore.loadData(this.tabId)
.catch(err => Notifications.error(String(err)));
}
get chartData() {
return this.props.installChartStore.getData(this.tabId);
}
get tabId() {
return this.props.tab.id;
}
get versions() {
return this.props.installChartStore.versions.getData(this.tabId);
}
get releaseDetails() {
return this.props.installChartStore.details.getData(this.tabId);
}
viewRelease = ({ release }: HelmReleaseUpdateDetails) => {
this.props.navigateToHelmReleases({
name: release.name,
namespace: release.namespace,
});
this.props.dockStore.closeTab(this.tabId);
};
save(data: Partial<IChartInstallData>) {
assert(this.chartData, "Cannot update data before data exists");
this.props.installChartStore.setData(this.tabId, { ...this.chartData, ...data });
}
onVersionChange = (option: SingleValue<SelectOption<string>>) => {
if (option) {
this.save({ ...option, values: "" });
this.props.installChartStore.loadValues(this.tabId);
}
};
onChange = action((values: string) => {
this.error = "";
this.save({ values });
});
onError = action((error: Error | string) => {
this.error = error.toString();
});
onNamespaceChange = (option: SingleValue<SelectOption<string>>) => {
if (option) {
this.save({ namespace: option.value });
}
};
onReleaseNameChange = (name: string) => {
this.save({ releaseName: name });
};
install = async ({ repo, name, version, namespace, values = "", releaseName }: IChartInstallData) => {
const details = await this.props.createRelease({
name: releaseName || undefined,
chart: name,
repo,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
namespace: namespace!,
version,
values,
});
this.props.installChartStore.details.setData(this.tabId, details);
return (
<p>
{"Chart Release "}
<b>{details.release.name}</b>
{" successfully created."}
</p>
);
};
render() {
const { tabId, chartData, versions, install, releaseDetails } = this;
if (chartData?.values === undefined || !versions) {
return <Spinner center />;
}
if (releaseDetails) {
return ( return (
<div className="InstallChartDone flex column gaps align-center justify-center"> <div className="InstallChartDone flex column gaps align-center justify-center">
<p> <p>
<Icon <Icon
material="check" material="check"
big big
sticker sticker />
/>
</p> </p>
<p>Installation complete!</p> <p>Installation complete!</p>
<div className="flex gaps align-center"> <div className="flex gaps align-center">
@ -160,30 +50,34 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
autoFocus autoFocus
primary primary
label="View Helm Release" label="View Helm Release"
onClick={prevDefault(() => this.viewRelease(releaseDetails))} onClick={prevDefault(model.navigateToInstalledRelease)}
data-testid={`show-release-${installed.release.name}-for-${tabId}`}
/> />
<Button <Button
plain plain
active active
label="Show Notes" label="Show Notes"
onClick={() => this.showNotes = true} onClick={model.executionOutput.show}
data-testid={`show-execution-output-for-${installed.release.name}-in-${tabId}`}
/> />
</div> </div>
<LogsDialog <LogsDialog
title="Helm Chart Install" title="Helm Chart Install"
isOpen={this.showNotes} isOpen={model.executionOutput.isShown.get()}
close={() => this.showNotes = false} close={model.executionOutput.close}
logs={releaseDetails.log} logs={installed.log}
/> />
</div> </div>
); );
} }
const { repo, name, version, namespace, releaseName } = chartData; const {
const versionOptions = versions.map(version => ({ configuration,
value: version, version,
label: version, namespace,
})); customName,
errorInConfiguration,
} = model;
return ( return (
<div className="InstallChart flex column"> <div className="InstallChart flex column">
@ -192,60 +86,77 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
controls={( controls={(
<div className="install-controls flex gaps align-center"> <div className="install-controls flex gaps align-center">
<span>Chart</span> <span>Chart</span>
<Badge label={`${repo}/${name}`} title="Repo/Name" /> <Badge label={model.chartName} title="Repo/Name" />
<span>Version</span> <span>Version</span>
<Select <Select
className="chart-version" className="chart-version"
value={version} value={version.value.get()}
options={versionOptions} options={version.options.get()}
onChange={this.onVersionChange} onChange={(changed) => version.onChange(changed?.value)}
menuPlacement="top" menuPlacement="top"
themeName="outlined" themeName="outlined"
id={`install-chart-version-select-for-${tabId}`}
/> />
<span>Namespace</span> <span>Namespace</span>
<NamespaceSelect <NamespaceSelect
showIcons={false} showIcons={false}
menuPlacement="top" menuPlacement="top"
themeName="outlined" themeName="outlined"
value={namespace} value={namespace.value.get()}
onChange={this.onNamespaceChange} onChange={namespace.onChange}
id={`install-chart-namespace-select-for-${tabId}`}
/> />
<Input <Input
placeholder="Name (optional)" placeholder="Name (optional)"
title="Release name" title="Release name"
maxLength={50} maxLength={50}
value={releaseName} value={customName.value.get()}
onChange={this.onReleaseNameChange} onChange={customName.onChange}
data-testid={`install-chart-custom-name-input-for-${tabId}`}
/> />
</div> </div>
)} )}
error={this.error} error={errorInConfiguration.value.get()}
submit={() => install(chartData)} submit={model.install}
disableSubmit={!chartData.namespace} disableSubmit={!model.isValid} // !namespace
submitLabel="Install" submitLabel="Install"
submittingMessage="Installing..." submittingMessage="Installing..."
showSubmitClose={false} showSubmitClose={false}
cancelTestId={`cancel-install-chart-from-tab-for-${tabId}`}
submitTestId={`install-chart-from-tab-for-${tabId}`}
submittingTestId={`installing-chart-from-tab-${tabId}`}
/> />
{configuration.isLoading.get() && (
<Spinner center data-testid="install-chart-configuration-spinner" />
)}
<EditorPanel <EditorPanel
tabId={tabId} tabId={tabId}
value={chartData.values} value={configuration.value.get()}
onChange={this.onChange} onChange={configuration.onChange}
onError={this.onError} onError={errorInConfiguration.onChange}
hidden={configuration.isLoading.get()}
/> />
</div> </div>
); );
} },
} );
export const InstallChart = withInjectables<Dependencies, InstallCharProps>( export const InstallChart = withInjectables<Dependencies, InstallCharProps>(
NonInjectedInstallChart, NonInjectedInstallChart,
{ {
getProps: (di, props) => ({ getPlaceholder: () => (
createRelease: di.inject(createReleaseInjectable), <Spinner
installChartStore: di.inject(installChartTabStoreInjectable), center
dockStore: di.inject(dockStoreInjectable), data-testid="install-chart-tab-spinner"
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable), />
),
getProps: async (di, props) => ({
model: await di.inject(installChartModelInjectable, props.tab.id),
...props, ...props,
}), }),
}, },

View File

@ -19,15 +19,19 @@ export interface TableRowProps<Item> extends React.DOMAttributes<HTMLDivElement>
sortItem?: Item; // data for sorting callback in <Table sortable={}/> sortItem?: Item; // data for sorting callback in <Table sortable={}/>
searchItem?: Item; // data for searching filters in <Table searchable={}/> searchItem?: Item; // data for searching filters in <Table searchable={}/>
disabled?: boolean; disabled?: boolean;
testId?: string;
} }
export class TableRow<Item> extends React.Component<TableRowProps<Item>> { export class TableRow<Item> extends React.Component<TableRowProps<Item>> {
render() { render() {
const { className, nowrap, selected, disabled, children, sortItem, searchItem, ...rowProps } = this.props; const { className, nowrap, selected, disabled, children, sortItem, searchItem, testId, ...rowProps } = this.props;
const classNames = cssNames("TableRow", className, { selected, nowrap, disabled }); const classNames = cssNames("TableRow", className, { selected, nowrap, disabled });
return ( return (
<div className={classNames} {...rowProps}> <div
className={classNames}
data-testid={testId}
{...rowProps}>
{children} {children}
</div> </div>
); );