From 5b4282ea8c740204360dbc12535c1bb3316b33cd Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Mon, 17 Jan 2022 10:24:25 +0200 Subject: [PATCH] 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 --- .../k8s-api/endpoints/helm-releases.api.ts | 53 ++++--- .../create-release.injectable.ts | 28 ++++ .../delete-release.injectable.ts | 26 ++++ .../get-release-secret.injectable.ts | 16 ++ .../get-release-secret/get-release-secret.ts | 21 +++ .../+apps-releases/release-details.tsx | 24 ++- .../+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 | 86 ++++++++--- .../removable-releases.injectable.ts | 22 +++ .../+apps-releases/removable-releases.ts | 52 +++++++ .../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 ++-- 20 files changed, 399 insertions(+), 247 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 create mode 100644 src/renderer/components/+apps-releases/get-release-secret/get-release-secret.injectable.ts create mode 100644 src/renderer/components/+apps-releases/get-release-secret/get-release-secret.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 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/get-release-secret/get-release-secret.injectable.ts b/src/renderer/components/+apps-releases/get-release-secret/get-release-secret.injectable.ts new file mode 100644 index 0000000000..b145dddab0 --- /dev/null +++ b/src/renderer/components/+apps-releases/get-release-secret/get-release-secret.injectable.ts @@ -0,0 +1,16 @@ +/** + * 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 { getReleaseSecret } from "./get-release-secret"; +import { secretsStore } from "../../+config-secrets/secrets.store"; + + +const getReleaseSecretInjectable = getInjectable({ + instantiate: () => getReleaseSecret({ secretsStore }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default getReleaseSecretInjectable; diff --git a/src/renderer/components/+apps-releases/get-release-secret/get-release-secret.ts b/src/renderer/components/+apps-releases/get-release-secret/get-release-secret.ts new file mode 100644 index 0000000000..5f1ac65bad --- /dev/null +++ b/src/renderer/components/+apps-releases/get-release-secret/get-release-secret.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 type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import type { SecretsStore } from "../../+config-secrets/secrets.store"; + +interface Dependencies { + secretsStore: SecretsStore; +} + +export const getReleaseSecret = + ({ secretsStore }: Dependencies) => + (release: HelmRelease) => { + return secretsStore + .getByLabel({ + owner: "helm", + name: release.getName(), + }) + .find((secret) => secret.getNs() == release.getNs()); + }; diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx index 7712ea4052..4d4a56a601 100644 --- a/src/renderer/components/+apps-releases/release-details.tsx +++ b/src/renderer/components/+apps-releases/release-details.tsx @@ -11,7 +11,13 @@ 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 { + getRelease, + getReleaseValues, + 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"; @@ -20,7 +26,6 @@ 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"; @@ -31,9 +36,10 @@ 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"; +import updateReleaseInjectable from "./update-release/update-release.injectable"; +import getReleaseSecretInjectable from "./get-release-secret/get-release-secret.injectable"; interface Props { release: HelmRelease; @@ -41,7 +47,8 @@ interface Props { } interface Dependencies { - releaseStore: ReleaseStore + getReleaseSecret: (release: HelmRelease) => Secret + updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise createUpgradeChartTab: (release: HelmRelease) => void } @@ -65,9 +72,9 @@ class NonInjectedReleaseDetails extends Component { }), reaction(() => secretsStore.getItems(), () => { if (!this.props.release) return; - const { getReleaseSecret } = this.props.releaseStore; + const { release } = this.props; - const secret = getReleaseSecret(release); + const secret = this.props.getReleaseSecret(release); if (this.releaseSecret) { if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return; @@ -125,7 +132,7 @@ class NonInjectedReleaseDetails extends Component { this.saving = true; try { - await this.props.releaseStore.update(name, namespace, data); + await this.props.updateRelease(name, namespace, data); Notifications.ok(

Release {name} successfully updated!

, ); @@ -313,7 +320,8 @@ export const ReleaseDetails = withInjectables( { getProps: (di, props) => ({ - releaseStore: di.inject(releaseStoreInjectable), + getReleaseSecret: di.inject(getReleaseSecretInjectable), + updateRelease: di.inject(updateReleaseInjectable), createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable), ...props, }), 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..98f68df173 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"; +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,17 +56,12 @@ 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 this.props.releases.get().find(release => { return release.getName() == name && release.getNs() == namespace; }); } @@ -101,14 +101,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(), @@ -170,10 +213,12 @@ class NonInjectedHelmReleases extends Component { detailsItem={this.selectedRelease} onDetails={this.onDetails} /> + + ); @@ -185,7 +230,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..cb3ffd68ea --- /dev/null +++ b/src/renderer/components/+apps-releases/removable-releases.ts @@ -0,0 +1,52 @@ +/** + * 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, }),