Making apiBase injectable (#6022)

* Making apiBase injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all of Helm functions to be DI

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make PortForward's use of apiBase fully injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all metric requests to be injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace resource applier with injectables

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch KubeJsonApi.forCluster to be injectable but do not use

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert the rest of shell sessions to be DI-ed

- This is a prerequesit for using the new
  createKubeJsonApiForClusterInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Use new createKubeJsonApiForClusterInjectable for openNodeShellSession

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make KubeconfigDialog injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove jest-fetch-mock and make fetch injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests with new global override

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add new injectable for create KubeJsonApi and JsonApi instances

Signed-off-by: Sebastian Malton <sebastian@malton.name>

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

* Fix show details and updating helm releases tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix residual typing issues related to metrics

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix crash on load due to circular dependency

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix create resource tab not working

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove legacy apiBase global

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Introduce and use isDebuggingInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Introduce and use windowLocationInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove global legacy apiKube

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve injectable filenames compared to the injectables inside

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove modifying input in requestActivePortForwardInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Introduce and use get(Milli)SecondsFromUnixEpochInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch to non-reactive way of gettting possible helm release versions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix typo

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix bug in KubeApi constructor

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all KubeApi related tests to use asyncFn

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix unit tests after introducing new injectables that have side effects

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix bad rebase causing tests to fail

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve expects for multiple field values

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix crash will looking up api refs

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix breaking change on KubeApi.list

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Better fix for formatting urls

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove injectable for time since we should just use useMockTime

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add happy path behavioural tests for upgrade chart tab

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove debug message

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix showing-details-for-helm-release tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix installing-helm-chart-from-new-tab tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests relating to hosted cluster id

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots to recent changes in master

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Reupdated upgrade chart new tab test snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix flakiness in unit test when using <Animated>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix flakiness and improve tests for DeleteClusterDialog

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix kubeconfig-sync tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix <Extensions> tests by removing mockFs and making everything injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix build issues

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix getElectronAppPathInjectable override not returning absolute paths

- Also fixes the listing-active-helm-repos-in-prefs tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace all uses of getAbsolutePath with joinPaths as it is more correct and less confusing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix opening application window tests by making override properly absolute

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots relating no longer using getAbsolutePath

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix and add behavioural tests for RenderDelay

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix extension discovery tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test flakiness because of path side effects, propagate uses to as many places

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix extension-discovery tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add global override to fix some tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Rewrite and fix implementation of KubeconfigManager and its tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests by global override pathExists

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix unit tests failing on windows by using injectable verions of path functions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Attempt to fix test timeout by using runInAction

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots after rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots after rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests after rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix setupIpcMainHandlers usage

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
authored and GitHub committed 2022-10-05 08:10:36 -04:00
1 parent ed073d6562
commit 76066c5ebf
367 files changed
+16701 -5302

No files matched your search

@@ -3,49 +3,35 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import path from "path";
import { hasCorrectExtension } from "./has-correct-extension";
import readFileInjectable from "../../../../common/fs/read-file.injectable";
import readDirInjectable from "../../../../common/fs/read-dir.injectable";
import readDirectoryInjectable from "../../../../common/fs/read-directory.injectable";
import type { RawTemplates } from "./create-resource-templates.injectable";
import type { GetAbsolutePath } from "../../../../common/path/get-absolute-path.injectable";
import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable";
import staticFilesDirectoryInjectable from "../../../../common/vars/static-files-directory.injectable";
interface Dependencies {
readDir: (dirPath: string) => Promise<string[]>;
readFile: (filePath: string) => Promise<string>;
getAbsolutePath: GetAbsolutePath;
staticFilesDirectory: string;
}
async function getTemplates({ readDir, readFile, getAbsolutePath, staticFilesDirectory }: Dependencies) {
const templatesFolder = getAbsolutePath(staticFilesDirectory, "../templates/create-resource");
/**
* Mapping between file names and their contents
*/
const templates: [file: string, contents: string][] = [];
for (const dirEntry of await readDir(templatesFolder)) {
if (hasCorrectExtension(dirEntry)) {
templates.push([path.parse(dirEntry).name, await readFile(path.join(templatesFolder, dirEntry))]);
}
}
return templates;
}
import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
import parsePathInjectable from "../../../../common/path/parse.injectable";
const lensCreateResourceTemplatesInjectable = getInjectable({
id: "lens-create-resource-templates",
instantiate: async (di): Promise<RawTemplates> => {
const templates = await getTemplates({
readFile: di.inject(readFileInjectable),
readDir: di.inject(readDirInjectable),
getAbsolutePath: di.inject(getAbsolutePathInjectable),
staticFilesDirectory: di.inject(staticFilesDirectoryInjectable),
});
const readFile = di.inject(readFileInjectable);
const readDir = di.inject(readDirectoryInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
const parsePath = di.inject(parsePathInjectable);
/**
* Mapping between file names and their contents
*/
const templates: [file: string, contents: string][] = [];
const templatesFolder = joinPaths(staticFilesDirectory, "../templates/create-resource");
for (const dirEntry of await readDir(templatesFolder)) {
if (hasCorrectExtension(dirEntry)) {
templates.push([parsePath(dirEntry).name, await readFile(joinPaths(templatesFolder, dirEntry))]);
}
}
return ["lens", templates];
},
@@ -3,101 +3,108 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import { computed, observable } from "mobx";
import path from "path";
import os from "os";
import { delay, getOrInsert, isErrnoException, waitForPath } from "../../../utils";
import { watch } from "chokidar";
import { readFile } from "fs/promises";
import logger from "../../../../common/logger";
import { hasCorrectExtension } from "./has-correct-extension";
import type { RawTemplates } from "./create-resource-templates.injectable";
const userTemplatesFolder = path.join(os.homedir(), ".k8slens", "templates");
function groupTemplates(templates: Map<string, string>): RawTemplates[] {
const res = new Map<string, [string, string][]>();
for (const [filePath, contents] of templates) {
const rawRelative = path.dirname(path.relative(userTemplatesFolder, filePath));
const title = rawRelative === "."
? "ungrouped"
: rawRelative;
getOrInsert(res, title, []).push([path.parse(filePath).name, contents]);
}
return [...res.entries()];
}
function watchUserCreateResourceTemplates(): IComputedValue<RawTemplates[]> {
/**
* Map between filePaths and template contents
*/
const templates = observable.map<string, string>();
const onAddOrChange = async (filePath: string) => {
if (!hasCorrectExtension(filePath)) {
// ignore non yaml or json files
return;
}
try {
const contents = await readFile(filePath, "utf-8");
templates.set(filePath, contents);
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT") {
// ignore, file disappeared
} else {
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while reading ${filePath}`, error);
}
}
};
const onUnlink = (filePath: string) => {
templates.delete(filePath);
};
(async () => {
for (let i = 1;; i *= 2) {
try {
await waitForPath(userTemplatesFolder);
break;
} catch (error) {
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while waiting for ${userTemplatesFolder} to exist, waiting and trying again`, error);
await delay(i * 1000); // exponential backoff in seconds
}
}
/**
* NOTE: There is technically a race condition here of the form "time-of-check to time-of-use"
*/
watch(userTemplatesFolder, {
disableGlobbing: true,
ignorePermissionErrors: true,
usePolling: false,
awaitWriteFinish: {
pollInterval: 100,
stabilityThreshold: 1000,
},
ignoreInitial: false,
atomic: 150, // for "atomic writes"
})
.on("add", onAddOrChange)
.on("change", onAddOrChange)
.on("unlink", onUnlink)
.on("error", error => {
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while watching files under ${userTemplatesFolder}`, error);
});
})();
return computed(() => groupTemplates(templates));
}
import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
import watchInjectable from "../../../../common/fs/watch/watch.injectable";
import getRelativePathInjectable from "../../../../common/path/get-relative-path.injectable";
import homeDirectoryPathInjectable from "../../../../common/os/home-directory-path.injectable";
import getDirnameOfPathInjectable from "../../../../common/path/get-dirname.injectable";
import loggerInjectable from "../../../../common/logger.injectable";
import parsePathInjectable from "../../../../common/path/parse.injectable";
const userCreateResourceTemplatesInjectable = getInjectable({
id: "user-create-resource-templates",
instantiate: () => watchUserCreateResourceTemplates(),
instantiate: (di) => {
const joinPaths = di.inject(joinPathsInjectable);
const watch = di.inject(watchInjectable);
const getRelativePath = di.inject(getRelativePathInjectable);
const homeDirectoryPath = di.inject(homeDirectoryPathInjectable);
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
const logger = di.inject(loggerInjectable);
const parsePath = di.inject(parsePathInjectable);
const userTemplatesFolder = joinPaths(homeDirectoryPath, ".k8slens", "templates");
const groupTemplates = (templates: Map<string, string>): RawTemplates[] => {
const res = new Map<string, [string, string][]>();
for (const [filePath, contents] of templates) {
const rawRelative = getDirnameOfPath(getRelativePath(userTemplatesFolder, filePath));
const title = rawRelative === "."
? "ungrouped"
: rawRelative;
getOrInsert(res, title, []).push([parsePath(filePath).name, contents]);
}
return [...res.entries()];
};
/**
* Map between filePaths and template contents
*/
const templates = observable.map<string, string>();
const onAddOrChange = async (filePath: string) => {
if (!hasCorrectExtension(filePath)) {
// ignore non yaml or json files
return;
}
try {
const contents = await readFile(filePath, "utf-8");
templates.set(filePath, contents);
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT") {
// ignore, file disappeared
} else {
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while reading ${filePath}`, error);
}
}
};
const onUnlink = (filePath: string) => {
templates.delete(filePath);
};
(async () => {
for (let i = 1;; i *= 2) {
try {
await waitForPath(userTemplatesFolder);
break;
} catch (error) {
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while waiting for ${userTemplatesFolder} to exist, waiting and trying again`, error);
await delay(i * 1000); // exponential backoff in seconds
}
}
/**
* NOTE: There is technically a race condition here of the form "time-of-check to time-of-use"
*/
watch(userTemplatesFolder, {
disableGlobbing: true,
ignorePermissionErrors: true,
usePolling: false,
awaitWriteFinish: {
pollInterval: 100,
stabilityThreshold: 1000,
},
ignoreInitial: false,
atomic: 150, // for "atomic writes"
})
.on("add", onAddOrChange)
.on("change", onAddOrChange)
.on("unlink", onUnlink)
.on("error", error => {
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while watching files under ${userTemplatesFolder}`, error);
});
})();
return computed(() => groupTemplates(templates));
},
});
export default userCreateResourceTemplatesInjectable;
@@ -6,14 +6,13 @@
import React from "react";
import type { SelectOption } from "../../select";
import { Select } from "../../select";
import yaml from "js-yaml";
import yaml, { dump } from "js-yaml";
import type { IComputedValue } from "mobx";
import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import type { CreateResourceTabStore } from "./store";
import { EditorPanel } from "../editor-panel";
import { InfoPanel } from "../info-panel";
import * as resourceApplierApi from "../../../../common/k8s-api/endpoints/resource-applier.api";
import { Notifications } from "../../notifications";
import logger from "../../../../common/logger";
import type { ApiManager } from "../../../../common/k8s-api/api-manager";
@@ -28,6 +27,8 @@ import type { GetDetailsUrl } from "../../kube-detail-params/get-details-url.inj
import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
import getDetailsUrlInjectable from "../../kube-detail-params/get-details-url.injectable";
import navigateInjectable from "../../../navigation/navigate.injectable";
import type { RequestKubeObjectCreation } from "../../../../common/k8s-api/endpoints/resource-applier.api/request-update.injectable";
import requestKubeObjectCreationInjectable from "../../../../common/k8s-api/endpoints/resource-applier.api/request-update.injectable";
export interface CreateResourceProps {
tabId: string;
@@ -39,6 +40,7 @@ interface Dependencies {
apiManager: ApiManager;
navigate: Navigate;
getDetailsUrl: GetDetailsUrl;
requestKubeObjectCreation: RequestKubeObjectCreation;
}
@observer
@@ -68,7 +70,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
};
create = async (): Promise<void> => {
const { apiManager, getDetailsUrl, navigate } = this.props;
const { apiManager, getDetailsUrl, navigate, requestKubeObjectCreation } = this.props;
if (this.error || !this.data?.trim()) {
// do not save when field is empty or there is an error
@@ -84,7 +86,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
const creatingResources = resources.map(async (resource) => {
try {
const data = await resourceApplierApi.update(resource);
const data = await requestKubeObjectCreation(dump(resource));
const { kind, apiVersion, metadata: { name, namespace }} = data;
const showDetails = () => {
@@ -168,5 +170,6 @@ export const CreateResource = withInjectables<Dependencies, CreateResourceProps>
apiManager: di.inject(apiManagerInjectable),
getDetailsUrl: di.inject(getDetailsUrlInjectable),
navigate: di.inject(navigateInjectable),
requestKubeObjectCreation: di.inject(requestKubeObjectCreationInjectable),
}),
});
@@ -103,6 +103,7 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
material="close"
tooltip={`Close ${this.props.isMac ? "⌘+W" : "Ctrl+W"}`}
onClick={close}
data-testid={`dock-tab-close-for-${id}`}
/>
</div>
)}
@@ -1,20 +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 { 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,
causesSideEffects: true,
});
export default callForHelmChartValuesInjectable;
@@ -5,36 +5,31 @@
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 { 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 { RequestCreateHelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
import requestCreateHelmReleaseInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
import 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";
import type { RequestHelmChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
import requestHelmChartValuesInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
import type { RequestHelmChartVersions } from "../../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import requestHelmChartVersionsInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
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 requestHelmChartValues = di.inject(requestHelmChartValuesInjectable);
const requestHelmChartVersions = di.inject(requestHelmChartVersionsInjectable);
const callForCreateHelmRelease = di.inject(requestCreateHelmReleaseInjectable);
const dockStore = di.inject(dockStoreInjectable);
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
const closeTab = () => dockStore.closeTab(tabId);
@@ -49,8 +44,8 @@ const installChartModelInjectable = getInjectable({
callForCreateHelmRelease,
closeTab,
navigateToHelmReleases,
callForHelmChartValues,
callForHelmChartVersions,
requestHelmChartValues,
requestHelmChartVersions,
store,
});
@@ -71,9 +66,9 @@ interface Dependencies {
closeTab: () => void;
navigateToHelmReleases: NavigateToHelmReleases;
waitForChart: () => Promise<void>;
callForCreateHelmRelease: CallForCreateHelmRelease;
callForHelmChartValues: CallForHelmChartValues;
callForHelmChartVersions: CallForHelmChartVersions;
callForCreateHelmRelease: RequestCreateHelmRelease;
requestHelmChartValues: RequestHelmChartValues;
requestHelmChartVersions: RequestHelmChartVersions;
store: InstallChartTabStore;
}
@@ -127,7 +122,7 @@ export class InstallChartModel {
this.configuration.isLoading.set(true);
});
const configuration = await this.dependencies.callForHelmChartValues(
const configuration = await this.dependencies.requestHelmChartValues(
this.chart.repo,
this.chart.name,
version,
@@ -193,13 +188,13 @@ export class InstallChartModel {
await this.dependencies.waitForChart();
const [defaultConfiguration, versions] = await Promise.all([
this.dependencies.callForHelmChartValues(
this.dependencies.requestHelmChartValues(
this.chart.repo,
this.chart.name,
this.chart.version,
),
this.dependencies.callForHelmChartVersions(
this.dependencies.requestHelmChartVersions(
this.chart.repo,
this.chart.name,
),
@@ -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();
@@ -3,9 +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";
export interface IChartUpgradeData {
releaseName: string;
releaseNamespace: string;
}
const upgradeChartTabStoreInjectable = getInjectable({
id: "upgrade-chart-tab-store",
@@ -13,9 +16,8 @@ const upgradeChartTabStoreInjectable = getInjectable({
instantiate: (di) => {
const createDockTabStore = di.inject(createDockTabStoreInjectable);
return new UpgradeChartTabStore({
createStorage: di.inject(createStorageInjectable),
valuesStore: createDockTabStore<string>(),
return createDockTabStore<IChartUpgradeData>({
storageKey: "chart_releases",
});
},
});
@@ -1,55 +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 { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import assert from "assert";
export interface IChartUpgradeData {
releaseName: string;
releaseNamespace: string;
}
export interface UpgradeChartTabStoreDependencies extends DockTabStoreDependencies {
valuesStore: DockTabStore<string>;
}
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 getReleaseValues(releaseName, releaseNamespace, true);
this.values.setData(tabId, values);
}
getTabIdByRelease(releaseName: string) {
return this.releaseNameReverseLookup.get(releaseName);
}
}
@@ -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, lifecycleEnum } from "@ogre-tools/injectable";
import { waitUntilDefined } from "../../../utils";
import upgradeChartTabStoreInjectable from "./store.injectable";
const upgradeChartTabDataInjectable = getInjectable({
id: "upgrade-chart-tab-data",
instantiate: (di, tabId) => {
const upgradeChartTabStore = di.inject(upgradeChartTabStoreInjectable);
return waitUntilDefined(() => upgradeChartTabStore.getData(tabId));
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, tabId: string) => tabId,
}),
});
export default upgradeChartTabDataInjectable;
@@ -0,0 +1,129 @@
/**
* 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 { HelmChartVersion } from "../../+helm-charts/helm-charts/versions";
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/request-configuration.injectable";
import { waitUntilDefined } from "../../../utils";
import type { SelectOption } from "../../select";
import type { DockTab } from "../dock/store";
import upgradeChartTabDataInjectable from "./tab-data.injectable";
export interface UpgradeChartModel {
readonly release: HelmRelease;
readonly versionOptions: IComputedValue<SelectOption<HelmChartVersion>[]>;
readonly configration: {
readonly value: IComputedValue<string>;
set: (value: string) => void;
readonly error: IComputedValue<string | undefined>;
setError: (error: unknown) => void;
};
readonly version: {
readonly value: IComputedValue<HelmChartVersion | undefined>;
set: (value: SingleValue<SelectOption<HelmChartVersion>>) => void;
};
submit: () => Promise<UpgradeChartSubmitResult>;
}
export interface UpgradeChartSubmitResult {
completedSuccessfully: boolean;
}
const upgradeChartModelInjectable = getInjectable({
id: "upgrade-chart-model",
instantiate: async (di, tab): Promise<UpgradeChartModel> => {
const releases = di.inject(releasesInjectable);
const requestHelmReleaseConfiguration = di.inject(requestHelmReleaseConfigurationInjectable);
const updateRelease = di.inject(updateReleaseInjectable);
const tabData = await di.inject(upgradeChartTabDataInjectable, tab.id);
const release = await waitUntilDefined(() => (
releases.value
.get()
.find(release => (
release.getName() === tabData.releaseName
&& release.getNs() === tabData.releaseNamespace
))
));
const versions = di.inject(helmChartVersionsInjectable, release);
const storedConfiguration = 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() ?? storedConfiguration.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<HelmChartVersion>(undefined, {
deep: false,
});
const version: UpgradeChartModel["version"] = {
value: computed(() => versionValue.get() ?? versions.value.get()[0]),
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 selectedVersion = version.value.get();
if (!selectedVersion || configrationEditError.get()) {
return {
completedSuccessfully: false,
};
}
await updateRelease(
release.getName(),
release.getNs(),
{
chart: release.getChart(),
values: configration.value.get(),
...selectedVersion,
},
);
storedConfiguration.invalidate();
return {
completedSuccessfully: true,
};
},
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, tab: DockTab) => tab.id,
}),
});
export default upgradeChartModelInjectable;
@@ -6,26 +6,19 @@
import "./upgrade-chart.scss";
import React from "react";
import { action, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
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 { helmChartStore, type ChartVersion } from "../../+helm-charts/helm-chart.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 { CallForHelmReleaseUpdate } from "../../+helm-releases/update-release/call-for-helm-release-update/call-for-helm-release-update.injectable";
import { first } from "lodash/fp";
import type { UpgradeChartModel } from "./upgrade-chart-model.injectable";
import upgradeChartModelInjectable from "./upgrade-chart-model.injectable";
import type { HelmChartVersion } from "../../+helm-charts/helm-charts/versions";
export interface UpgradeChartProps {
className?: string;
@@ -33,159 +26,74 @@ export interface UpgradeChartProps {
}
interface Dependencies {
releases: IAsyncComputed<HelmRelease[]>;
upgradeChartTabStore: UpgradeChartTabStore;
updateRelease: CallForHelmReleaseUpdate;
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 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)}>
<div
className={cssNames("UpgradeChart flex column", className)}
data-testid={`upgrade-chart-dock-tab-contents-for-${release.getId()}`}
>
<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={release.getVersion()} />
<span>Upgrade version</span>
<Select<HelmChartVersion, SelectOption<HelmChartVersion>, 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>
);
@@ -193,10 +101,9 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
}
export const UpgradeChart = withInjectables<Dependencies, UpgradeChartProps>(NonInjectedUpgradeChart, {
getProps: (di, props) => ({
releases: di.inject(releasesInjectable),
updateRelease: di.inject(updateReleaseInjectable),
upgradeChartTabStore: di.inject(upgradeChartTabStoreInjectable),
getPlaceholder: () => <Spinner center />,
getProps: async (di, props) => ({
...props,
model: await di.inject(upgradeChartModelInjectable, props.tab),
}),
});