mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix infinite render loop in release details by replacing stateful, UI-triggered releaseStore with reactive async computed
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
5ca194b401
commit
5b4282ea8c
@ -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<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
|
||||
|
||||
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
||||
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
|
||||
const releases = await apiBase.get<HelmReleaseDto[]>(endpoint({ namespace }));
|
||||
|
||||
return releases.map(HelmRelease.create);
|
||||
return releases.map(toHelmRelease);
|
||||
}
|
||||
|
||||
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
||||
@ -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<string>
|
||||
}
|
||||
|
||||
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 : "";
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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());
|
||||
};
|
||||
@ -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<IReleaseUpdateDetails>
|
||||
createUpgradeChartTab: (release: HelmRelease) => void
|
||||
}
|
||||
|
||||
@ -65,9 +72,9 @@ class NonInjectedReleaseDetails extends Component<Props & Dependencies> {
|
||||
}),
|
||||
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<Props & Dependencies> {
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await this.props.releaseStore.update(name, namespace, data);
|
||||
await this.props.updateRelease(name, namespace, data);
|
||||
Notifications.ok(
|
||||
<p>Release <b>{name}</b> successfully updated!</p>,
|
||||
);
|
||||
@ -313,7 +320,8 @@ export const ReleaseDetails = withInjectables<Dependencies, Props>(
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
releaseStore: di.inject(releaseStoreInjectable),
|
||||
getReleaseSecret: di.inject(getReleaseSecretInjectable),
|
||||
updateRelease: di.inject(updateReleaseInjectable),
|
||||
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
||||
...props,
|
||||
}),
|
||||
|
||||
@ -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<any>
|
||||
createUpgradeChartTab: (release: HelmRelease) => void
|
||||
openRollbackDialog: (release: HelmRelease) => void
|
||||
}
|
||||
|
||||
class NonInjectedHelmReleaseMenu extends React.Component<Props & Dependencies> {
|
||||
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<Dependencies, Props>(
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
releaseStore: di.inject(releaseStoreInjectable),
|
||||
deleteRelease: di.inject(deleteReleaseInjectable),
|
||||
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
||||
openRollbackDialog: di.inject(releaseRollbackDialogModelInjectable).open,
|
||||
|
||||
|
||||
@ -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<any>
|
||||
rollbackRelease: (releaseName: string, namespace: string, revisionNumber: number) => Promise<void>
|
||||
model: ReleaseRollbackDialogModel
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ export const ReleaseRollbackDialog = withInjectables<Dependencies, Props>(
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
rollbackRelease: di.inject(releaseStoreInjectable).rollback,
|
||||
rollbackRelease: di.inject(rollbackReleaseInjectable),
|
||||
model: di.inject(releaseRollbackDialogModelInjectable),
|
||||
...props,
|
||||
}),
|
||||
|
||||
@ -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;
|
||||
@ -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<HelmRelease> {
|
||||
releaseSecrets = observable.map<string, Secret>();
|
||||
|
||||
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<void> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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<ReleaseRouteParams> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
releaseStore: ReleaseStore
|
||||
releases: IComputedValue<RemovableHelmRelease[]>
|
||||
releasesArePending: IComputedValue<boolean>
|
||||
selectNamespace: (namespace: string) => void
|
||||
}
|
||||
|
||||
@ -51,17 +56,12 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
|
||||
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<Dependencies & Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.releasesArePending.get()) {
|
||||
// TODO: Make Spinner "center" work properly
|
||||
return <div className="flex center" style={{ height: "100%" }}><Spinner /></div>;
|
||||
}
|
||||
|
||||
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<RemovableHelmRelease>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ItemListLayout
|
||||
store={legacyReleaseStore}
|
||||
preloadStores={false}
|
||||
isConfigurable
|
||||
tableId="helm_releases"
|
||||
className="HelmReleases"
|
||||
store={this.props.releaseStore}
|
||||
dependentStores={[secretsStore]}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: release => release.getName(),
|
||||
[columnId.namespace]: release => release.getNs(),
|
||||
@ -170,10 +213,12 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
|
||||
detailsItem={this.selectedRelease}
|
||||
onDetails={this.onDetails}
|
||||
/>
|
||||
|
||||
<ReleaseDetails
|
||||
release={this.selectedRelease}
|
||||
hideDetails={this.hideDetails}
|
||||
/>
|
||||
|
||||
<ReleaseRollbackDialog/>
|
||||
</>
|
||||
);
|
||||
@ -185,7 +230,8 @@ export const HelmReleases = withInjectables<Dependencies, Props>(
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
releaseStore: di.inject(releaseStoreInjectable),
|
||||
releases: di.inject(removableReleasesInjectable),
|
||||
releasesArePending: di.inject(releasesInjectable).pending,
|
||||
selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces,
|
||||
...props,
|
||||
}),
|
||||
|
||||
@ -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<string, boolean>(),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default removableReleasesInjectable;
|
||||
52
src/renderer/components/+apps-releases/removable-releases.ts
Normal file
52
src/renderer/components/+apps-releases/removable-releases.ts
Normal file
@ -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<HelmRelease[]>;
|
||||
releaseSelectionStatus: ObservableMap<string, boolean>;
|
||||
deleteRelease: (release: HelmRelease) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface RemovableHelmRelease extends HelmRelease {
|
||||
toggle: () => void;
|
||||
isSelected: boolean;
|
||||
delete: () => Promise<void>;
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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<Dependencies, Props>(
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
createRelease: di.inject(releaseStoreInjectable).create,
|
||||
createRelease: di.inject(createReleaseInjectable),
|
||||
installChartStore: di.inject(installChartStoreInjectable),
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
...props,
|
||||
|
||||
@ -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<string>();
|
||||
|
||||
return new UpgradeChartStore({
|
||||
releaseStore: di.inject(releaseStoreInjectable),
|
||||
releases: di.inject(releasesInjectable),
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
createStorage: di.inject(createStorageInjectable),
|
||||
valuesStore,
|
||||
|
||||
@ -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<HelmRelease[]>
|
||||
valuesStore: DockTabStore<string>
|
||||
dockStore: DockStore
|
||||
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
|
||||
@ -60,14 +70,14 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
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<IChartUpgradeData> {
|
||||
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<IChartUpgradeData> {
|
||||
const values = this.values.getData(tabId);
|
||||
|
||||
await Promise.all([
|
||||
!this.dependencies.releaseStore.isLoaded && this.dependencies.releaseStore.loadFromContextNamespaces(),
|
||||
!values && this.loadValues(tabId),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -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<HelmRelease[]>
|
||||
upgradeChartStore: UpgradeChartStore
|
||||
updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails>
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -61,7 +69,7 @@ export class NonInjectedUpgradeChart extends React.Component<Props & Dependencie
|
||||
|
||||
if (!tabData) return null;
|
||||
|
||||
return this.props.releaseStore.getByName(tabData.releaseName);
|
||||
return this.props.releases.value.get().find(release => release.getName() === tabData.releaseName);
|
||||
}
|
||||
|
||||
get value() {
|
||||
@ -95,7 +103,7 @@ export class NonInjectedUpgradeChart extends React.Component<Props & Dependencie
|
||||
const releaseName = this.release.getName();
|
||||
const releaseNs = this.release.getNs();
|
||||
|
||||
await this.props.releaseStore.update(releaseName, releaseNs, {
|
||||
await this.props.updateRelease(releaseName, releaseNs, {
|
||||
chart: this.release.getChart(),
|
||||
values: this.value,
|
||||
repo, version,
|
||||
@ -167,7 +175,8 @@ export const UpgradeChart = withInjectables<Dependencies, Props>(
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
releaseStore: di.inject(releaseStoreInjectable),
|
||||
releases: di.inject(releasesInjectable),
|
||||
updateRelease: di.inject(updateReleaseInjectable),
|
||||
upgradeChartStore: di.inject(upgradeChartStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user