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

Fix showing-details-for-helm-release behavioural tests

- Remove HelmChartStore in favour of all injectables

- Create a model for UpgradeChartDockTab

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-08-10 12:58:47 -04:00
parent 4bef236f8c
commit 042b679ca6
14 changed files with 447 additions and 375 deletions

View File

@ -434,14 +434,8 @@ exports[`opening dock tab for installing helm chart given application is started
</div>
</div>
<div
class="NoItems flex box grow"
>
<div
class="box center"
>
Item list is empty
</div>
</div>
class="Spinner singleColor center"
/>
</div>
<div
class="AddRemoveButtons flex gaps"

View File

@ -12295,8 +12295,147 @@ exports[`showing details for helm release given application is started when navi
style="flex-basis: 300px;"
>
<div
class="Spinner singleColor center"
/>
class="UpgradeChart flex column"
>
<div
class="InfoPanel flex gaps align-center"
>
<div
class="controls"
>
<div
class="upgrade flex gaps align-center"
>
<span>
Release
</span>
<div
class="badge"
>
some-name
</div>
<span>
Namespace
</span>
<div
class="badge"
>
some-namespace
</div>
<span>
Version
</span>
<div
class="badge"
/>
<span>
Upgrade version
</span>
<div
class="Select theme-outlined chart-version css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-char-version-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container css-319lph-ValueContainer"
>
<div
class="Select__placeholder css-14el2xx-placeholder"
id="react-select-char-version-input-placeholder"
>
Select...
</div>
<div
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-char-version-input-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="char-version-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="flex gaps align-center"
/>
<button
class="Button plain"
type="button"
>
Cancel
</button>
<button
class="Button active outlined"
type="button"
>
Upgrade
</button>
<button
class="Button primary active"
type="button"
>
Upgrade & Close
</button>
</div>
<textarea
data-testid="monaco-editor-for-some-tab-id"
/>
</div>
</div>
</div>
</div>

View File

@ -23,6 +23,14 @@ import requestHelmReleaseInjectable from "../../renderer/components/+helm-releas
import showSuccessNotificationInjectable from "../../renderer/components/notifications/show-success-notification.injectable";
import showCheckedErrorInjectable from "../../renderer/components/notifications/show-checked-error.injectable";
import getRandomUpgradeChartTabIdInjectable from "../../renderer/components/dock/upgrade-chart/get-random-upgrade-chart-tab-id.injectable";
import type { RequestHelmCharts } from "../../common/k8s-api/endpoints/helm-charts.api/list.injectable";
import type { RequestHelmChartVersions } from "../../common/k8s-api/endpoints/helm-charts.api/get-versions.injectable";
import type { RequestHelmChartReadme } from "../../common/k8s-api/endpoints/helm-charts.api/get-readme.injectable";
import type { RequestHelmChartValues } from "../../common/k8s-api/endpoints/helm-charts.api/get-values.injectable";
import requestHelmChartsInjectable from "../../common/k8s-api/endpoints/helm-charts.api/list.injectable";
import requestHelmChartVersionsInjectable from "../../common/k8s-api/endpoints/helm-charts.api/get-versions.injectable";
import requestHelmChartReadmeInjectable from "../../common/k8s-api/endpoints/helm-charts.api/get-readme.injectable";
import requestHelmChartValuesInjectable from "../../common/k8s-api/endpoints/helm-charts.api/get-values.injectable";
describe("showing details for helm release", () => {
let builder: ApplicationBuilder;
@ -30,6 +38,10 @@ describe("showing details for helm release", () => {
let requestHelmReleaseMock: AsyncFnMock<RequestHelmRelease>;
let requestHelmReleaseConfigurationMock: AsyncFnMock<RequestHelmReleaseConfiguration>;
let requestHelmReleaseUpdateMock: AsyncFnMock<RequestHelmReleaseUpdate>;
let requestHelmChartsMock: AsyncFnMock<RequestHelmCharts>;
let requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
let requestHelmChartReadmeMock: AsyncFnMock<RequestHelmChartReadme>;
let requestHelmChartValuesMock: AsyncFnMock<RequestHelmChartValues>;
let showSuccessNotificationMock: jest.Mock;
let showCheckedErrorNotificationMock: jest.Mock;
@ -44,6 +56,10 @@ describe("showing details for helm release", () => {
requestHelmReleaseMock = asyncFn();
requestHelmReleaseConfigurationMock = asyncFn();
requestHelmReleaseUpdateMock = asyncFn();
requestHelmChartsMock = asyncFn();
requestHelmChartVersionsMock = asyncFn();
requestHelmChartReadmeMock = asyncFn();
requestHelmChartValuesMock = asyncFn();
showSuccessNotificationMock = jest.fn();
showCheckedErrorNotificationMock = jest.fn();
@ -56,6 +72,10 @@ describe("showing details for helm release", () => {
windowDi.override(requestHelmReleaseInjectable, () => requestHelmReleaseMock);
windowDi.override(requestHelmReleaseConfigurationInjectable, () => requestHelmReleaseConfigurationMock);
windowDi.override(requestHelmReleaseUpdateInjectable, () => requestHelmReleaseUpdateMock);
windowDi.override(requestHelmChartsInjectable, () => requestHelmChartsMock);
windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock);
windowDi.override(requestHelmChartReadmeInjectable, () => requestHelmChartReadmeMock);
windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock);
windowDi.override(
namespaceStoreInjectable,
() =>
@ -449,6 +469,16 @@ describe("showing details for helm release", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows spinner", () => {
const saveButton = rendered.getByTestId(
"helm-release-configuration-save-button",
);
expect(saveButton).toHaveClass("waiting");
});
it("calls for update", () => {
expect(requestHelmReleaseUpdateMock).toHaveBeenCalledWith(
"some-name",
@ -463,14 +493,6 @@ describe("showing details for helm release", () => {
);
});
it("shows spinner", () => {
const saveButton = rendered.getByTestId(
"helm-release-configuration-save-button",
);
expect(saveButton).toHaveClass("waiting");
});
describe("when update resolves with success", () => {
beforeEach(async () => {
requestHelmReleasesMock.mockClear();

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import getHelmChartReadmeInjectable from "./get-helm-chart-readme.injectable";
export default getGlobalOverride(getHelmChartReadmeInjectable, () => () => {
throw new Error("tried to get a helm chart's readme without overriding");
});

View File

@ -20,8 +20,7 @@ import navigateToHelmChartsInjectable from "../../../common/front-end-routing/ro
import { HelmChartIcon } from "./icon";
import helmChartsInjectable from "./helm-charts/helm-charts.injectable";
import selectedHelmChartInjectable from "./helm-charts/selected-helm-chart.injectable";
import type { HelmChartStore } from "./store";
import helmChartStoreInjectable from "./store.injectable";
import { noop } from "lodash";
enum columnId {
name = "name",
@ -39,7 +38,6 @@ interface Dependencies {
navigateToHelmCharts: NavigateToHelmCharts;
charts: IAsyncComputed<HelmChart[]>;
selectedChart: IComputedValue<HelmChart | undefined>;
helmChartStore: HelmChartStore;
}
@observer
@ -69,17 +67,31 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
};
render() {
const { charts } = this.props;
const selectedChart = this.props.selectedChart.get();
return (
<SiblingsInTabLayout>
<div data-testid="page-for-helm-charts" style={{ display: "none" }}/>
<ItemListLayout
<ItemListLayout<HelmChart, false>
isConfigurable
tableId="helm_charts"
className="HelmCharts"
store={this.props.helmChartStore}
store={{
get isLoaded() {
return !charts.pending.get();
},
failedLoading: false,
getTotalCount: () => charts.value.get().length,
isSelected: (item) => item === selectedChart,
toggleSelection: noop,
isSelectedAll: () => false,
toggleSelectionAll: () => false,
pickOnlySelected: () => [],
removeSelectedItems: async () => {},
}}
preloadStores={false}
getItems={() => this.props.charts.value.get()}
isSelectable={false}
sortingCallbacks={{
@ -138,8 +150,6 @@ export const HelmCharts = withInjectables<Dependencies>(NonInjectedHelmCharts, {
navigateToHelmCharts: di.inject(navigateToHelmChartsInjectable),
charts: di.inject(helmChartsInjectable),
selectedChart: di.inject(selectedHelmChartInjectable),
helmChartStore: di.inject(helmChartStoreInjectable),
}),
},
);
});

View File

@ -0,0 +1,47 @@
/**
* 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 { coerce } from "semver";
import requestHelmChartVersionsInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/get-versions.injectable";
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { sortCompareChartVersions } from "../../../utils";
import helmChartsInjectable from "./helm-charts.injectable";
export interface ChartVersion {
repo: string;
version: string;
}
const sortChartVersions = (versions: ChartVersion[]) => (
versions
.map(chartVersion => ({ ...chartVersion, __version: coerce(chartVersion.version, { loose: true }) }))
.sort(sortCompareChartVersions)
.map(({ __version, ...chartVersion }) => chartVersion)
);
const helmChartVersionsInjectable = getInjectable({
id: "helm-chart-versions-loader",
instantiate: (di, release) => {
const requestHelmChartVersions = di.inject(requestHelmChartVersionsInjectable);
const helmCharts = di.inject(helmChartsInjectable);
return asyncComputed(async () => {
const rawVersions = await Promise.all((
helmCharts.value.get()
.filter(chart => chart.getName() === release.getChart())
.map(chart => chart.getRepository())
.map(repo => requestHelmChartVersions(repo, release.getChart()))
));
return sortChartVersions(rawVersions.flat());
}, []);
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, release: HelmRelease) => release.getName(),
}),
});
export default helmChartVersionsInjectable;

View File

@ -1,18 +0,0 @@
/**
* 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 requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/get-versions.injectable";
import requestHelmChartsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/list.injectable";
import { HelmChartStore } from "./store";
const helmChartStoreInjectable = getInjectable({
id: "helm-chart-store",
instantiate: (di) => new HelmChartStore({
requestHelmCharts: di.inject(requestHelmChartsInjectable),
requestHelmChartVersions: di.inject(requestHelmChartVersionsInjectable),
}),
});
export default helmChartStoreInjectable;

View File

@ -1,112 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import semver from "semver";
import { observable, makeObservable } from "mobx";
import { autoBind, sortCompareChartVersions } from "../../utils";
import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
import { ItemStore } from "../../../common/item.store";
import flatten from "lodash/flatten";
import type { RequestHelmCharts } from "../../../common/k8s-api/endpoints/helm-charts.api/list.injectable";
import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/get-versions.injectable";
export interface ChartVersion {
repo: string;
version: string;
}
interface Dependencies {
requestHelmCharts: RequestHelmCharts;
requestHelmChartVersions: RequestHelmChartVersions;
}
export class HelmChartStore extends ItemStore<HelmChart> {
@observable versions = observable.map<string, ChartVersion[]>();
constructor(protected readonly dependencies: Dependencies) {
super();
makeObservable(this);
autoBind(this);
}
async loadAll() {
try {
const res = await this.loadItems(() => this.dependencies.requestHelmCharts());
this.failedLoading = false;
return res;
} catch (error) {
this.failedLoading = true;
throw error;
}
}
getByName(name: string, repo?: string) {
if (typeof repo !== "string") {
/**
* FIXME:
* This is here because in strict mode `getByName` MUST be 100% compatiable if called in the
* situation where it is only a "ItemStore"
*/
throw new TypeError("repo must be provided");
}
return this.items.find(chart => chart.getName() === name && chart.getRepository() === repo);
}
protected sortVersions = (versions: ChartVersion[]) => {
return versions
.map(chartVersion => ({ ...chartVersion, __version: semver.coerce(chartVersion.version, { loose: true }) }))
.sort(sortCompareChartVersions)
.map(({ __version, ...chartVersion }) => chartVersion);
};
async getVersions(chartName: string, force?: boolean): Promise<ChartVersion[]> {
const versions = this.versions.get(chartName);
if (versions && !force) {
return versions;
}
const loadVersions = async (repo: string) => {
const versions = await this.dependencies.requestHelmChartVersions(repo, chartName);
return versions.map(chart => ({
repo,
version: chart.getVersion(),
}));
};
if (!this.isLoaded) {
await this.loadAll();
}
const repos = this.items
.filter(chart => chart.getName() === chartName)
.map(chart => chart.getRepository());
const newVersions = await Promise.all(repos.map(loadVersions))
.then(flatten)
.then(this.sortVersions);
this.versions.set(chartName, newVersions);
return newVersions;
}
reset() {
super.reset();
this.versions.clear();
}
/**
* @deprecated Not supported
*/
removeItems(): Promise<void> {
throw new Error("removeItems is not supported");
}
}

View File

@ -4,7 +4,8 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { capitalize } from "lodash";
import helmChartStoreInjectable from "../+helm-charts/store.injectable";
import { when } from "mobx";
import helmChartVersionsInjectable from "../+helm-charts/helm-charts/versions.injectable";
import type { HelmRelease, HelmReleaseDto } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { formatDuration } from "../../utils";
@ -13,7 +14,7 @@ export type ToHelmRelease = (release: HelmReleaseDto) => HelmRelease;
const toHelmReleaseInjectable = getInjectable({
id: "to-helm-release",
instantiate: (di): ToHelmRelease => {
const helmChartStore = di.inject(helmChartStoreInjectable);
const helmChartVersions = (release: HelmRelease) => di.inject(helmChartVersionsInjectable, release);
return (release) => ({
...release,
@ -71,14 +72,14 @@ const toHelmReleaseInjectable = getInjectable({
// Helm does not store from what repository the release is installed,
// so we have to try to guess it by searching charts
async getRepo() {
const chartName = this.getChart();
const version = this.getVersion();
const versions = await helmChartStore.getVersions(chartName);
const chartVersion = versions.find(
(chartVersion) => chartVersion.version === version,
);
const versionsComputed = helmChartVersions(this);
return chartVersion ? chartVersion.repo : "";
await when(() => !versionsComputed.pending.get());
const version = this.getVersion();
const versions = versionsComputed.value.get();
return versions.find((chartVersion) => chartVersion.version === version)?.repo ?? "";
},
});
},

View File

@ -3,23 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { IChartUpgradeData } from "./store.injectable";
import upgradeChartTabStoreInjectable from "./store.injectable";
import dockStoreInjectable from "../dock/store.injectable";
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import type { DockStore, DockTabCreateSpecific, TabId } from "../dock/store";
import { TabKind } from "../dock/store";
import type { UpgradeChartTabStore } from "./store";
import { runInAction } from "mobx";
import getRandomUpgradeChartTabIdInjectable from "./get-random-upgrade-chart-tab-id.injectable";
import type { DockTabStore } from "../dock-tab-store/dock-tab.store";
interface Dependencies {
upgradeChartStore: UpgradeChartTabStore;
upgradeChartStore: DockTabStore<IChartUpgradeData>;
dockStore: DockStore;
getRandomId: () => string;
}
const createUpgradeChartTab = ({ upgradeChartStore, dockStore, getRandomId }: Dependencies) => (release: HelmRelease, tabParams: DockTabCreateSpecific = {}): TabId => {
const tabId = upgradeChartStore.getTabIdByRelease(release.getName());
const tabId = upgradeChartStore.findTabIdFromData(val => (
val.releaseName === release.getName()
&& val.releaseNamespace === release.getNs()
));
if (tabId) {
dockStore.open();

View File

@ -3,10 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { UpgradeChartTabStore } from "./store";
import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
import requestHelmReleaseConfigurationInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/get-configuration.injectable";
export interface IChartUpgradeData {
releaseName: string;
releaseNamespace: string;
}
const upgradeChartTabStoreInjectable = getInjectable({
id: "upgrade-chart-tab-store",
@ -14,10 +16,8 @@ const upgradeChartTabStoreInjectable = getInjectable({
instantiate: (di) => {
const createDockTabStore = di.inject(createDockTabStoreInjectable);
return new UpgradeChartTabStore({
createStorage: di.inject(createStorageInjectable),
valuesStore: createDockTabStore<string>(),
requestHelmReleaseConfiguration: di.inject(requestHelmReleaseConfigurationInjectable),
return createDockTabStore<IChartUpgradeData>({
storageKey: "chart_releases",
});
},
});

View File

@ -1,56 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, computed, makeObservable } from "mobx";
import type { TabId } from "../dock/store";
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
import assert from "assert";
import type { RequestHelmReleaseConfiguration } from "../../../../common/k8s-api/endpoints/helm-releases.api/get-configuration.injectable";
export interface IChartUpgradeData {
releaseName: string;
releaseNamespace: string;
}
export interface UpgradeChartTabStoreDependencies extends DockTabStoreDependencies {
valuesStore: DockTabStore<string>;
requestHelmReleaseConfiguration: RequestHelmReleaseConfiguration;
}
export class UpgradeChartTabStore extends DockTabStore<IChartUpgradeData> {
@computed private get releaseNameReverseLookup(): Map<string, string> {
return new Map(this.getAllData().map(([id, { releaseName }]) => [releaseName, id]));
}
get values() {
return this.dependencies.valuesStore;
}
constructor(protected readonly dependencies: UpgradeChartTabStoreDependencies) {
super(dependencies, {
storageKey: "chart_releases",
});
makeObservable(this);
}
@action
async reloadValues(tabId: TabId) {
this.values.clearData(tabId); // reset
const data = this.getData(tabId);
assert(data, "cannot reload values if no data");
const { releaseName, releaseNamespace } = data;
const values = await this.dependencies.requestHelmReleaseConfiguration(releaseName, releaseNamespace, true);
this.values.setData(tabId, values);
}
getTabIdByRelease(releaseName: string) {
return this.releaseNameReverseLookup.get(releaseName);
}
}

View File

@ -0,0 +1,121 @@
/**
* 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 type { IComputedValue } from "mobx";
import { action, computed, observable, when } from "mobx";
import type { SingleValue } from "react-select";
import type { ChartVersion } from "../../+helm-charts/helm-charts/versions.injectable";
import helmChartVersionsInjectable from "../../+helm-charts/helm-charts/versions.injectable";
import releasesInjectable from "../../+helm-releases/releases.injectable";
import updateReleaseInjectable from "../../+helm-releases/update-release/update-release.injectable";
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import requestHelmReleaseConfigurationInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/get-configuration.injectable";
import { waitUntilDefined } from "../../../utils";
import type { SelectOption } from "../../select";
import type { DockTab } from "../dock/store";
import upgradeChartTabStoreInjectable from "./store.injectable";
export interface UpgradeChartModel {
readonly release: HelmRelease;
readonly versionOptions: IComputedValue<SelectOption<ChartVersion>[]>;
readonly configration: {
readonly value: IComputedValue<string>;
set: (value: string) => void;
readonly error: IComputedValue<string | undefined>;
setError: (error: unknown) => void;
};
readonly version: {
readonly value: IComputedValue<ChartVersion | undefined>;
set: (value: SingleValue<SelectOption<ChartVersion>>) => void;
};
submit: () => Promise<UpgradeChartSubmitResult>;
}
export interface UpgradeChartSubmitResult {
completedSuccessfully: boolean;
}
const upgradeChartModelInjectable = getInjectable({
id: "upgrade-chart-model",
instantiate: async (di, tab): Promise<UpgradeChartModel> => {
const upgradeChartTabStore = di.inject(upgradeChartTabStoreInjectable);
const releases = di.inject(releasesInjectable);
const requestHelmReleaseConfiguration = di.inject(requestHelmReleaseConfigurationInjectable);
const updateRelease = di.inject(updateReleaseInjectable);
const tabData = await waitUntilDefined(() => upgradeChartTabStore.getData(tab.id));
const release = await waitUntilDefined(() => releases.value.get().find(release => release.getName() === tabData.releaseName));
const versions = di.inject(helmChartVersionsInjectable, release);
const storedConfigration = asyncComputed(() => requestHelmReleaseConfiguration(
release.getName(),
release.getNs(),
true,
), "");
await when(() => !versions.pending.get());
const configrationValue = observable.box<string>();
const configrationEditError = observable.box<string>();
const configration: UpgradeChartModel["configration"] = {
value: computed(() => configrationValue.get() ?? storedConfigration.value.get()),
set: action((value) => {
configrationValue.set(value);
configrationEditError.set(undefined);
}),
error: computed(() => configrationEditError.get()),
setError: action((error) => configrationEditError.set(String(error))),
};
const versionValue = observable.box<ChartVersion | undefined>(versions.value.get()[0]);
const version: UpgradeChartModel["version"] = {
value: computed(() => versionValue.get()),
set: action((option) => versionValue.set(option?.value)),
};
const versionOptions = computed(() => (
versions.value
.get()
.map(version => ({
value: version,
label: `${version.repo}/${release.getChart()}-${version.version}`,
}))
));
return {
release,
versionOptions,
configration,
version,
submit: async () => {
const version = versionValue.get();
if (!version || configrationEditError.get()) {
return {
completedSuccessfully: false,
};
}
await updateRelease(
release.getName(),
release.getNs(),
{
chart: release.getChart(),
values: configration.value.get(),
...version,
},
);
storedConfigration.invalidate();
return {
completedSuccessfully: true,
};
},
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, tab: DockTab) => tab.id,
}),
});
export default upgradeChartModelInjectable;

View File

@ -6,27 +6,20 @@
import "./upgrade-chart.scss";
import React from "react";
import { action, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import { cssNames } from "../../../utils";
import type { DockTab } from "../dock/store";
import { InfoPanel } from "../info-panel";
import type { UpgradeChartTabStore } from "./store";
import { Spinner } from "../../spinner";
import { Badge } from "../../badge";
import { EditorPanel } from "../editor-panel";
import type { HelmChartStore, ChartVersion } from "../../+helm-charts/store";
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import type { SelectOption } from "../../select";
import { Select } from "../../select";
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import upgradeChartTabStoreInjectable from "./store.injectable";
import updateReleaseInjectable from "../../+helm-releases/update-release/update-release.injectable";
import releasesInjectable from "../../+helm-releases/releases.injectable";
import type { RequestHelmReleaseUpdate } from "../../../../common/k8s-api/endpoints/helm-releases.api/update.injectable";
import { first } from "lodash/fp";
import helmChartStoreInjectable from "../../+helm-charts/store.injectable";
import type { ChartVersion } from "../../+helm-charts/helm-charts/versions.injectable";
import type { UpgradeChartModel } from "./upgrade-chart-model.injectable";
import upgradeChartModelInjectable from "./upgrade-chart-model.injectable";
export interface UpgradeChartProps {
className?: string;
@ -34,160 +27,78 @@ export interface UpgradeChartProps {
}
interface Dependencies {
releases: IAsyncComputed<HelmRelease[]>;
upgradeChartTabStore: UpgradeChartTabStore;
updateRelease: RequestHelmReleaseUpdate;
helmChartStore: HelmChartStore;
model: UpgradeChartModel;
}
@observer
export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps & Dependencies> {
@observable error?: string;
@observable versions = observable.array<ChartVersion>();
@observable version: ChartVersion | undefined = undefined;
constructor(props: UpgradeChartProps & Dependencies) {
super(props);
makeObservable(this);
}
componentDidMount() {
disposeOnUnmount(this, [
reaction(
() => this.release,
release => this.reloadVersions(release),
{
fireImmediately: true,
},
),
reaction(
() => this.release?.getRevision(),
() => this.reloadValues(),
{
fireImmediately: true,
},
),
]);
}
get tabId() {
return this.props.tab.id;
}
get release() {
const tabData = this.props.upgradeChartTabStore.getData(this.tabId);
if (!tabData) return null;
return this.props.releases.value.get().find(release => release.getName() === tabData.releaseName);
}
get value() {
return this.props.upgradeChartTabStore.values.getData(this.tabId);
}
async reloadValues() {
this.props.upgradeChartTabStore.reloadValues(this.props.tab.id);
}
async reloadVersions(release: HelmRelease | null | undefined) {
if (!release) {
return;
}
this.version = undefined;
this.versions.clear();
const versions = await this.props.helmChartStore.getVersions(release.getChart());
this.versions.replace(versions);
this.version = first(this.versions);
}
onChange = action((value: string) => {
this.error = "";
this.props.upgradeChartTabStore.values.setData(this.tabId, value);
});
onError = action((error: Error | string) => {
this.error = error.toString();
});
upgrade = async () => {
if (this.error || !this.release || !this.version || !this.value) {
return null;
const { model } = this.props;
const { completedSuccessfully } = await model.submit();
if (completedSuccessfully) {
return (
<p>
{"Release "}
<b>{model.release.getName()}</b>
{" successfully upgraded to version "}
<b>{model.version.value.get()}</b>
</p>
);
}
const { version, repo } = this.version;
const releaseName = this.release.getName();
const releaseNs = this.release.getNs();
await this.props.updateRelease(releaseName, releaseNs, {
chart: this.release.getChart(),
values: this.value,
repo, version,
});
return (
<p>
{"Release "}
<b>{releaseName}</b>
{" successfully upgraded to version "}
<b>{version}</b>
</p>
);
return null;
};
render() {
const { tabId, release, value, error, onChange, onError, upgrade, versions, version } = this;
const { className } = this.props;
if (!release || !this.props.upgradeChartTabStore.isReady(tabId) || !version) {
return <Spinner center />;
}
const currentVersion = release.getVersion();
const versionOptions = versions.map(version => ({
value: version,
label: `${version.repo}/${release.getChart()}-${version.version}`,
}));
const controlsAndInfo = (
<div className="upgrade flex gaps align-center">
<span>Release</span>
{" "}
<Badge label={release.getName()} />
<span>Namespace</span>
{" "}
<Badge label={release.getNs()} />
<span>Version</span>
{" "}
<Badge label={currentVersion} />
<span>Upgrade version</span>
<Select<ChartVersion, SelectOption<ChartVersion>, false>
id="char-version-input"
className="chart-version"
menuPlacement="top"
themeName="outlined"
value={version}
options={versionOptions}
onChange={option => this.version = option?.value}
/>
</div>
);
const { model, className, tab } = this.props;
const tabId = tab.id;
const { release } = model;
return (
<div className={cssNames("UpgradeChart flex column", className)}>
<InfoPanel
tabId={tabId}
error={error}
submit={upgrade}
error={model.configration.error.get()}
submit={this.upgrade}
submitLabel="Upgrade"
submittingMessage="Updating.."
controls={controlsAndInfo}
controls={(
<div className="upgrade flex gaps align-center">
<span>Release</span>
{" "}
<Badge label={release.getName()} />
<span>Namespace</span>
{" "}
<Badge label={release.getNs()} />
<span>Version</span>
{" "}
<Badge label={model.version.value.get()} />
<span>Upgrade version</span>
<Select<ChartVersion, SelectOption<ChartVersion>, false>
id="char-version-input"
className="chart-version"
menuPlacement="top"
themeName="outlined"
value={model.version.value.get()}
options={model.versionOptions.get()}
onChange={model.version.set}
/>
</div>
)}
/>
<EditorPanel
tabId={tabId}
value={value ?? ""}
onChange={onChange}
onError={onError}
value={model.configration.value.get()}
onChange={model.configration.set}
onError={model.configration.setError}
/>
</div>
);
@ -195,11 +106,9 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
}
export const UpgradeChart = withInjectables<Dependencies, UpgradeChartProps>(NonInjectedUpgradeChart, {
getProps: (di, props) => ({
getPlaceholder: () => <Spinner center />,
getProps: async (di, props) => ({
...props,
releases: di.inject(releasesInjectable),
updateRelease: di.inject(updateReleaseInjectable),
upgradeChartTabStore: di.inject(upgradeChartTabStoreInjectable),
helmChartStore: di.inject(helmChartStoreInjectable),
model: await di.inject(upgradeChartModelInjectable, props.tab),
}),
});