Make opening of release details work properly when release has resources without namespace (#6088)

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>
Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
authored and GitHub committed 2022-09-08 13:57:11 -04:00
1 parent d4bb416f99
commit ec78080d99
38 files changed
+11090 -10160

No files matched your search

@@ -8,8 +8,8 @@ import "./release-details.scss";
import React from "react";
import { Link } from "react-router-dom";
import { Drawer, DrawerItem, DrawerTitle } from "../../drawer";
import { cssNames, stopPropagation } from "../../../utils";
import { DrawerItem, DrawerTitle } from "../../drawer";
import { stopPropagation } from "../../../utils";
import { observer } from "mobx-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { ConfigurationInput, MinimalResourceGroup, OnlyUserSuppliedValuesAreShownToggle, ReleaseDetailsModel } from "./release-details-model/release-details-model.injectable";
@@ -20,7 +20,6 @@ import { Badge } from "../../badge";
import { SubTitle } from "../../layout/sub-title";
import { Table, TableCell, TableHead, TableRow } from "../../table";
import { ReactiveDuration } from "../../duration/reactive-duration";
import { HelmReleaseMenu } from "../release-menu";
import { Checkbox } from "../../checkbox";
import { MonacoEditor } from "../../monaco-editor";
import { Spinner } from "../../spinner";
@@ -35,97 +34,86 @@ interface Dependencies {
}
const NonInjectedReleaseDetailsContent = observer(({ model }: Dependencies & ReleaseDetailsContentProps) => {
const isLoading = model.isLoading.get();
const loadingError = model.loadingError.get();
if (loadingError) {
return (
<div data-testid="helm-release-detail-error">
Failed to load release:
{" "}
{loadingError}
</div>
);
}
return (
<Drawer
className={cssNames("ReleaseDetails", model.activeTheme)}
usePortal={true}
open={true}
title={isLoading ? "" : model.release.getName()}
onClose={model.close}
testIdForClose="close-helm-release-detail"
toolbar={
!isLoading && (
<HelmReleaseMenu
release={model.release}
toolbar
hideDetails={model.close}
<div>
<DrawerItem name="Chart" className="chart">
<div className="flex gaps align-center">
<span>{model.release.chart}</span>
<Button
primary
label="Upgrade"
className="box right upgrade"
onClick={model.startUpgradeProcess}
data-testid="helm-release-upgrade-button"
/>
)
}
data-testid={`helm-release-details-for-${model.id}`}
>
{isLoading ? (
<Spinner center data-testid="helm-release-detail-content-spinner" />
) : (
<div>
<DrawerItem name="Chart" className="chart">
<div className="flex gaps align-center">
<span>{model.release.chart}</span>
</div>
</DrawerItem>
<Button
primary
label="Upgrade"
className="box right upgrade"
onClick={model.startUpgradeProcess}
data-testid="helm-release-upgrade-button"
/>
</div>
</DrawerItem>
<DrawerItem name="Updated">
{`${model.release.getUpdated()} ago (${model.release.updated})`}
</DrawerItem>
<DrawerItem name="Updated">
{model.release.getUpdated()}
{` ago (${model.release.updated})`}
</DrawerItem>
<DrawerItem name="Namespace">{model.release.getNs()}</DrawerItem>
<DrawerItem name="Namespace">{model.release.getNs()}</DrawerItem>
<DrawerItem name="Version" onClick={stopPropagation}>
<div className="version flex gaps align-center">
<span>{model.release.getVersion()}</span>
</div>
</DrawerItem>
<DrawerItem name="Version" onClick={stopPropagation}>
<div className="version flex gaps align-center">
<span>{model.release.getVersion()}</span>
</div>
</DrawerItem>
<DrawerItem
name="Status"
className="status"
labelsOnly>
<Badge
label={model.release.getStatus()}
className={kebabCase(model.release.getStatus())}
/>
</DrawerItem>
<DrawerItem
name="Status"
className="status"
labelsOnly>
<Badge
label={model.release.getStatus()}
className={kebabCase(model.release.getStatus())}
/>
</DrawerItem>
<ReleaseValues
configuration={model.configuration}
onlyUserSuppliedValuesAreShown={
model.onlyUserSuppliedValuesAreShown
}
/>
<ReleaseValues
configuration={model.configuration}
onlyUserSuppliedValuesAreShown={
model.onlyUserSuppliedValuesAreShown
}
/>
<DrawerTitle>Notes</DrawerTitle>
<DrawerTitle>Notes</DrawerTitle>
{model.notes && <div className="notes">{model.notes}</div>}
{model.notes && <div className="notes">{model.notes}</div>}
<DrawerTitle>Resources</DrawerTitle>
<DrawerTitle>Resources</DrawerTitle>
{model.groupedResources.length > 0 && (
<div className="resources">
{model.groupedResources.map((group) => (
<ResourceGroup key={group.kind} group={group} />
))}
</div>
)}
{model.groupedResources.length > 0 && (
<div className="resources">
{model.groupedResources.map((group) => (
<ResourceGroup key={group.kind} group={group} />
))}
</div>
)}
</Drawer>
</div>
);
});
export const ReleaseDetailsContent = withInjectables<Dependencies, ReleaseDetailsContentProps>(NonInjectedReleaseDetailsContent, {
getProps: (di, props) => ({
model: di.inject(releaseDetailsModelInjectable, props.targetRelease),
getPlaceholder: () => <Spinner center data-testid="helm-release-detail-content-spinner" />,
getProps: async (di, props) => ({
model: await di.inject(releaseDetailsModelInjectable, props.targetRelease),
...props,
}),
});
@@ -0,0 +1,53 @@
/**
* 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 from "react";
import { observer } from "mobx-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { TargetHelmRelease } from "./target-helm-release.injectable";
import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import type { ReleaseDetailsModel } from "./release-details-model/release-details-model.injectable";
import releaseDetailsModelInjectable from "./release-details-model/release-details-model.injectable";
import { HelmReleaseMenu } from "../release-menu";
interface ReleaseDetailsDrawerProps {
targetRelease: TargetHelmRelease;
}
interface Dependencies {
model: ReleaseDetailsModel;
navigateToHelmReleases: () => void;
}
const NonInjectedReleaseDetailsDrawerToolbar = observer(({
model,
navigateToHelmReleases,
}: Dependencies & ReleaseDetailsDrawerProps) => (
model.loadingError.get()
? null
: (
<HelmReleaseMenu
release={model.release}
toolbar
hideDetails={navigateToHelmReleases}
/>
)
));
export const ReleaseDetailsDrawerToolbar = withInjectables<
Dependencies,
ReleaseDetailsDrawerProps
>(NonInjectedReleaseDetailsDrawerToolbar, {
getPlaceholder: () => <></>,
getProps: async (di, props) => ({
model: await di.inject(releaseDetailsModelInjectable, props.targetRelease),
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
...props,
}),
});
@@ -0,0 +1,58 @@
/**
* 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 from "react";
import { Drawer } from "../../drawer";
import { cssNames } from "../../../utils";
import { observer } from "mobx-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { TargetHelmRelease } from "./target-helm-release.injectable";
import type { ActiveThemeType } from "../../../themes/active-type.injectable";
import activeThemeTypeInjectable from "../../../themes/active-type.injectable";
import { ReleaseDetailsContent } from "./release-details-content";
import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import { ReleaseDetailsDrawerToolbar } from "./release-details-drawer-toolbar";
interface ReleaseDetailsDrawerProps {
targetRelease: TargetHelmRelease;
}
interface Dependencies {
activeThemeType: ActiveThemeType;
closeDrawer: () => void;
}
const NonInjectedReleaseDetailsDrawer = observer(({
activeThemeType,
closeDrawer,
targetRelease,
}: Dependencies & ReleaseDetailsDrawerProps) => (
<Drawer
className={cssNames("ReleaseDetails", activeThemeType.get())}
usePortal={true}
open={true}
title={targetRelease.name}
onClose={closeDrawer}
testIdForClose="close-helm-release-detail"
toolbar={<ReleaseDetailsDrawerToolbar targetRelease={targetRelease} />}
data-testid={`helm-release-details-for-${targetRelease.namespace}/${targetRelease.name}`}
>
<ReleaseDetailsContent targetRelease={targetRelease} />
</Drawer>
));
export const ReleaseDetailsDrawer = withInjectables<
Dependencies,
ReleaseDetailsDrawerProps
>(NonInjectedReleaseDetailsDrawer, {
getProps: (di, props) => ({
activeThemeType: di.inject(activeThemeTypeInjectable),
closeDrawer: di.inject(navigateToHelmReleasesInjectable),
...props,
}),
});
@@ -7,6 +7,7 @@ import type { HelmReleaseDto } from "../../../../../../common/k8s-api/endpoints/
import callForHelmReleasesInjectable from "../../../call-for-helm-releases/call-for-helm-releases.injectable";
import type { HelmReleaseDetails } from "./call-for-helm-release-details/call-for-helm-release-details.injectable";
import callForHelmReleaseDetailsInjectable from "./call-for-helm-release-details/call-for-helm-release-details.injectable";
import type { AsyncResult } from "../../../../../../common/utils/async-result";
export interface DetailedHelmRelease {
release: HelmReleaseDto;
@@ -16,7 +17,7 @@ export interface DetailedHelmRelease {
export type CallForHelmRelease = (
name: string,
namespace: string
) => Promise<DetailedHelmRelease | undefined>;
) => Promise<AsyncResult<DetailedHelmRelease | undefined>>;
const callForHelmReleaseInjectable = getInjectable({
id: "call-for-helm-release",
@@ -36,10 +37,13 @@ const callForHelmReleaseInjectable = getInjectable({
);
if (!release) {
return undefined;
return {
callWasSuccessful: false,
error: `Release ${name} didn't exist in ${namespace} namespace.`,
};
}
return { release, details };
return { callWasSuccessful: true, response: { release, details }};
};
},
});
@@ -26,16 +26,14 @@ import showSuccessNotificationInjectable from "../../../notifications/show-succe
import React from "react";
import createUpgradeChartTabInjectable from "../../../dock/upgrade-chart/create-upgrade-chart-tab.injectable";
import type { HelmRelease } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
import type { NavigateToHelmReleases } from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import navigateToHelmReleasesInjectable from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import assert from "assert";
import withOrphanPromiseInjectable from "../../../../../common/utils/with-orphan-promise/with-orphan-promise.injectable";
import activeThemeInjectable from "../../../../themes/active.injectable";
const releaseDetailsModelInjectable = getInjectable({
id: "release-details-model",
instantiate: (di, targetRelease: TargetHelmRelease) => {
instantiate: async (di, targetRelease: TargetHelmRelease) => {
const callForHelmRelease = di.inject(callForHelmReleaseInjectable);
const callForHelmReleaseConfiguration = di.inject(callForHelmReleaseConfigurationInjectable);
const activeTheme = di.inject(activeThemeInjectable);
@@ -45,7 +43,6 @@ const releaseDetailsModelInjectable = getInjectable({
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
const createUpgradeChartTab = di.inject(createUpgradeChartTabInjectable);
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
const withOrphanPromise = di.inject(withOrphanPromiseInjectable);
const model = new ReleaseDetailsModel({
callForHelmRelease,
@@ -60,56 +57,52 @@ const releaseDetailsModelInjectable = getInjectable({
navigateToHelmReleases,
});
const load = withOrphanPromise(model.load);
// TODO: Reorganize Drawer to allow setting of header-bar in children to make "getPlaceholder" from injectable usable.
load();
await model.load();
return model;
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, release: TargetHelmRelease) =>
`${release.namespace}/${release.name}`,
getInstanceKey: (di, release: TargetHelmRelease) => `${release.namespace}/${release.name}`,
}),
});
export default releaseDetailsModelInjectable;
export interface OnlyUserSuppliedValuesAreShownToggle {
value: IObservableValue<boolean>;
readonly value: IObservableValue<boolean>;
toggle: () => Promise<void>;
}
export interface ConfigurationInput {
nonSavedValue: IObservableValue<string>;
isLoading: IObservableValue<boolean>;
isSaving: IObservableValue<boolean>;
readonly nonSavedValue: IObservableValue<string>;
readonly isLoading: IObservableValue<boolean>;
readonly isSaving: IObservableValue<boolean>;
onChange: (value: string) => void;
save: () => Promise<void>;
}
interface Dependencies {
readonly targetRelease: TargetHelmRelease;
readonly activeTheme: IComputedValue<LensTheme>;
callForHelmRelease: CallForHelmRelease;
targetRelease: TargetHelmRelease;
activeTheme: IComputedValue<LensTheme>;
callForHelmReleaseConfiguration: CallForHelmReleaseConfiguration;
getResourceDetailsUrl: GetResourceDetailsUrl;
updateRelease: CallForHelmReleaseUpdate;
showCheckedErrorNotification: ShowCheckedErrorNotification;
showSuccessNotification: ShowNotification;
createUpgradeChartTab: (release: HelmRelease) => string;
navigateToHelmReleases: NavigateToHelmReleases;
navigateToHelmReleases: () => void;
}
export class ReleaseDetailsModel {
id = `${this.dependencies.targetRelease.namespace}/${this.dependencies.targetRelease.name}`;
readonly id = `${this.dependencies.targetRelease.namespace}/${this.dependencies.targetRelease.name}`;
constructor(private dependencies: Dependencies) {}
constructor(private readonly dependencies: Dependencies) {}
private detailedRelease = observable.box<DetailedHelmRelease | undefined>();
private readonly detailedRelease = observable.box<DetailedHelmRelease | undefined>();
readonly isLoading = observable.box(false);
readonly loadingError = observable.box<string>();
readonly configuration: ConfigurationInput = {
nonSavedValue: observable.box(""),
@@ -176,26 +169,26 @@ export class ReleaseDetailsModel {
};
load = async () => {
runInAction(() => {
this.isLoading.set(true);
});
const { name, namespace } = this.dependencies.targetRelease;
const detailedRelease = await this.dependencies.callForHelmRelease(
const result = await this.dependencies.callForHelmRelease(
name,
namespace,
);
if (!result.callWasSuccessful) {
runInAction(() => {
this.loadingError.set(result.error);
});
return;
}
runInAction(() => {
this.detailedRelease.set(detailedRelease);
this.detailedRelease.set(result.response);
});
await this.loadConfiguration();
runInAction(() => {
this.isLoading.set(false);
});
};
private loadConfiguration = async () => {
@@ -269,7 +262,7 @@ export class ReleaseDetailsModel {
startUpgradeProcess = () => {
this.dependencies.createUpgradeChartTab(this.release);
this.close();
this.dependencies.navigateToHelmReleases();
};
}
@@ -49,16 +49,19 @@
.TableCell {
text-overflow: unset;
word-break: break-word;
min-width: 100px;
&.name {
flex-basis: auto;
flex-grow: 0;
width: 230px;
flex-grow: 3;
}
&.volume {
flex-basis: 30%;
&.namespace {
flex-grow: 1;
}
&.age {
flex-basis: 100px;
flex-grow: 0;
flex-shrink: 0;
}
}
}
@@ -11,9 +11,9 @@ import { observer } from "mobx-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import { ReleaseDetailsContent } from "./release-details-content";
import type { TargetHelmRelease } from "./target-helm-release.injectable";
import targetHelmReleaseInjectable from "./target-helm-release.injectable";
import { ReleaseDetailsDrawer } from "./release-details-drawer";
interface Dependencies {
targetRelease: IComputedValue<
@@ -25,7 +25,7 @@ const NonInjectedReleaseDetails = observer(
({ targetRelease }: Dependencies) => {
const release = targetRelease.get();
return release ? <ReleaseDetailsContent targetRelease={release} /> : null;
return release ? <ReleaseDetailsDrawer targetRelease={release} /> : null;
},
);
@@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import type { LensThemeType } from "./store";
import themeStoreInjectable from "./store.injectable";
export type ActiveThemeType = IComputedValue<LensThemeType>;
const activeThemeTypeInjectable = getInjectable({
id: "active-theme-type",
instantiate: (di) => {
const store = di.inject(themeStoreInjectable);
return computed(() => store.activeTheme.type);
},
});
export default activeThemeTypeInjectable;
+2 -2
View File
@@ -17,10 +17,10 @@ import type { ReadonlyDeep } from "type-fest/source/readonly-deep";
import assert from "assert";
export type ThemeId = string;
export type LensThemeType = "dark" | "light";
export interface LensTheme {
name: string;
type: "dark" | "light";
type: LensThemeType;
colors: Record<string, string>;
description: string;
author: string;