From 4f5a2988cba452c4b1f30abc317d8576b294f4b4 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Fri, 21 Jan 2022 16:29:10 +0200 Subject: [PATCH] Fix infinite render loop in release details (#4710) * Fix infinite render loop in release details by replacing stateful, UI-triggered releaseStore with reactive async computed Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Update injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove unnecessary return Signed-off-by: Janne Savolainen * Remove empty lines Signed-off-by: Janne Savolainen * Allow injection of history Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make data required for opening of release details a dependency to make sure it's present Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove dead code Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make arriving release values not re-render whole details Signed-off-by: Janne Savolainen --- package.json | 4 +- .../k8s-api/endpoints/helm-releases.api.ts | 53 +-- .../create-release.injectable.ts | 28 ++ .../delete-release.injectable.ts | 26 ++ .../+apps-releases/release-details.tsx | 321 ------------------ .../release-details.injectable.ts | 21 ++ .../release-details.scss | 2 +- .../release-details/release-details.tsx | 280 +++++++++++++++ .../release-route-parameters.injectable.ts | 31 ++ .../release-values.injectable.ts | 30 ++ .../release-details/release.injectable.ts | 30 ++ ...er-supplied-values-are-shown.injectable.ts | 27 ++ .../+apps-releases/release-menu.tsx | 15 +- .../release-rollback-dialog.tsx | 6 +- .../release-store.injectable.ts | 17 - .../+apps-releases/release.store.ts | 145 -------- .../+apps-releases/releases.injectable.ts | 40 +++ .../components/+apps-releases/releases.tsx | 100 ++++-- .../removable-releases.injectable.ts | 22 ++ .../+apps-releases/removable-releases.ts | 48 +++ .../rollback-release.injectable.ts | 22 ++ .../update-release.injectable.ts | 13 + .../components/dock/install-chart.tsx | 4 +- .../upgrade-chart-store.injectable.ts | 4 +- .../upgrade-chart.store.ts | 25 +- .../components/dock/upgrade-chart.tsx | 27 +- src/renderer/components/drawer/drawer.tsx | 11 +- .../frames/cluster-frame/cluster-frame.tsx | 14 +- src/renderer/frames/root-frame/root-frame.tsx | 18 +- src/renderer/navigation/history.injectable.ts | 13 + src/renderer/navigation/history.ts | 6 + .../observable-history.injectable.ts | 15 + yarn.lock | 18 +- 33 files changed, 836 insertions(+), 600 deletions(-) create mode 100644 src/renderer/components/+apps-releases/create-release/create-release.injectable.ts create mode 100644 src/renderer/components/+apps-releases/delete-release/delete-release.injectable.ts delete mode 100644 src/renderer/components/+apps-releases/release-details.tsx create mode 100644 src/renderer/components/+apps-releases/release-details/release-details.injectable.ts rename src/renderer/components/+apps-releases/{ => release-details}/release-details.scss (97%) create mode 100644 src/renderer/components/+apps-releases/release-details/release-details.tsx create mode 100644 src/renderer/components/+apps-releases/release-details/release-route-parameters.injectable.ts create mode 100644 src/renderer/components/+apps-releases/release-details/release-values.injectable.ts create mode 100644 src/renderer/components/+apps-releases/release-details/release.injectable.ts create mode 100644 src/renderer/components/+apps-releases/release-details/user-supplied-values-are-shown.injectable.ts delete mode 100644 src/renderer/components/+apps-releases/release-store.injectable.ts delete mode 100644 src/renderer/components/+apps-releases/release.store.ts create mode 100644 src/renderer/components/+apps-releases/releases.injectable.ts create mode 100644 src/renderer/components/+apps-releases/removable-releases.injectable.ts create mode 100644 src/renderer/components/+apps-releases/removable-releases.ts create mode 100644 src/renderer/components/+apps-releases/rollback-release/rollback-release.injectable.ts create mode 100644 src/renderer/components/+apps-releases/update-release/update-release.injectable.ts create mode 100644 src/renderer/navigation/history.injectable.ts create mode 100644 src/renderer/navigation/observable-history.injectable.ts diff --git a/package.json b/package.json index 2667dea25b..6adc67e0f5 100644 --- a/package.json +++ b/package.json @@ -195,8 +195,8 @@ "@hapi/call": "^8.0.1", "@hapi/subtext": "^7.0.3", "@kubernetes/client-node": "^0.16.1", - "@ogre-tools/injectable": "3.1.1", - "@ogre-tools/injectable-react": "3.1.1", + "@ogre-tools/injectable": "3.2.0", + "@ogre-tools/injectable-react": "3.2.0", "@sentry/electron": "^2.5.4", "@sentry/integrations": "^6.15.0", "@types/circular-dependency-plugin": "5.0.4", diff --git a/src/common/k8s-api/endpoints/helm-releases.api.ts b/src/common/k8s-api/endpoints/helm-releases.api.ts index ce0e5eed8e..b0da1ff3cb 100644 --- a/src/common/k8s-api/endpoints/helm-releases.api.ts +++ b/src/common/k8s-api/endpoints/helm-releases.api.ts @@ -4,7 +4,7 @@ */ import yaml from "js-yaml"; -import { autoBind, formatDuration } from "../../utils"; +import { formatDuration } from "../../utils"; import capitalize from "lodash/capitalize"; import { apiBase } from "../index"; import { helmChartStore } from "../../../renderer/components/+apps-helm-charts/helm-chart.store"; @@ -80,9 +80,9 @@ interface EndpointQuery { const endpoint = buildURLPositional("/v2/releases/:namespace?/:name?/:route?"); export async function listReleases(namespace?: string): Promise { - const releases = await apiBase.get(endpoint({ namespace })); + const releases = await apiBase.get(endpoint({ namespace })); - return releases.map(HelmRelease.create); + return releases.map(toHelmRelease); } export async function getRelease(name: string, namespace: string): Promise { @@ -152,7 +152,7 @@ export async function rollbackRelease(name: string, namespace: string, revision: return apiBase.put(path, { data }); } -export interface HelmRelease { +interface HelmReleaseDto { appVersion: string; name: string; namespace: string; @@ -162,27 +162,30 @@ export interface HelmRelease { revision: string; } -export class HelmRelease implements ItemObject { - constructor(data: any) { - Object.assign(this, data); - autoBind(this); - } +export interface HelmRelease extends HelmReleaseDto, ItemObject { + getNs: () => string + getChart: (withVersion?: boolean) => string + getRevision: () => number + getStatus: () => string + getVersion: () => string + getUpdated: (humanize?: boolean, compact?: boolean) => string | number + getRepo: () => Promise +} - static create(data: any) { - return new HelmRelease(data); - } +const toHelmRelease = (release: HelmReleaseDto) : HelmRelease => ({ + ...release, getId() { return this.namespace + this.name; - } + }, getName() { return this.name; - } + }, getNs() { return this.namespace; - } + }, getChart(withVersion = false) { let chart = this.chart; @@ -194,24 +197,24 @@ export class HelmRelease implements ItemObject { } return chart; - } + }, getRevision() { return parseInt(this.revision, 10); - } + }, getStatus() { return capitalize(this.status); - } + }, getVersion() { const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/); return versions?.[0] ?? ""; - } + }, getUpdated(humanize = true, compact = true) { - const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date() + const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date() const updatedDate = new Date(updated).getTime(); const diff = Date.now() - updatedDate; @@ -220,7 +223,7 @@ export class HelmRelease implements ItemObject { } return diff; - } + }, // Helm does not store from what repository the release is installed, // so we have to try to guess it by searching charts @@ -228,8 +231,10 @@ export class HelmRelease implements ItemObject { const chartName = this.getChart(); const version = this.getVersion(); const versions = await helmChartStore.getVersions(chartName); - const chartVersion = versions.find(chartVersion => chartVersion.version === version); + const chartVersion = versions.find( + (chartVersion) => chartVersion.version === version, + ); return chartVersion ? chartVersion.repo : ""; - } -} + }, +}); diff --git a/src/renderer/components/+apps-releases/create-release/create-release.injectable.ts b/src/renderer/components/+apps-releases/create-release/create-release.injectable.ts new file mode 100644 index 0000000000..17c31ff6c2 --- /dev/null +++ b/src/renderer/components/+apps-releases/create-release/create-release.injectable.ts @@ -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 { + createRelease, + IReleaseCreatePayload, +} from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import releasesInjectable from "../releases.injectable"; + +const createReleaseInjectable = getInjectable({ + instantiate: (di) => { + const releases = di.inject(releasesInjectable); + + return async (payload: IReleaseCreatePayload) => { + const release = await createRelease(payload); + + releases.invalidate(); + + return release; + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createReleaseInjectable; diff --git a/src/renderer/components/+apps-releases/delete-release/delete-release.injectable.ts b/src/renderer/components/+apps-releases/delete-release/delete-release.injectable.ts new file mode 100644 index 0000000000..df2e4fa1e9 --- /dev/null +++ b/src/renderer/components/+apps-releases/delete-release/delete-release.injectable.ts @@ -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, lifecycleEnum } from "@ogre-tools/injectable"; +import { + deleteRelease, + HelmRelease, +} from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import releasesInjectable from "../releases.injectable"; + +const deleteReleaseInjectable = getInjectable({ + instantiate: (di) => { + const releases = di.inject(releasesInjectable); + + return async (release: HelmRelease) => { + await deleteRelease(release.getName(), release.getNs()); + + releases.invalidate(); + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default deleteReleaseInjectable; diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx deleted file mode 100644 index 7712ea4052..0000000000 --- a/src/renderer/components/+apps-releases/release-details.tsx +++ /dev/null @@ -1,321 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import "./release-details.scss"; - -import React, { Component } from "react"; -import groupBy from "lodash/groupBy"; -import isEqual from "lodash/isEqual"; -import { makeObservable, observable, reaction } from "mobx"; -import { Link } from "react-router-dom"; -import kebabCase from "lodash/kebabCase"; -import { getRelease, getReleaseValues, HelmRelease, IReleaseDetails } from "../../../common/k8s-api/endpoints/helm-releases.api"; -import { HelmReleaseMenu } from "./release-menu"; -import { Drawer, DrawerItem, DrawerTitle } from "../drawer"; -import { Badge } from "../badge"; -import { cssNames, stopPropagation } from "../../utils"; -import { disposeOnUnmount, observer } from "mobx-react"; -import { Spinner } from "../spinner"; -import { Table, TableCell, TableHead, TableRow } from "../table"; -import { Button } from "../button"; -import type { ReleaseStore } from "./release.store"; -import { Notifications } from "../notifications"; -import { ThemeStore } from "../../theme.store"; -import { apiManager } from "../../../common/k8s-api/api-manager"; -import { SubTitle } from "../layout/sub-title"; -import { secretsStore } from "../+config-secrets/secrets.store"; -import { Secret } from "../../../common/k8s-api/endpoints"; -import { getDetailsUrl } from "../kube-detail-params"; -import { Checkbox } from "../checkbox"; -import { MonacoEditor } from "../monaco-editor"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import releaseStoreInjectable from "./release-store.injectable"; -import createUpgradeChartTabInjectable - from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; - -interface Props { - release: HelmRelease; - hideDetails(): void; -} - -interface Dependencies { - releaseStore: ReleaseStore - createUpgradeChartTab: (release: HelmRelease) => void -} - -@observer -class NonInjectedReleaseDetails extends Component { - @observable details: IReleaseDetails | null = null; - @observable values = ""; - @observable valuesLoading = false; - @observable showOnlyUserSuppliedValues = true; - @observable saving = false; - @observable releaseSecret: Secret; - @observable error?: string = undefined; - - componentDidMount() { - disposeOnUnmount(this, [ - reaction(() => this.props.release, release => { - if (!release) return; - this.loadDetails(); - this.loadValues(); - this.releaseSecret = null; - }), - reaction(() => secretsStore.getItems(), () => { - if (!this.props.release) return; - const { getReleaseSecret } = this.props.releaseStore; - const { release } = this.props; - const secret = getReleaseSecret(release); - - if (this.releaseSecret) { - if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return; - this.loadDetails(); - } - this.releaseSecret = secret; - }), - reaction(() => this.showOnlyUserSuppliedValues, () => { - this.loadValues(); - }), - ]); - } - - constructor(props: Props & Dependencies) { - super(props); - makeObservable(this); - } - - async loadDetails() { - const { release } = this.props; - - try { - this.details = null; - this.details = await getRelease(release.getName(), release.getNs()); - } catch (error) { - this.error = `Failed to get release details: ${error}`; - } - } - - async loadValues() { - const { release } = this.props; - - try { - this.valuesLoading = true; - this.values = (await getReleaseValues(release.getName(), release.getNs(), !this.showOnlyUserSuppliedValues)) ?? ""; - } catch (error) { - Notifications.error(`Failed to load values for ${release.getName()}: ${error}`); - this.values = ""; - } finally { - this.valuesLoading = false; - } - } - - updateValues = async () => { - const { release } = this.props; - const name = release.getName(); - const namespace = release.getNs(); - const data = { - chart: release.getChart(), - repo: await release.getRepo(), - version: release.getVersion(), - values: this.values, - }; - - this.saving = true; - - try { - await this.props.releaseStore.update(name, namespace, data); - Notifications.ok( -

Release {name} successfully updated!

, - ); - } catch (err) { - Notifications.error(err); - } - this.saving = false; - }; - - upgradeVersion = () => { - const { release, hideDetails } = this.props; - - this.props.createUpgradeChartTab(release); - hideDetails(); - }; - - renderValues() { - const { values, valuesLoading, saving } = this; - - return ( -
- -
- this.showOnlyUserSuppliedValues = value} - disabled={valuesLoading} - /> - this.values = text} - > - {valuesLoading && } - -
-
- ); - } - - renderNotes() { - if (!this.details.info?.notes) return null; - const { notes } = this.details.info; - - return ( -
- {notes} -
- ); - } - - renderResources() { - const { resources } = this.details; - - if (!resources) return null; - const groups = groupBy(resources, item => item.kind); - const tables = Object.entries(groups).map(([kind, items]) => { - return ( - - - - - Name - {items[0].getNs() && Namespace} - Age - - {items.map(item => { - const name = item.getName(); - const namespace = item.getNs(); - const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == item.apiVersion); - const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : ""; - - return ( - - - {detailsUrl ? {name} : name} - - {namespace && {namespace}} - {item.getAge()} - - ); - })} -
-
- ); - }); - - return ( -
- {tables} -
- ); - } - - renderContent() { - const { release } = this.props; - - if (!release) return null; - - if (this.error) { - return ( -
- {this.error} -
- ); - } - - if (!this.details) { - return ; - } - - return ( -
- -
- {release.getChart()} -
-
- - {release.getUpdated()} ago ({release.updated}) - - - {release.getNs()} - - -
- - {release.getVersion()} - -
-
- - - - {this.renderValues()} - - {this.renderNotes()} - - {this.renderResources()} -
- ); - } - - render() { - const { release, hideDetails } = this.props; - const title = release ? `Release: ${release.getName()}` : ""; - const toolbar = ; - - return ( - - {this.renderContent()} - - ); - } -} - -export const ReleaseDetails = withInjectables( - NonInjectedReleaseDetails, - - { - getProps: (di, props) => ({ - releaseStore: di.inject(releaseStoreInjectable), - createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable), - ...props, - }), - }, -); diff --git a/src/renderer/components/+apps-releases/release-details/release-details.injectable.ts b/src/renderer/components/+apps-releases/release-details/release-details.injectable.ts new file mode 100644 index 0000000000..1dac3b716f --- /dev/null +++ b/src/renderer/components/+apps-releases/release-details/release-details.injectable.ts @@ -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 { getRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import { asyncComputed } from "@ogre-tools/injectable-react"; +import releaseInjectable from "./release.injectable"; + +const releaseDetailsInjectable = getInjectable({ + instantiate: (di) => + asyncComputed(async () => { + const release = di.inject(releaseInjectable).value.get(); + + return await getRelease(release.name, release.namespace); + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default releaseDetailsInjectable; diff --git a/src/renderer/components/+apps-releases/release-details.scss b/src/renderer/components/+apps-releases/release-details/release-details.scss similarity index 97% rename from src/renderer/components/+apps-releases/release-details.scss rename to src/renderer/components/+apps-releases/release-details/release-details.scss index 20ce0a2cef..0a4acfcca3 100644 --- a/src/renderer/components/+apps-releases/release-details.scss +++ b/src/renderer/components/+apps-releases/release-details/release-details.scss @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -@import "release.mixins"; +@import "../release.mixins"; .ReleaseDetails { .DrawerItem { diff --git a/src/renderer/components/+apps-releases/release-details/release-details.tsx b/src/renderer/components/+apps-releases/release-details/release-details.tsx new file mode 100644 index 0000000000..77f6ab2d87 --- /dev/null +++ b/src/renderer/components/+apps-releases/release-details/release-details.tsx @@ -0,0 +1,280 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./release-details.scss"; + +import React, { Component } from "react"; +import groupBy from "lodash/groupBy"; +import { computed, makeObservable, observable } from "mobx"; +import { Link } from "react-router-dom"; +import kebabCase from "lodash/kebabCase"; +import type { HelmRelease, IReleaseDetails, IReleaseUpdateDetails, IReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import { HelmReleaseMenu } from "../release-menu"; +import { Drawer, DrawerItem, DrawerTitle } from "../../drawer"; +import { Badge } from "../../badge"; +import { cssNames, stopPropagation } from "../../../utils"; +import { Observer, observer } from "mobx-react"; +import { Spinner } from "../../spinner"; +import { Table, TableCell, TableHead, TableRow } from "../../table"; +import { Button } from "../../button"; +import { Notifications } from "../../notifications"; +import { ThemeStore } from "../../../theme.store"; +import { apiManager } from "../../../../common/k8s-api/api-manager"; +import { SubTitle } from "../../layout/sub-title"; +import { getDetailsUrl } from "../../kube-detail-params"; +import { Checkbox } from "../../checkbox"; +import { MonacoEditor } from "../../monaco-editor"; +import { IAsyncComputed, withInjectables } from "@ogre-tools/injectable-react"; +import createUpgradeChartTabInjectable from "../../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; +import updateReleaseInjectable from "../update-release/update-release.injectable"; +import releaseInjectable from "./release.injectable"; +import releaseDetailsInjectable from "./release-details.injectable"; +import releaseValuesInjectable from "./release-values.injectable"; +import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable"; + +interface Props { + hideDetails(): void; +} + +interface Dependencies { + release: IAsyncComputed + releaseDetails: IAsyncComputed + releaseValues: IAsyncComputed + updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise + createUpgradeChartTab: (release: HelmRelease) => void + userSuppliedValuesAreShown: { toggle: () => void, value: boolean } +} + +@observer +class NonInjectedReleaseDetails extends Component { + @observable saving = false; + + private nonSavedValues: string; + + constructor(props: Props & Dependencies) { + super(props); + makeObservable(this); + } + + @computed get release() { + return this.props.release.value.get(); + } + + @computed get details() { + return this.props.releaseDetails.value.get(); + } + + updateValues = async () => { + const name = this.release.getName(); + const namespace = this.release.getNs(); + const data = { + chart: this.release.getChart(), + repo: await this.release.getRepo(), + version: this.release.getVersion(), + values: this.nonSavedValues, + }; + + this.saving = true; + + try { + await this.props.updateRelease(name, namespace, data); + Notifications.ok( +

Release {name} successfully updated!

, + ); + + this.props.releaseValues.invalidate(); + } catch (err) { + Notifications.error(err); + } + this.saving = false; + }; + + upgradeVersion = () => { + const { hideDetails } = this.props; + + this.props.createUpgradeChartTab(this.release); + hideDetails(); + }; + + renderValues() { + return ( + + {() => { + const { saving } = this; + + const releaseValuesArePending = + this.props.releaseValues.pending.get(); + + this.nonSavedValues = this.props.releaseValues.value.get(); + + return ( +
+ +
+ + (this.nonSavedValues = text)} + /> +
+
+ ); + }} +
+ ); + } + + renderNotes() { + if (!this.details.info?.notes) return null; + const { notes } = this.details.info; + + return ( +
+ {notes} +
+ ); + } + + renderResources() { + const { resources } = this.details; + + if (!resources) return null; + const groups = groupBy(resources, item => item.kind); + const tables = Object.entries(groups).map(([kind, items]) => { + return ( + + + + + Name + {items[0].getNs() && Namespace} + Age + + {items.map(item => { + const name = item.getName(); + const namespace = item.getNs(); + const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == item.apiVersion); + const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : ""; + + return ( + + + {detailsUrl ? {name} : name} + + {namespace && {namespace}} + {item.getAge()} + + ); + })} +
+
+ ); + }); + + return ( +
+ {tables} +
+ ); + } + + renderContent() { + if (!this.release) return null; + + if (!this.details) { + return ; + } + + return ( +
+ +
+ {this.release.getChart()} +
+
+ + {this.release.getUpdated()} ago ({this.release.updated}) + + + {this.release.getNs()} + + +
+ + {this.release.getVersion()} + +
+
+ + + + {this.renderValues()} + + {this.renderNotes()} + + {this.renderResources()} +
+ ); + } + + render() { + const { hideDetails } = this.props; + const title = this.release ? `Release: ${this.release.getName()}` : ""; + const toolbar = ; + + return ( + + {this.renderContent()} + + ); + } +} + +export const ReleaseDetails = withInjectables( + NonInjectedReleaseDetails, + + { + getProps: (di, props) => ({ + release: di.inject(releaseInjectable), + releaseDetails: di.inject(releaseDetailsInjectable), + releaseValues: di.inject(releaseValuesInjectable), + + userSuppliedValuesAreShown: di.inject(userSuppliedValuesAreShownInjectable), + + updateRelease: di.inject(updateReleaseInjectable), + createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/+apps-releases/release-details/release-route-parameters.injectable.ts b/src/renderer/components/+apps-releases/release-details/release-route-parameters.injectable.ts new file mode 100644 index 0000000000..d179c539be --- /dev/null +++ b/src/renderer/components/+apps-releases/release-details/release-route-parameters.injectable.ts @@ -0,0 +1,31 @@ +/** + * 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 { computed } from "mobx"; +import { matchPath } from "react-router"; +import observableHistoryInjectable from "../../../navigation/observable-history.injectable"; +import { releaseRoute, ReleaseRouteParams } from "../../../../common/routes"; + +const releaseRouteParametersInjectable = getInjectable({ + instantiate: (di) => { + const observableHistory = di.inject(observableHistoryInjectable); + + return computed(() => { + const releasePathParameters = matchPath(observableHistory.location.pathname, { + path: releaseRoute.path, + }); + + if (!releasePathParameters) { + return {}; + } + + return releasePathParameters.params; + }); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default releaseRouteParametersInjectable; diff --git a/src/renderer/components/+apps-releases/release-details/release-values.injectable.ts b/src/renderer/components/+apps-releases/release-details/release-values.injectable.ts new file mode 100644 index 0000000000..d7cc2c965c --- /dev/null +++ b/src/renderer/components/+apps-releases/release-details/release-values.injectable.ts @@ -0,0 +1,30 @@ +/** + * 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 { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import { asyncComputed } from "@ogre-tools/injectable-react"; +import releaseInjectable from "./release.injectable"; +import { Notifications } from "../../notifications"; +import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable"; + +const releaseValuesInjectable = getInjectable({ + instantiate: (di) => + asyncComputed(async () => { + const release = di.inject(releaseInjectable).value.get(); + const userSuppliedValuesAreShown = di.inject(userSuppliedValuesAreShownInjectable).value; + + try { + return await getReleaseValues(release.getName(), release.getNs(), !userSuppliedValuesAreShown) ?? ""; + } catch (error) { + Notifications.error(`Failed to load values for ${release.getName()}: ${error}`); + + return ""; + } + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default releaseValuesInjectable; diff --git a/src/renderer/components/+apps-releases/release-details/release.injectable.ts b/src/renderer/components/+apps-releases/release-details/release.injectable.ts new file mode 100644 index 0000000000..6da2295765 --- /dev/null +++ b/src/renderer/components/+apps-releases/release-details/release.injectable.ts @@ -0,0 +1,30 @@ +/** + * 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 { matches } from "lodash/fp"; +import releasesInjectable from "../releases.injectable"; +import releaseRouteParametersInjectable from "./release-route-parameters.injectable"; +import { asyncComputed } from "@ogre-tools/injectable-react"; + +const releaseInjectable = getInjectable({ + instantiate: (di) => { + const releases = di.inject(releasesInjectable); + const releaseRouteParameters = di.inject(releaseRouteParametersInjectable); + + return asyncComputed(async () => { + const { name, namespace } = releaseRouteParameters.get(); + + if (!name || !namespace) { + return null; + } + + return releases.value.get().find(matches({ name, namespace })); + }); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default releaseInjectable; diff --git a/src/renderer/components/+apps-releases/release-details/user-supplied-values-are-shown.injectable.ts b/src/renderer/components/+apps-releases/release-details/user-supplied-values-are-shown.injectable.ts new file mode 100644 index 0000000000..374f982882 --- /dev/null +++ b/src/renderer/components/+apps-releases/release-details/user-supplied-values-are-shown.injectable.ts @@ -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, lifecycleEnum } from "@ogre-tools/injectable"; +import { observable } from "mobx"; + +const userSuppliedValuesAreShownInjectable = getInjectable({ + instantiate: () => { + const state = observable.box(false); + + return { + get value() { + return state.get(); + }, + + toggle: () => { + state.set(!state.get()); + }, + }; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default userSuppliedValuesAreShownInjectable; + diff --git a/src/renderer/components/+apps-releases/release-menu.tsx b/src/renderer/components/+apps-releases/release-menu.tsx index a4d4587e86..a1dc263806 100644 --- a/src/renderer/components/+apps-releases/release-menu.tsx +++ b/src/renderer/components/+apps-releases/release-menu.tsx @@ -6,16 +6,13 @@ import React from "react"; import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; import { cssNames } from "../../utils"; -import type { ReleaseStore } from "./release.store"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; import { withInjectables } from "@ogre-tools/injectable-react"; -import releaseStoreInjectable from "./release-store.injectable"; -import createUpgradeChartTabInjectable - from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; -import releaseRollbackDialogModelInjectable - from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable"; +import createUpgradeChartTabInjectable from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; +import releaseRollbackDialogModelInjectable from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable"; +import deleteReleaseInjectable from "./delete-release/delete-release.injectable"; interface Props extends MenuActionsProps { release: HelmRelease; @@ -23,14 +20,14 @@ interface Props extends MenuActionsProps { } interface Dependencies { - releaseStore: ReleaseStore + deleteRelease: (release: HelmRelease) => Promise createUpgradeChartTab: (release: HelmRelease) => void openRollbackDialog: (release: HelmRelease) => void } class NonInjectedHelmReleaseMenu extends React.Component { remove = () => { - return this.props.releaseStore.remove(this.props.release); + return this.props.deleteRelease(this.props.release); }; upgrade = () => { @@ -87,7 +84,7 @@ export const HelmReleaseMenu = withInjectables( { getProps: (di, props) => ({ - releaseStore: di.inject(releaseStoreInjectable), + deleteRelease: di.inject(deleteReleaseInjectable), createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable), openRollbackDialog: di.inject(releaseRollbackDialogModelInjectable).open, diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx index d8f731f902..03f789475d 100644 --- a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx +++ b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx @@ -15,16 +15,16 @@ import { Select, SelectOption } from "../select"; import { Notifications } from "../notifications"; import orderBy from "lodash/orderBy"; import { withInjectables } from "@ogre-tools/injectable-react"; -import releaseStoreInjectable from "./release-store.injectable"; import releaseRollbackDialogModelInjectable from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable"; import type { ReleaseRollbackDialogModel } from "./release-rollback-dialog-model/release-rollback-dialog-model"; +import rollbackReleaseInjectable from "./rollback-release/rollback-release.injectable"; interface Props extends DialogProps { } interface Dependencies { - rollbackRelease: (releaseName: string, namespace: string, revisionNumber: number) => Promise + rollbackRelease: (releaseName: string, namespace: string, revisionNumber: number) => Promise model: ReleaseRollbackDialogModel } @@ -119,7 +119,7 @@ export const ReleaseRollbackDialog = withInjectables( { getProps: (di, props) => ({ - rollbackRelease: di.inject(releaseStoreInjectable).rollback, + rollbackRelease: di.inject(rollbackReleaseInjectable), model: di.inject(releaseRollbackDialogModelInjectable), ...props, }), diff --git a/src/renderer/components/+apps-releases/release-store.injectable.ts b/src/renderer/components/+apps-releases/release-store.injectable.ts deleted file mode 100644 index 3404201071..0000000000 --- a/src/renderer/components/+apps-releases/release-store.injectable.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 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 { ReleaseStore } from "./release.store"; -import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; - -const releaseStoreInjectable = getInjectable({ - instantiate: (di) => new ReleaseStore({ - namespaceStore: di.inject(namespaceStoreInjectable), - }), - - lifecycle: lifecycleEnum.singleton, -}); - -export default releaseStoreInjectable; diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts deleted file mode 100644 index 3755a70080..0000000000 --- a/src/renderer/components/+apps-releases/release.store.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import isEqual from "lodash/isEqual"; -import { action, observable, reaction, when, makeObservable } from "mobx"; -import { autoBind } from "../../utils"; -import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; -import { ItemStore } from "../../../common/item.store"; -import type { Secret } from "../../../common/k8s-api/endpoints"; -import { secretsStore } from "../+config-secrets/secrets.store"; -import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; -import { Notifications } from "../notifications"; - -interface Dependencies { - namespaceStore: NamespaceStore -} - -export class ReleaseStore extends ItemStore { - releaseSecrets = observable.map(); - - constructor(private dependencies: Dependencies ) { - super(); - makeObservable(this); - autoBind(this); - - when(() => secretsStore.isLoaded, () => { - this.releaseSecrets.replace(this.getReleaseSecrets()); - }); - } - - watchAssociatedSecrets(): (() => void) { - return reaction(() => secretsStore.getItems(), () => { - if (this.isLoading) return; - const newSecrets = this.getReleaseSecrets(); - const amountChanged = newSecrets.length !== this.releaseSecrets.size; - const labelsChanged = newSecrets.some(([id, secret]) => ( - !isEqual(secret.getLabels(), this.releaseSecrets.get(id)?.getLabels()) - )); - - if (amountChanged || labelsChanged) { - this.loadFromContextNamespaces(); - } - this.releaseSecrets.replace(newSecrets); - }, { - fireImmediately: true, - }); - } - - watchSelectedNamespaces(): (() => void) { - return reaction(() => this.dependencies.namespaceStore.context.contextNamespaces, namespaces => { - this.loadAll(namespaces); - }, { - fireImmediately: true, - }); - } - - private getReleaseSecrets() { - return secretsStore - .getByLabel({ owner: "helm" }) - .map(s => [s.getId(), s] as const); - } - - getReleaseSecret(release: HelmRelease) { - return secretsStore.getByLabel({ - owner: "helm", - name: release.getName(), - }) - .find(secret => secret.getNs() == release.getNs()); - } - - @action - async loadAll(namespaces: string[]) { - this.isLoading = true; - this.isLoaded = false; - - try { - const items = await this.loadItems(namespaces); - - this.items.replace(this.sortItems(items)); - this.isLoaded = true; - this.failedLoading = false; - } catch (error) { - this.failedLoading = true; - console.warn("Loading Helm Chart releases has failed", error); - - if (error.error) { - Notifications.error(error.error); - } - } finally { - this.isLoading = false; - } - } - - async loadFromContextNamespaces(): Promise { - return this.loadAll(this.dependencies.namespaceStore.context.contextNamespaces); - } - - async loadItems(namespaces: string[]) { - const isLoadingAll = this.dependencies.namespaceStore.context.allNamespaces?.length > 1 - && this.dependencies.namespaceStore.context.cluster.accessibleNamespaces.length === 0 - && this.dependencies.namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns)); - - if (isLoadingAll) { - return listReleases(); - } - - return Promise // load resources per namespace - .all(namespaces.map(namespace => listReleases(namespace))) - .then(items => items.flat()); - } - - create = async (payload: IReleaseCreatePayload) => { - const response = await createRelease(payload); - - if (this.isLoaded) this.loadFromContextNamespaces(); - - return response; - }; - - async update(name: string, namespace: string, payload: IReleaseUpdatePayload) { - const response = await updateRelease(name, namespace, payload); - - if (this.isLoaded) this.loadFromContextNamespaces(); - - return response; - } - - rollback = async (name: string, namespace: string, revision: number) => { - const response = await rollbackRelease(name, namespace, revision); - - if (this.isLoaded) this.loadFromContextNamespaces(); - - return response; - }; - - async remove(release: HelmRelease) { - return super.removeItem(release, () => deleteRelease(release.getName(), release.getNs())); - } - - async removeSelectedItems() { - return Promise.all(this.selectedItems.map(this.remove)); - } -} diff --git a/src/renderer/components/+apps-releases/releases.injectable.ts b/src/renderer/components/+apps-releases/releases.injectable.ts new file mode 100644 index 0000000000..9858fb1834 --- /dev/null +++ b/src/renderer/components/+apps-releases/releases.injectable.ts @@ -0,0 +1,40 @@ +/** + * 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 namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; +import { listReleases } from "../../../common/k8s-api/endpoints/helm-releases.api"; + +const releasesInjectable = getInjectable({ + instantiate: (di) => { + const namespaceStore = di.inject(namespaceStoreInjectable); + + // TODO: Inject clusterContext directly instead of accessing dependency of a dependency + const clusterContext = namespaceStore.context; + + return asyncComputed(async () => { + const contextNamespaces = namespaceStore.contextNamespaces || []; + + const isLoadingAll = + clusterContext.allNamespaces?.length > 1 && + clusterContext.cluster.accessibleNamespaces.length === 0 && + clusterContext.allNamespaces.every((namespace) => + contextNamespaces.includes(namespace), + ); + + const releaseArrays = await (isLoadingAll ? listReleases() : Promise.all( + contextNamespaces.map((namespace) => + listReleases(namespace), + ), + )); + + return releaseArrays.flat(); + }, []); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default releasesInjectable; diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx index 998e99e902..a71cc8e7a1 100644 --- a/src/renderer/components/+apps-releases/releases.tsx +++ b/src/renderer/components/+apps-releases/releases.tsx @@ -3,26 +3,30 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import "../item-object-list/item-list-layout.scss"; import "./releases.scss"; import React, { Component } from "react"; -import kebabCase from "lodash/kebabCase"; -import { disposeOnUnmount, observer } from "mobx-react"; import type { RouteComponentProps } from "react-router"; -import type { ReleaseStore } from "./release.store"; import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; -import { ReleaseDetails } from "./release-details"; -import { ReleaseRollbackDialog } from "./release-rollback-dialog"; import { navigation } from "../../navigation"; -import { ItemListLayout } from "../item-object-list/item-list-layout"; -import { HelmReleaseMenu } from "./release-menu"; -import { secretsStore } from "../+config-secrets/secrets.store"; -import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import type { ReleaseRouteParams } from "../../../common/routes"; import { releaseURL } from "../../../common/routes"; import { withInjectables } from "@ogre-tools/injectable-react"; -import releaseStoreInjectable from "./release-store.injectable"; import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; +import { ItemListLayout } from "../item-object-list"; +import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; +import { kebabCase } from "lodash/fp"; +import { HelmReleaseMenu } from "./release-menu"; +import type { ItemStore } from "../../../common/item.store"; +import { ReleaseRollbackDialog } from "./release-rollback-dialog"; +import { ReleaseDetails } from "./release-details/release-details"; +import removableReleasesInjectable from "./removable-releases.injectable"; +import type { RemovableHelmRelease } from "./removable-releases"; +import { observer } from "mobx-react"; +import type { IComputedValue } from "mobx"; +import releasesInjectable from "./releases.injectable"; +import { Spinner } from "../spinner"; enum columnId { name = "name", @@ -39,7 +43,8 @@ interface Props extends RouteComponentProps { } interface Dependencies { - releaseStore: ReleaseStore + releases: IComputedValue + releasesArePending: IComputedValue selectNamespace: (namespace: string) => void } @@ -51,27 +56,10 @@ class NonInjectedHelmReleases extends Component { if (namespace) { this.props.selectNamespace(namespace); } - - disposeOnUnmount(this, [ - this.props.releaseStore.watchAssociatedSecrets(), - this.props.releaseStore.watchSelectedNamespaces(), - ]); - } - - get selectedRelease() { - const { match: { params: { name, namespace }}} = this.props; - - return this.props.releaseStore.items.find(release => { - return release.getName() == name && release.getNs() == namespace; - }); } onDetails = (item: HelmRelease) => { - if (item === this.selectedRelease) { - this.hideDetails(); - } else { - this.showDetails(item); - } + this.showDetails(item); }; showDetails = (item: HelmRelease) => { @@ -101,14 +89,57 @@ class NonInjectedHelmReleases extends Component { } render() { + if (this.props.releasesArePending.get()) { + // TODO: Make Spinner "center" work properly + return
; + } + + const releases = this.props.releases; + + // TODO: Implement ItemListLayout without stateful stores + const legacyReleaseStore = { + get items() { + return releases.get(); + }, + + loadAll: () => Promise.resolve(), + isLoaded: true, + failedLoading: false, + + getTotalCount: () => releases.get().length, + + toggleSelection: (item) => { + item.toggle(); + }, + + isSelectedAll: () => + releases.get().every((release) => release.isSelected), + + toggleSelectionAll: () => { + releases.get().forEach((release) => release.toggle()); + }, + + isSelected: (item) => item.isSelected, + + get selectedItems() { + return releases.get().filter((release) => release.isSelected); + }, + + removeSelectedItems() { + return Promise.all( + releases.get().filter((release) => release.isSelected).map((release) => release.delete()), + ); + }, + } as ItemStore; + return ( <> release.getName(), [columnId.namespace]: release => release.getNs(), @@ -167,13 +198,13 @@ class NonInjectedHelmReleases extends Component { customizeRemoveDialog={selectedItems => ({ message: this.renderRemoveDialogMessage(selectedItems), })} - detailsItem={this.selectedRelease} onDetails={this.onDetails} /> + + ); @@ -185,7 +216,8 @@ export const HelmReleases = withInjectables( { getProps: (di, props) => ({ - releaseStore: di.inject(releaseStoreInjectable), + releases: di.inject(removableReleasesInjectable), + releasesArePending: di.inject(releasesInjectable).pending, selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces, ...props, }), diff --git a/src/renderer/components/+apps-releases/removable-releases.injectable.ts b/src/renderer/components/+apps-releases/removable-releases.injectable.ts new file mode 100644 index 0000000000..6d41c63768 --- /dev/null +++ b/src/renderer/components/+apps-releases/removable-releases.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { observable } from "mobx"; +import releasesInjectable from "./releases.injectable"; +import deleteReleaseInjectable from "./delete-release/delete-release.injectable"; +import { removableReleases } from "./removable-releases"; + +const removableReleasesInjectable = getInjectable({ + instantiate: (di) => + removableReleases({ + releases: di.inject(releasesInjectable), + deleteRelease: di.inject(deleteReleaseInjectable), + releaseSelectionStatus: observable.map(), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default removableReleasesInjectable; diff --git a/src/renderer/components/+apps-releases/removable-releases.ts b/src/renderer/components/+apps-releases/removable-releases.ts new file mode 100644 index 0000000000..5d72e0b5ea --- /dev/null +++ b/src/renderer/components/+apps-releases/removable-releases.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { IAsyncComputed } from "@ogre-tools/injectable-react"; +import { computed, ObservableMap } from "mobx"; +import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; + +interface Dependencies { + releases: IAsyncComputed; + releaseSelectionStatus: ObservableMap; + deleteRelease: (release: HelmRelease) => Promise; +} + +export interface RemovableHelmRelease extends HelmRelease { + toggle: () => void; + isSelected: boolean; + delete: () => Promise; +} + +export const removableReleases = ({ + releases, + releaseSelectionStatus, + deleteRelease, +}: Dependencies) => { + const isSelected = (release: HelmRelease) => + releaseSelectionStatus.get(release.getId()) || false; + + return computed(() => + releases.value.get().map( + (release): RemovableHelmRelease => ({ + ...release, + + toggle: () => { + releaseSelectionStatus.set(release.getId(), !isSelected(release)); + }, + + get isSelected() { + return isSelected(release); + }, + + delete: async () => { + await deleteRelease(release); + }, + }), + ), + ); +}; diff --git a/src/renderer/components/+apps-releases/rollback-release/rollback-release.injectable.ts b/src/renderer/components/+apps-releases/rollback-release/rollback-release.injectable.ts new file mode 100644 index 0000000000..fe04ce71e6 --- /dev/null +++ b/src/renderer/components/+apps-releases/rollback-release/rollback-release.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { rollbackRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import releasesInjectable from "../releases.injectable"; + +const rollbackReleaseInjectable = getInjectable({ + instantiate: (di) => { + const releases = di.inject(releasesInjectable); + + return async (name: string, namespace: string, revision: number) => { + await rollbackRelease(name, namespace, revision); + + releases.invalidate(); + }; + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default rollbackReleaseInjectable; diff --git a/src/renderer/components/+apps-releases/update-release/update-release.injectable.ts b/src/renderer/components/+apps-releases/update-release/update-release.injectable.ts new file mode 100644 index 0000000000..2e5a681f51 --- /dev/null +++ b/src/renderer/components/+apps-releases/update-release/update-release.injectable.ts @@ -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, lifecycleEnum } from "@ogre-tools/injectable"; +import { updateRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api"; + +const updateReleaseInjectable = getInjectable({ + instantiate: () => updateRelease, + lifecycle: lifecycleEnum.singleton, +}); + +export default updateReleaseInjectable; diff --git a/src/renderer/components/dock/install-chart.tsx b/src/renderer/components/dock/install-chart.tsx index 29cd13fe5d..d159a8df08 100644 --- a/src/renderer/components/dock/install-chart.tsx +++ b/src/renderer/components/dock/install-chart.tsx @@ -27,10 +27,10 @@ import type { IReleaseCreatePayload, IReleaseUpdateDetails, } from "../../../common/k8s-api/endpoints/helm-releases.api"; -import releaseStoreInjectable from "../+apps-releases/release-store.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; import installChartStoreInjectable from "./install-chart-store/install-chart-store.injectable"; import dockStoreInjectable from "./dock-store/dock-store.injectable"; +import createReleaseInjectable from "../+apps-releases/create-release/create-release.injectable"; interface Props { tab: DockTab; @@ -222,7 +222,7 @@ export const InstallChart = withInjectables( { getProps: (di, props) => ({ - createRelease: di.inject(releaseStoreInjectable).create, + createRelease: di.inject(createReleaseInjectable), installChartStore: di.inject(installChartStoreInjectable), dockStore: di.inject(dockStoreInjectable), ...props, diff --git a/src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts b/src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts index 7a660c133a..d0732e7fdb 100644 --- a/src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts +++ b/src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts @@ -4,10 +4,10 @@ */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { UpgradeChartStore } from "./upgrade-chart.store"; -import releaseStoreInjectable from "../../+apps-releases/release-store.injectable"; import dockStoreInjectable from "../dock-store/dock-store.injectable"; import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; +import releasesInjectable from "../../+apps-releases/releases.injectable"; const upgradeChartStoreInjectable = getInjectable({ instantiate: (di) => { @@ -16,7 +16,7 @@ const upgradeChartStoreInjectable = getInjectable({ const valuesStore = createDockTabStore(); return new UpgradeChartStore({ - releaseStore: di.inject(releaseStoreInjectable), + releases: di.inject(releasesInjectable), dockStore: di.inject(dockStoreInjectable), createStorage: di.inject(createStorageInjectable), valuesStore, diff --git a/src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts b/src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts index 917bc46ce8..f6e6a2dde2 100644 --- a/src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts +++ b/src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts @@ -3,12 +3,22 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { action, autorun, computed, IReactionDisposer, reaction, makeObservable } from "mobx"; +import { + action, + autorun, + computed, + IReactionDisposer, + reaction, + makeObservable, +} from "mobx"; import { DockStore, DockTab, TabId, TabKind } from "../dock-store/dock.store"; import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; -import { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api"; -import type { ReleaseStore } from "../../+apps-releases/release.store"; +import { + getReleaseValues, + HelmRelease, +} from "../../../../common/k8s-api/endpoints/helm-releases.api"; import { iter, StorageHelper } from "../../../utils"; +import type { IAsyncComputed } from "@ogre-tools/injectable-react"; export interface IChartUpgradeData { releaseName: string; @@ -16,7 +26,7 @@ export interface IChartUpgradeData { } interface Dependencies { - releaseStore: ReleaseStore + releases: IAsyncComputed valuesStore: DockTabStore dockStore: DockStore createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> @@ -60,14 +70,14 @@ export class UpgradeChartStore extends DockTabStore { return; } const dispose = reaction(() => { - const release = this.dependencies.releaseStore.getByName(releaseName); + const release = this.dependencies.releases.value.get().find(release => release.getName() === releaseName); return release?.getRevision(); // watch changes only by revision }, release => { const releaseTab = this.getTabByRelease(releaseName); - if (!this.dependencies.releaseStore.isLoaded || !releaseTab) { + if (!releaseTab) { return; } @@ -91,7 +101,7 @@ export class UpgradeChartStore extends DockTabStore { isLoading(tabId = this.dependencies.dockStore.selectedTabId) { const values = this.values.getData(tabId); - return !this.dependencies.releaseStore.isLoaded || values === undefined; + return values === undefined; } @action @@ -99,7 +109,6 @@ export class UpgradeChartStore extends DockTabStore { const values = this.values.getData(tabId); await Promise.all([ - !this.dependencies.releaseStore.isLoaded && this.dependencies.releaseStore.loadFromContextNamespaces(), !values && this.loadValues(tabId), ]); } diff --git a/src/renderer/components/dock/upgrade-chart.tsx b/src/renderer/components/dock/upgrade-chart.tsx index 0136d65d53..270656e9f4 100644 --- a/src/renderer/components/dock/upgrade-chart.tsx +++ b/src/renderer/components/dock/upgrade-chart.tsx @@ -13,15 +13,22 @@ import type { DockTab } from "./dock-store/dock.store"; import { InfoPanel } from "./info-panel"; import type { UpgradeChartStore } from "./upgrade-chart-store/upgrade-chart.store"; import { Spinner } from "../spinner"; -import type { ReleaseStore } from "../+apps-releases/release.store"; import { Badge } from "../badge"; import { EditorPanel } from "./editor-panel"; -import { helmChartStore, IChartVersion } from "../+apps-helm-charts/helm-chart.store"; -import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; +import { + helmChartStore, + IChartVersion, +} from "../+apps-helm-charts/helm-chart.store"; +import type { + HelmRelease, + IReleaseUpdateDetails, + IReleaseUpdatePayload, +} from "../../../common/k8s-api/endpoints/helm-releases.api"; import { Select, SelectOption } from "../select"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import releaseStoreInjectable from "../+apps-releases/release-store.injectable"; +import { IAsyncComputed, withInjectables } from "@ogre-tools/injectable-react"; import upgradeChartStoreInjectable from "./upgrade-chart-store/upgrade-chart-store.injectable"; +import updateReleaseInjectable from "../+apps-releases/update-release/update-release.injectable"; +import releasesInjectable from "../+apps-releases/releases.injectable"; interface Props { className?: string; @@ -29,8 +36,9 @@ interface Props { } interface Dependencies { - releaseStore: ReleaseStore + releases: IAsyncComputed upgradeChartStore: UpgradeChartStore + updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise } @observer @@ -61,7 +69,7 @@ export class NonInjectedUpgradeChart extends React.Component release.getName() === tabData.releaseName); } get value() { @@ -95,7 +103,7 @@ export class NonInjectedUpgradeChart extends React.Component( { getProps: (di, props) => ({ - releaseStore: di.inject(releaseStoreInjectable), + releases: di.inject(releasesInjectable), + updateRelease: di.inject(updateReleaseInjectable), upgradeChartStore: di.inject(upgradeChartStoreInjectable), ...props, }), diff --git a/src/renderer/components/drawer/drawer.tsx b/src/renderer/components/drawer/drawer.tsx index ce06c26945..deaf6bad2b 100644 --- a/src/renderer/components/drawer/drawer.tsx +++ b/src/renderer/components/drawer/drawer.tsx @@ -11,12 +11,13 @@ import { createPortal } from "react-dom"; import { cssNames, noop, StorageHelper } from "../../utils"; import { Icon } from "../icon"; import { Animate, AnimateName } from "../animate"; -import { history } from "../../navigation"; import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor"; import drawerStorageInjectable, { defaultDrawerWidth, } from "./drawer-storage/drawer-storage.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; +import historyInjectable from "../../navigation/history.injectable"; +import type { History } from "history"; export type DrawerPosition = "top" | "left" | "right" | "bottom"; @@ -59,6 +60,7 @@ resizingAnchorProps.set("top", [ResizeDirection.VERTICAL, ResizeSide.TRAILING, R resizingAnchorProps.set("bottom", [ResizeDirection.VERTICAL, ResizeSide.LEADING, ResizeGrowthDirection.BOTTOM_TO_TOP]); interface Dependencies { + history: History drawerStorage: StorageHelper<{ width: number }>; } @@ -70,7 +72,7 @@ class NonInjectedDrawer extends React.Component(); - private stopListenLocation = history.listen(() => { + private stopListenLocation = this.props.history.listen(() => { this.restoreScrollPos(); }); @@ -111,14 +113,14 @@ class NonInjectedDrawer extends React.Component { if (!this.scrollElem) return; - const key = history.location.key; + const key = this.props.history.location.key; this.scrollPos.set(key, this.scrollElem.scrollTop); }; restoreScrollPos = () => { if (!this.scrollElem) return; - const key = history.location.key; + const key = this.props.history.location.key; this.scrollElem.scrollTop = this.scrollPos.get(key) || 0; }; @@ -232,6 +234,7 @@ export const Drawer = withInjectables( { getProps: (di, props) => ({ + history: di.inject(historyInjectable), drawerStorage: di.inject(drawerStorageInjectable), ...props, }), diff --git a/src/renderer/frames/cluster-frame/cluster-frame.tsx b/src/renderer/frames/cluster-frame/cluster-frame.tsx index 8cc1277e6c..cad742f485 100755 --- a/src/renderer/frames/cluster-frame/cluster-frame.tsx +++ b/src/renderer/frames/cluster-frame/cluster-frame.tsx @@ -6,7 +6,6 @@ import React from "react"; import { observable, makeObservable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { Redirect, Route, Router, Switch } from "react-router"; -import { history } from "../../navigation"; import { UserManagement } from "../../components/+user-management/user-management"; import { ConfirmDialog } from "../../components/confirm-dialog"; import { ClusterOverview } from "../../components/+cluster/cluster-overview"; @@ -41,17 +40,18 @@ import { PortForwardDialog } from "../../port-forward"; import { DeleteClusterDialog } from "../../components/delete-cluster-dialog"; import type { NamespaceStore } from "../../components/+namespaces/namespace-store/namespace.store"; import { withInjectables } from "@ogre-tools/injectable-react"; -import namespaceStoreInjectable - from "../../components/+namespaces/namespace-store/namespace-store.injectable"; +import namespaceStoreInjectable from "../../components/+namespaces/namespace-store/namespace-store.injectable"; import type { ClusterId } from "../../../common/cluster-types"; -import hostedClusterInjectable - from "../../../common/cluster-store/hosted-cluster/hosted-cluster.injectable"; +import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster/hosted-cluster.injectable"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import type { Disposer } from "../../../common/utils"; import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; +import historyInjectable from "../../navigation/history.injectable"; +import type { History } from "history"; interface Dependencies { + history: History, namespaceStore: NamespaceStore hostedClusterId: ClusterId subscribeStores: (stores: KubeObjectStore[]) => Disposer @@ -135,10 +135,11 @@ class NonInjectedClusterFrame extends React.Component { render() { return ( - + } footer={}> + @@ -181,6 +182,7 @@ class NonInjectedClusterFrame extends React.Component { export const ClusterFrame = withInjectables(NonInjectedClusterFrame, { getProps: di => ({ + history: di.inject(historyInjectable), namespaceStore: di.inject(namespaceStoreInjectable), hostedClusterId: di.inject(hostedClusterInjectable).id, subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, diff --git a/src/renderer/frames/root-frame/root-frame.tsx b/src/renderer/frames/root-frame/root-frame.tsx index bc148f3a70..2216865b67 100644 --- a/src/renderer/frames/root-frame/root-frame.tsx +++ b/src/renderer/frames/root-frame/root-frame.tsx @@ -7,7 +7,6 @@ import { injectSystemCAs } from "../../../common/system-ca"; import React from "react"; import { Route, Router, Switch } from "react-router"; import { observer } from "mobx-react"; -import { history } from "../../navigation"; import { ClusterManager } from "../../components/cluster-manager"; import { ErrorBoundary } from "../../components/error-boundary"; import { Notifications } from "../../components/notifications"; @@ -16,14 +15,21 @@ import { CommandContainer } from "../../components/command-palette/command-conta import { ipcRenderer } from "electron"; import { IpcRendererNavigationEvents } from "../../navigation/events"; import { ClusterFrameHandler } from "../../components/cluster-manager/lens-views"; +import historyInjectable from "../../navigation/history.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { History } from "history"; injectSystemCAs(); +interface Dependencies { + history: History +} + @observer -export class RootFrame extends React.Component { +class NonInjectedRootFrame extends React.Component { static displayName = "RootFrame"; - constructor(props: {}) { + constructor(props: Dependencies) { super(props); ClusterFrameHandler.createInstance(); @@ -35,7 +41,7 @@ export class RootFrame extends React.Component { render() { return ( - + @@ -48,3 +54,7 @@ export class RootFrame extends React.Component { ); } } + +export const RootFrame = withInjectables(NonInjectedRootFrame, { + getProps: (di) => ({ history: di.inject(historyInjectable) }), +}); diff --git a/src/renderer/navigation/history.injectable.ts b/src/renderer/navigation/history.injectable.ts new file mode 100644 index 0000000000..0f5acaa2d8 --- /dev/null +++ b/src/renderer/navigation/history.injectable.ts @@ -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, lifecycleEnum } from "@ogre-tools/injectable"; +import { history } from "./history"; + +const historyInjectable = getInjectable({ + instantiate: () => history, + lifecycle: lifecycleEnum.singleton, +}); + +export default historyInjectable; diff --git a/src/renderer/navigation/history.ts b/src/renderer/navigation/history.ts index 87c5625209..fa7df172b4 100644 --- a/src/renderer/navigation/history.ts +++ b/src/renderer/navigation/history.ts @@ -14,8 +14,14 @@ export const searchParamsOptions: ObservableSearchParamsOptions = { joinArraysWith: ",", // param values splitter, applicable only with {joinArrays:true} }; +/** + * @deprecated: Switch to using di.inject(historyInjectable) + */ export const history = ipcRenderer ? createBrowserHistory() : createMemoryHistory(); +/** + * @deprecated: Switch to using di.inject(observableHistoryInjectable) + */ export const navigation = createObservableHistory(history, { searchParams: searchParamsOptions, }); diff --git a/src/renderer/navigation/observable-history.injectable.ts b/src/renderer/navigation/observable-history.injectable.ts new file mode 100644 index 0000000000..db50db04f2 --- /dev/null +++ b/src/renderer/navigation/observable-history.injectable.ts @@ -0,0 +1,15 @@ +/** + * 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 { navigation as observableHistory } from "./history"; + +const observableHistoryInjectable = getInjectable({ + instantiate: () => observableHistory, + + lifecycle: lifecycleEnum.singleton, +}); + +export default observableHistoryInjectable; diff --git a/yarn.lock b/yarn.lock index 5d0e02d9ee..f1390aa0ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -979,19 +979,19 @@ dependencies: lodash "^4.17.21" -"@ogre-tools/injectable-react@3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-3.1.1.tgz#d95ecec518ba798c36fa3a6f651fa52748e72b00" - integrity sha512-Fhb/51NzrLzkA3G5zCpNOshvm0el1gROWGHkBqq1d/8PEekcEijIL8HZ6B/ylCWjQTJ1MaYViJdzs2iNP1oQxw== +"@ogre-tools/injectable-react@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-3.2.0.tgz#468542e846952deb8e7a4f6757da4813ee8f11fa" + integrity sha512-VU5l0uKe86psVzEPbXl1TLlflnoL+uSeOaOCy/mAGzau4nqRb+eA4RzYgzUs/D9tDYzJ7Es+LZWD9uSyXmpyXg== dependencies: "@ogre-tools/fp" "^3.0.0" - "@ogre-tools/injectable" "^3.1.1" + "@ogre-tools/injectable" "^3.2.0" lodash "^4.17.21" -"@ogre-tools/injectable@3.1.1", "@ogre-tools/injectable@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-3.1.1.tgz#2f293a90e4d3f730ebab2689fd609edc24ffc563" - integrity sha512-X7cDU2Mkcl2bP8JtR9l/Hx31jmKYEuCVJGjZIYxWlE1Nvd3HGq98oTV5uEGNP6+GjLHhXjzoscT9SKKzexyQWg== +"@ogre-tools/injectable@3.2.0", "@ogre-tools/injectable@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-3.2.0.tgz#7d8f653cb3a2c0253a29422bcffd5123308600a9" + integrity sha512-aRlRdvLefJMBvFu1tRlTGNgpMbqE250lwMXT8Y6/ruC88rCL+TygCWcsJELad1OgqX1cfHkCgCYeQeohV+G3Zg== dependencies: "@ogre-tools/fp" "^3.0.0" lodash "^4.17.21"