chore: Remove leading '+' on some directory names

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton committed 2023-04-26 09:26:39 -04:00
1 parent 7dff8e31a6
commit 4379edf0c7
903 files changed
+438 -438

No files matched your search

@@ -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 } from "@ogre-tools/injectable";
import releasesInjectable from "../releases.injectable";
import type { RequestCreateHelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
import requestCreateHelmReleaseInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
const createReleaseInjectable = getInjectable({
id: "create-release",
instantiate: (di): RequestCreateHelmRelease => {
const releases = di.inject(releasesInjectable);
const callForCreateRelease = di.inject(requestCreateHelmReleaseInjectable);
return async (payload) => {
const release = await callForCreateRelease(payload);
releases.invalidate();
return release;
};
},
});
export default createReleaseInjectable;
@@ -0,0 +1,25 @@
/**
* 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 { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import requestDeleteHelmReleaseInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-delete.injectable";
import releasesInjectable from "../releases.injectable";
const deleteReleaseInjectable = getInjectable({
id: "delete-release",
instantiate: (di) => {
const releases = di.inject(releasesInjectable);
const requestDeleteHelmRelease = di.inject(requestDeleteHelmReleaseInjectable);
return async (release: HelmRelease) => {
await requestDeleteHelmRelease(release.getName(), release.getNs());
releases.invalidate();
};
},
});
export default deleteReleaseInjectable;
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.ReleaseRollbackDialog {
.WizardStep {
text-align: center;
.step-content {
padding: var(--wizard-spacing);
}
}
}
@@ -0,0 +1,150 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./dialog.scss";
import React from "react";
import type { IObservableValue } from "mobx";
import { computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import type { DialogProps } from "../../dialog";
import { Dialog } from "../../dialog";
import { Wizard, WizardStep } from "../../wizard";
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { Select } from "../../select";
import orderBy from "lodash/orderBy";
import { withInjectables } from "@ogre-tools/injectable-react";
import releaseRollbackDialogStateInjectable from "./state.injectable";
import type { RollbackRelease } from "../rollback-release/rollback-release.injectable";
import rollbackReleaseInjectable from "../rollback-release/rollback-release.injectable";
import type { HelmReleaseRevision, RequestHelmReleaseHistory } from "../../../../common/k8s-api/endpoints/helm-releases.api/request-history.injectable";
import requestHelmReleaseHistoryInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-history.injectable";
import type { ShowCheckedErrorNotification } from "../../notifications/show-checked-error.injectable";
import showCheckedErrorNotificationInjectable from "../../notifications/show-checked-error.injectable";
export interface ReleaseRollbackDialogProps extends DialogProps {
}
interface Dependencies {
rollbackRelease: RollbackRelease;
state: IObservableValue<HelmRelease | undefined>;
requestHelmReleaseHistory: RequestHelmReleaseHistory;
showCheckedErrorNotification: ShowCheckedErrorNotification;
}
@observer
class NonInjectedReleaseRollbackDialog extends React.Component<ReleaseRollbackDialogProps & Dependencies> {
readonly isLoading = observable.box(false);
readonly revision = observable.box<HelmReleaseRevision | undefined>();
readonly revisions = observable.array<HelmReleaseRevision>();
readonly revisionOptions = computed(() => (
this.revisions.map(revision => ({
value: revision,
label: `${revision.revision} - ${revision.chart} - ${revision.app_version}, updated: ${new Date(revision.updated).toLocaleString()}`,
}))
));
close = () => {
this.props.state.set(undefined);
};
onOpen = async (release: HelmRelease) => {
this.isLoading.set(true);
const releases = await this.props.requestHelmReleaseHistory(release.getName(), release.getNs());
runInAction(() => {
this.revisions.replace(orderBy(releases, "revision", "desc"));
this.revision.set(this.revisions[0]);
this.isLoading.set(false);
});
};
rollback = async (release: HelmRelease) => {
const revision = this.revision.get();
if (!revision) {
return;
}
try {
await this.props.rollbackRelease(release.getName(), release.getNs(), revision.revision);
this.close();
} catch (err) {
this.props.showCheckedErrorNotification(err, "Unknown error occured while rolling back release");
}
};
renderRevisionContent() {
const revision = this.revision.get();
if (!revision) {
return <p>No revisions to rollback.</p>;
}
return (
<div className="flex gaps align-center">
<b>Revision</b>
<Select
id="revision-input"
themeName="light"
value={revision}
options={this.revisionOptions.get()}
onChange={option => this.revision.set(option?.value)}
/>
</div>
);
}
renderContent(release: HelmRelease) {
return (
<Wizard
header={(
<h5>
Rollback
<b>{release.getName()}</b>
</h5>
)}
done={this.close}
>
<WizardStep
scrollable={false}
nextLabel="Rollback"
next={() => this.rollback(release)}
loading={this.isLoading.get()}
>
{this.renderRevisionContent()}
</WizardStep>
</Wizard>
);
}
render() {
const { state, ...dialogProps } = this.props;
const release = state.get();
return (
<Dialog
{...dialogProps}
className="ReleaseRollbackDialog"
isOpen={Boolean(release)}
onOpen={release ? (() => this.onOpen(release)) : undefined}
close={this.close}
>
{release && this.renderContent(release)}
</Dialog>
);
}
}
export const ReleaseRollbackDialog = withInjectables<Dependencies, ReleaseRollbackDialogProps>(NonInjectedReleaseRollbackDialog, {
getProps: (di, props) => ({
...props,
rollbackRelease: di.inject(rollbackReleaseInjectable),
state: di.inject(releaseRollbackDialogStateInjectable),
requestHelmReleaseHistory: di.inject(requestHelmReleaseHistoryInjectable),
showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
}),
});
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import releaseRollbackDialogStateInjectable from "./state.injectable";
export type OpenHelmReleaseRollbackDialog = (release: HelmRelease) => void;
const openHelmReleaseRollbackDialogInjectable = getInjectable({
id: "open-helm-release-dialog",
instantiate: (di): OpenHelmReleaseRollbackDialog => {
const state = di.inject(releaseRollbackDialogStateInjectable);
return release => state.set(release);
},
});
export default openHelmReleaseRollbackDialogInjectable;
@@ -0,0 +1,14 @@
/**
* 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 { observable } from "mobx";
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
const releaseRollbackDialogStateInjectable = getInjectable({
id: "release-rollback-dialog-state",
instantiate: () => observable.box<HelmRelease | undefined>(undefined),
});
export default releaseRollbackDialogStateInjectable;
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { HelmReleases } from "./releases";
import helmReleasesRouteInjectable from "../../../common/front-end-routing/routes/cluster/helm/releases/helm-releases-route.injectable";
import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
const helmReleasesRouteComponentInjectable = getInjectable({
id: "helm-releases-route-component",
instantiate: (di) => ({
route: di.inject(helmReleasesRouteInjectable),
Component: HelmReleases,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default helmReleasesRouteComponentInjectable;
@@ -0,0 +1,24 @@
/**
* 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 { computed } from "mobx";
import helmReleasesRouteInjectable from "../../../common/front-end-routing/routes/cluster/helm/releases/helm-releases-route.injectable";
import routePathParametersInjectable from "../../routes/route-path-parameters.injectable";
const helmReleasesRouteParametersInjectable = getInjectable({
id: "helm-releases-route-parameters",
instantiate: (di) => {
const route = di.inject(helmReleasesRouteInjectable);
const pathParameters = di.inject(routePathParametersInjectable, route);
return {
namespace: computed(() => pathParameters.get().namespace),
name: computed(() => pathParameters.get().name),
};
},
});
export default helmReleasesRouteParametersInjectable;
@@ -0,0 +1,38 @@
/**
* 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 { computed } from "mobx";
import helmReleasesRouteInjectable from "../../../common/front-end-routing/routes/cluster/helm/releases/helm-releases-route.injectable";
import { helmSidebarItemId } from "../helm/helm-sidebar-items.injectable";
import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable";
import routeIsActiveInjectable from "../../routes/route-is-active.injectable";
import navigateToHelmReleasesInjectable from "../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
const helmReleasesSidebarItemsInjectable = getInjectable({
id: "helm-releases-sidebar-items",
instantiate: (di) => {
const route = di.inject(helmReleasesRouteInjectable);
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
const routeIsActive = di.inject(routeIsActiveInjectable, route);
return computed(() => [
{
id: "releases",
parentId: helmSidebarItemId,
title: "Releases",
onClick: navigateToHelmReleases,
isActive: routeIsActive,
isVisible: route.isEnabled,
orderNumber: 20,
},
]);
},
injectionToken: sidebarItemsInjectionToken,
});
export default helmReleasesSidebarItemsInjectable;
@@ -0,0 +1,6 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./releases";
@@ -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 } from "@ogre-tools/injectable";
import { computed } from "mobx";
import { clusterFrameChildComponentInjectionToken } from "../../../frames/cluster-frame/cluster-frame-child-component-injection-token";
import { ReleaseDetails } from "./release-details";
import targetHelmReleaseInjectable from "./target-helm-release.injectable";
const releaseDetailsClusterFrameChildComponentInjectable = getInjectable({
id: "release-details-cluster-frame-child-component",
instantiate: (di) => {
const targetRelease = di.inject(targetHelmReleaseInjectable);
return {
id: "release-details",
Component: ReleaseDetails,
shouldRender: computed(() => !!targetRelease.get()),
};
},
injectionToken: clusterFrameChildComponentInjectionToken,
});
export default releaseDetailsClusterFrameChildComponentInjectable;
@@ -0,0 +1,193 @@
/**
* 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 { Link } from "react-router-dom";
import { DrawerItem, DrawerTitle } from "../../drawer";
import { stopPropagation } from "@k8slens/utilities";
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";
import releaseDetailsModelInjectable from "./release-details-model/release-details-model.injectable";
import { Button } from "@k8slens/button";
import { kebabCase } from "lodash/fp";
import { Badge } from "../../badge";
import { SubTitle } from "../../layout/sub-title";
import { Table, TableCell, TableHead, TableRow } from "../../table";
import { Checkbox } from "../../checkbox";
import { MonacoEditor } from "../../monaco-editor";
import { Spinner } from "../../spinner";
import type { TargetHelmRelease } from "./target-helm-release.injectable";
interface ReleaseDetailsContentProps {
targetRelease: TargetHelmRelease;
}
interface Dependencies {
model: ReleaseDetailsModel;
}
const NonInjectedReleaseDetailsContent = observer(({ model }: Dependencies & ReleaseDetailsContentProps) => {
const loadingError = model.loadingError.get();
if (loadingError) {
return (
<div data-testid="helm-release-detail-error">
Failed to load release:
{" "}
{loadingError}
</div>
);
}
return (
<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"
/>
</div>
</DrawerItem>
<DrawerItem name="Updated">
{`${model.release.getUpdated()} ago (${model.release.updated})`}
</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="Status"
className="status"
labelsOnly>
<Badge
label={model.release.getStatus()}
className={kebabCase(model.release.getStatus())}
/>
</DrawerItem>
<ReleaseValues
releaseId={model.id}
configuration={model.configuration}
onlyUserSuppliedValuesAreShown={
model.onlyUserSuppliedValuesAreShown
}
/>
<DrawerTitle>Notes</DrawerTitle>
{model.notes && <div className="notes">{model.notes}</div>}
<DrawerTitle>Resources</DrawerTitle>
{model.groupedResources.length > 0 && (
<div className="resources">
{model.groupedResources.map((group) => (
<ResourceGroup key={group.kind} group={group} />
))}
</div>
)}
</div>
);
});
export const ReleaseDetailsContent = withInjectables<Dependencies, ReleaseDetailsContentProps>(NonInjectedReleaseDetailsContent, {
getPlaceholder: () => <Spinner center data-testid="helm-release-detail-content-spinner" />,
getProps: async (di, props) => ({
model: await di.inject(releaseDetailsModelInjectable, props.targetRelease),
...props,
}),
});
const ResourceGroup = ({
group: { kind, isNamespaced, resources },
}: {
group: MinimalResourceGroup;
}) => (
<>
<SubTitle title={kind} />
<Table scrollable={false}>
<TableHead sticky={false}>
<TableCell className="name">Name</TableCell>
{isNamespaced && <TableCell className="namespace">Namespace</TableCell>}
</TableHead>
{resources.map(
({ detailsUrl, name, namespace, uid }) => (
<TableRow key={uid}>
<TableCell className="name">
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
</TableCell>
{isNamespaced && (
<TableCell className="namespace">{namespace}</TableCell>
)}
</TableRow>
),
)}
</Table>
</>
);
interface ReleaseValuesProps {
releaseId: string;
configuration: ConfigurationInput;
onlyUserSuppliedValuesAreShown: OnlyUserSuppliedValuesAreShownToggle;
}
const ReleaseValues = observer(({ releaseId, configuration, onlyUserSuppliedValuesAreShown }: ReleaseValuesProps) => {
const configurationIsLoading = configuration.isLoading.get();
return (
<div className="values">
<DrawerTitle>Values</DrawerTitle>
<div className="flex column gaps">
<Checkbox
label="User-supplied values only"
value={onlyUserSuppliedValuesAreShown.value.get()}
onChange={onlyUserSuppliedValuesAreShown.toggle}
disabled={configurationIsLoading}
data-testid="user-supplied-values-only-checkbox"
/>
<MonacoEditor
id={`helm-release-configuration-${releaseId}`}
style={{ minHeight: 300 }}
value={configuration.nonSavedValue.get()}
onChange={configuration.onChange}
/>
<Button
primary
label="Save"
waiting={configuration.isSaving.get()}
disabled={configurationIsLoading}
onClick={configuration.save}
data-testid="helm-release-configuration-save-button"
/>
</div>
</div>
);
});
@@ -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 "@k8slens/utilities";
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,
}),
});
@@ -0,0 +1,45 @@
/**
* 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 apiManagerInjectable from "../../../../../common/k8s-api/api-manager/manager.injectable";
import getDetailsUrlInjectable from "../../../kube-detail-params/get-details-url.injectable";
export type GetResourceDetailsUrl = (
kind: string,
apiVersion: string,
namespace: string | undefined,
name: string
) => string;
const getResourceDetailsUrlInjectable = getInjectable({
id: "get-resource-details-url",
instantiate: (di): GetResourceDetailsUrl => {
const apiManager = di.inject(apiManagerInjectable);
const getDetailsUrl = di.inject(getDetailsUrlInjectable);
const getKubeApi = (kind: string, apiVersion: string) =>
apiManager.getApi(
(api) => api.kind === kind && api.apiVersionWithGroup == apiVersion,
);
return (kind, apiVersion, namespace, name) => {
const kubeApi = getKubeApi(kind, apiVersion);
if (!kubeApi) {
return "";
}
const resourceUrl = kubeApi.getUrl({
name,
namespace,
});
return getDetailsUrl(resourceUrl);
};
},
});
export default getResourceDetailsUrlInjectable;
@@ -0,0 +1,292 @@
/**
* 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 type { IComputedValue, IObservableValue } from "mobx";
import { runInAction, action, observable, computed } from "mobx";
import type { TargetHelmRelease } from "../target-helm-release.injectable";
import type { RequestDetailedHelmRelease, DetailedHelmRelease } from "./request-detailed-helm-release.injectable";
import requestDetailedHelmReleaseInjectable from "./request-detailed-helm-release.injectable";
import type { LensTheme } from "../../../../themes/lens-theme";
import type { RequestHelmReleaseConfiguration } from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import requestHelmReleaseConfigurationInjectable from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import { pipeline } from "@ogre-tools/fp";
import { groupBy, map } from "lodash/fp";
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api";
import type { GetResourceDetailsUrl } from "./get-resource-details-url.injectable";
import getResourceDetailsUrlInjectable from "./get-resource-details-url.injectable";
import type { RequestHelmReleaseUpdate } from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-update.injectable";
import updateReleaseInjectable from "../../update-release/update-release.injectable";
import type { ShowCheckedErrorNotification } from "../../../notifications/show-checked-error.injectable";
import showCheckedErrorNotificationInjectable from "../../../notifications/show-checked-error.injectable";
import type { ShowNotification } from "../../../notifications";
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
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 activeThemeInjectable from "../../../../themes/active.injectable";
import type { ToHelmRelease } from "../../to-helm-release.injectable";
import toHelmReleaseInjectable from "../../to-helm-release.injectable";
const releaseDetailsModelInjectable = getInjectable({
id: "release-details-model",
instantiate: async (di, targetRelease: TargetHelmRelease) => {
const model = new ReleaseDetailsModel({
requestDetailedHelmRelease: di.inject(requestDetailedHelmReleaseInjectable),
targetRelease,
activeTheme: di.inject(activeThemeInjectable),
requestHelmReleaseConfiguration: di.inject(requestHelmReleaseConfigurationInjectable),
getResourceDetailsUrl: di.inject(getResourceDetailsUrlInjectable),
updateRelease: di.inject(updateReleaseInjectable),
showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
toHelmRelease: di.inject(toHelmReleaseInjectable),
});
await model.load();
return model;
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, release: TargetHelmRelease) => `${release.namespace}/${release.name}`,
}),
});
export default releaseDetailsModelInjectable;
export interface OnlyUserSuppliedValuesAreShownToggle {
readonly value: IObservableValue<boolean>;
toggle: () => Promise<void>;
}
export interface ConfigurationInput {
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>;
requestDetailedHelmRelease: RequestDetailedHelmRelease;
requestHelmReleaseConfiguration: RequestHelmReleaseConfiguration;
getResourceDetailsUrl: GetResourceDetailsUrl;
updateRelease: RequestHelmReleaseUpdate;
showCheckedErrorNotification: ShowCheckedErrorNotification;
showSuccessNotification: ShowNotification;
createUpgradeChartTab: (release: HelmRelease) => string;
navigateToHelmReleases: NavigateToHelmReleases;
toHelmRelease: ToHelmRelease;
}
export class ReleaseDetailsModel {
readonly id = `${this.dependencies.targetRelease.namespace}/${this.dependencies.targetRelease.name}`;
constructor(protected readonly dependencies: Dependencies) {}
private readonly detailedRelease = observable.box<DetailedHelmRelease | undefined>();
readonly loadingError = observable.box<string>();
readonly configuration: ConfigurationInput = {
nonSavedValue: observable.box(""),
isLoading: observable.box(false),
isSaving: observable.box(false),
onChange: action((value: string) => {
this.configuration.nonSavedValue.set(value);
}),
save: async () => {
runInAction(() => {
this.configuration.isSaving.set(true);
});
const name = this.release.getName();
const namespace = this.release.getNs();
const data = {
chart: this.release.getChart(),
repo: await this.release.getRepo(),
version: this.release.getVersion(),
values: this.configuration.nonSavedValue.get(),
};
const result = await this.dependencies.updateRelease(name, namespace, data);
runInAction(() => {
this.configuration.isSaving.set(false);
});
if (!result.callWasSuccessful) {
this.dependencies.showCheckedErrorNotification(
result.error,
"Unknown error occured while updating release",
);
return;
}
this.dependencies.showSuccessNotification(
<p>
Release
{" "}
<b>{name}</b>
{" successfully updated!"}
</p>,
);
await this.loadConfiguration();
},
};
readonly onlyUserSuppliedValuesAreShown: OnlyUserSuppliedValuesAreShownToggle = {
value: observable.box(false),
toggle: action(async () => {
const value = this.onlyUserSuppliedValuesAreShown.value;
value.set(!value.get());
await this.loadConfiguration();
}),
};
load = async () => {
const { name, namespace } = this.dependencies.targetRelease;
const result = await this.dependencies.requestDetailedHelmRelease(
name,
namespace,
);
if (!result.callWasSuccessful) {
runInAction(() => {
this.loadingError.set(result.error);
});
return;
}
runInAction(() => {
this.detailedRelease.set(result.response);
});
await this.loadConfiguration();
};
private loadConfiguration = async () => {
runInAction(() => {
this.configuration.isLoading.set(true);
});
const { name, namespace } = this.release;
const configuration =
await this.dependencies.requestHelmReleaseConfiguration(
name,
namespace,
!this.onlyUserSuppliedValuesAreShown.value.get(),
);
runInAction(() => {
this.configuration.isLoading.set(false);
this.configuration.nonSavedValue.set(configuration);
});
};
@computed get release() {
const detailedRelease = this.detailedRelease.get();
assert(detailedRelease, "Tried to access release before load");
return this.dependencies.toHelmRelease(detailedRelease.release);
}
@computed private get details() {
const detailedRelease = this.detailedRelease.get();
assert(detailedRelease, "Tried to access details before load");
return detailedRelease.details;
}
@computed get notes() {
return this.details?.info.notes ?? "";
}
@computed get groupedResources(): MinimalResourceGroup[] {
return pipeline(
this.details?.resources ?? [],
groupBy((resource) => resource.kind),
(grouped) => Object.entries(grouped),
map(([kind, resources]) => ({
kind,
resources: resources.map(
toMinimalResourceFor(this.dependencies.getResourceDetailsUrl, kind),
),
isNamespaced: resources.some(
(resource) => !!resource.metadata.namespace,
),
})),
);
}
@computed get activeTheme() {
return this.dependencies.activeTheme.get().type;
}
close = () => {
this.dependencies.navigateToHelmReleases();
};
startUpgradeProcess = () => {
this.dependencies.createUpgradeChartTab(this.release);
this.dependencies.navigateToHelmReleases();
};
}
export interface MinimalResourceGroup {
kind: string;
isNamespaced: boolean;
resources: MinimalResource[];
}
export interface MinimalResource {
uid: string | undefined;
name: string;
namespace: string | undefined;
detailsUrl: string | undefined;
}
const toMinimalResourceFor =
(getResourceDetailsUrl: GetResourceDetailsUrl, kind: string) =>
(resource: KubeJsonApiData): MinimalResource => {
const { name, namespace, uid } = resource.metadata;
return {
uid,
name,
namespace,
detailsUrl: getResourceDetailsUrl(
kind,
resource.apiVersion,
namespace,
name,
),
};
};
@@ -0,0 +1,51 @@
/**
* 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 { HelmReleaseDto } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
import requestHelmReleasesInjectable from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import type { HelmReleaseDetails } from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-details.injectable";
import requestHelmReleaseDetailsInjectable from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-details.injectable";
import type { AsyncResult } from "@k8slens/utilities";
export interface DetailedHelmRelease {
release: HelmReleaseDto;
details?: HelmReleaseDetails;
}
export type RequestDetailedHelmRelease = (
name: string,
namespace: string
) => AsyncResult<DetailedHelmRelease>;
const requestDetailedHelmReleaseInjectable = getInjectable({
id: "request-detailed-helm-release",
instantiate: (di): RequestDetailedHelmRelease => {
const requestHelmReleases = di.inject(requestHelmReleasesInjectable);
const requestHelmReleaseDetails = di.inject(requestHelmReleaseDetailsInjectable);
return async (name, namespace) => {
const [releases, details] = await Promise.all([
requestHelmReleases(namespace),
requestHelmReleaseDetails(name, namespace),
]);
const release = releases.find(
(rel) => rel.name === name && rel.namespace === namespace,
);
if (!release) {
return {
callWasSuccessful: false,
error: `Release ${name} didn't exist in ${namespace} namespace.`,
};
}
return { callWasSuccessful: true, response: { release, details }};
};
},
});
export default requestDetailedHelmReleaseInjectable;
@@ -0,0 +1,79 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
@import "../release.mixins";
.ReleaseDetails {
.DrawerItem {
align-items: center;
}
.loading-error {
color: red;
text-align: center;
}
.status {
@include release-status-bgs;
}
.chart {
.upgrade {
min-width: 150px;
}
}
.resources {
.SubTitle {
text-transform: none;
padding-left: 0;
}
.Table {
margin-bottom: $margin * 4;
border: 1px solid var(--drawerSubtitleBackground);
border-radius: $radius;
overflow: auto;
.TableHead {
border-bottom: none;
.TableCell {
background-color: var(--drawerSubtitleBackground);
word-break: unset;
}
}
.TableCell {
text-overflow: unset;
word-break: break-word;
&.name {
flex-grow: 3;
}
&.namespace {
flex-grow: 1;
}
&.age {
flex-basis: 100px;
flex-grow: 0;
flex-shrink: 0;
}
}
}
}
.notes {
white-space: pre-line;
font-family: var(--font-monospace);
font-size: small;
}
.values + .Button {
align-self: flex-start;
}
}
@@ -0,0 +1,39 @@
/**
* 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 { IComputedValue } from "mobx";
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<
TargetHelmRelease | undefined
>;
}
const NonInjectedReleaseDetails = observer(
({ targetRelease }: Dependencies) => {
const release = targetRelease.get();
return release ? <ReleaseDetailsDrawer targetRelease={release} /> : null;
},
);
export const ReleaseDetails = withInjectables<Dependencies>(
NonInjectedReleaseDetails,
{
getProps: (di) => ({
targetRelease: di.inject(targetHelmReleaseInjectable),
}),
},
);
@@ -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 } from "@ogre-tools/injectable";
import { computed } from "mobx";
import helmReleasesRouteParametersInjectable from "../helm-releases-route-parameters.injectable";
export interface TargetHelmRelease { name: string; namespace: string }
const targetHelmReleaseInjectable = getInjectable({
id: "target-helm-release",
instantiate: (di) => {
const routeParameters = di.inject(helmReleasesRouteParametersInjectable);
return computed((): TargetHelmRelease | undefined => {
const name = routeParameters.name.get();
const namespace = routeParameters.namespace.get();
return name && namespace ? { name, namespace } : undefined;
});
},
});
export default targetHelmReleaseInjectable;
@@ -0,0 +1,114 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { cssNames } from "@k8slens/utilities";
import type { MenuActionsProps } from "../menu/menu-actions";
import { MenuActions } from "../menu/menu-actions";
import { MenuItem } from "../menu";
import { Icon } from "../icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import createUpgradeChartTabInjectable from "../dock/upgrade-chart/create-upgrade-chart-tab.injectable";
import deleteReleaseInjectable from "./delete-release/delete-release.injectable";
import type { OpenHelmReleaseRollbackDialog } from "./dialog/open.injectable";
import openHelmReleaseRollbackDialogInjectable from "./dialog/open.injectable";
export interface HelmReleaseMenuProps extends MenuActionsProps {
release: HelmRelease;
hideDetails?(): void;
}
interface Dependencies {
deleteRelease: (release: HelmRelease) => Promise<any>;
createUpgradeChartTab: (release: HelmRelease) => void;
openRollbackDialog: OpenHelmReleaseRollbackDialog;
}
class NonInjectedHelmReleaseMenu extends React.Component<HelmReleaseMenuProps & Dependencies> {
remove = () => {
return this.props.deleteRelease(this.props.release);
};
upgrade = () => {
const { release, hideDetails } = this.props;
this.props.createUpgradeChartTab(release);
hideDetails?.();
};
rollback = () => {
this.props.openRollbackDialog(this.props.release);
};
renderContent() {
const { release, toolbar } = this.props;
if (!release) return null;
const hasRollback = release && release.getRevision() > 1;
return (
<>
{hasRollback && (
<MenuItem onClick={this.rollback}>
<Icon
material="history"
interactive={toolbar}
tooltip="Rollback"
/>
<span className="title">Rollback</span>
</MenuItem>
)}
<MenuItem
onClick={this.upgrade}
data-testid={`upgrade-chart-menu-item-for-${release.getId()}`}
>
<Icon
material="refresh"
interactive={toolbar}
tooltip="Upgrade"
/>
<span className="title">Upgrade</span>
</MenuItem>
</>
);
}
render() {
const { className, release, ...menuProps } = this.props;
return (
<MenuActions
id={`menu-actions-for-release-menu-for-${release.getId()}`}
triggerIcon={{
material: "more_vert",
"data-testid": `menu-actions-icon-for-release-menu-for-${release.getId()}`,
}}
{...menuProps}
className={cssNames("HelmReleaseMenu", className)}
removeAction={this.remove}
removeConfirmationMessage={() => (
<p>
Remove Helm Release
{" "}
<b>{release.name}</b>
?
</p>
)}
>
{this.renderContent()}
</MenuActions>
);
}
}
export const HelmReleaseMenu = withInjectables<Dependencies, HelmReleaseMenuProps>(NonInjectedHelmReleaseMenu, {
getProps: (di, props) => ({
...props,
deleteRelease: di.inject(deleteReleaseInjectable),
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
openRollbackDialog: di.inject(openHelmReleaseRollbackDialogInjectable),
}),
});
@@ -0,0 +1,37 @@
/**
* 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 { computed, onBecomeObserved, onBecomeUnobserved } from "mobx";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import secretStoreInjectable from "../config-secrets/store.injectable";
const releaseSecretsInjectable = getInjectable({
id: "release-secrets",
instantiate: (di) => {
const subscribeStores = di.inject(subscribeStoresInjectable);
const secretStore = di.inject(secretStoreInjectable);
const releaseSecrets = computed(() =>
secretStore.contextItems.filter((secret) =>
secret.type.startsWith("helm.sh/release"),
),
);
let unsubscribe: () => void;
onBecomeObserved(releaseSecrets, () => {
unsubscribe = subscribeStores([secretStore]);
});
onBecomeUnobserved(releaseSecrets, () => {
unsubscribe?.();
});
return releaseSecrets;
},
});
export default releaseSecretsInjectable;
@@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
$release-status-color-list: (
deployed: var(--colorSuccess),
failed: var(--colorError),
deleting: var(--colorWarning),
pendingInstall: var(--colorInfo),
pendingUpgrade: var(--colorInfo),
pendingRollback: var(--colorInfo),
);
@mixin release-status-bgs {
@each $status, $color in $release-status-color-list {
&.#{$status} {
color: white;
background: $color;
}
}
}
@mixin release-status-colors {
@each $status, $color in $release-status-color-list {
&.#{$status} {
color: $color;
}
}
}
@@ -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 } from "@ogre-tools/injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable";
import releaseSecretsInjectable from "./release-secrets.injectable";
import requestHelmReleasesInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import toHelmReleaseInjectable from "./to-helm-release.injectable";
const releasesInjectable = getInjectable({
id: "releases",
instantiate: (di) => {
const clusterContext = di.inject(clusterFrameContextForNamespacedResourcesInjectable);
const releaseSecrets = di.inject(releaseSecretsInjectable);
const requestHelmReleases = di.inject(requestHelmReleasesInjectable);
const toHelmRelease = di.inject(toHelmReleaseInjectable);
return asyncComputed({
getValueFromObservedPromise: async () => {
void releaseSecrets.get();
const releaseArrays = await (
clusterContext.hasSelectedAll
? requestHelmReleases()
: Promise.all(clusterContext.contextNamespaces.map((namespace) => requestHelmReleases(namespace)))
);
return releaseArrays.flat().map(toHelmRelease);
},
valueWhenPending: [],
});
},
});
export default releasesInjectable;
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
@import "release.mixins";
.HelmReleases {
.TableCell {
&.status {
@include release-status-colors;
}
&.version {
> .Icon {
margin-left: $margin;
&.new-version {
color: var(--colorInfo);
}
}
}
}
}
@@ -0,0 +1,216 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* 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 type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { ItemListStore } from "../item-object-list";
import { ItemListLayout } from "../item-object-list";
import { NamespaceSelectFilter } from "../namespaces/namespace-select-filter";
import { kebabCase } from "lodash/fp";
import { HelmReleaseMenu } from "./release-menu";
import { ReleaseRollbackDialog } from "./dialog/dialog";
import removableReleasesInjectable from "./removable-releases.injectable";
import type { RemovableHelmRelease } from "./removable-releases";
import type { IComputedValue } from "mobx";
import releasesInjectable from "./releases.injectable";
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
import helmReleasesRouteParametersInjectable from "./helm-releases-route-parameters.injectable";
import type { NavigateToHelmReleases } from "../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import navigateToHelmReleasesInjectable from "../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import { NamespaceSelectBadge } from "../namespaces/namespace-select-badge";
enum columnId {
name = "name",
namespace = "namespace",
revision = "revision",
chart = "chart",
version = "version",
appVersion = "app-version",
status = "status",
updated = "update",
}
interface Dependencies {
releases: IComputedValue<RemovableHelmRelease[]>;
releasesArePending: IComputedValue<boolean>;
namespace: IComputedValue<string>;
navigateToHelmReleases: NavigateToHelmReleases;
}
class NonInjectedHelmReleases extends Component<Dependencies> {
onDetails = (item: HelmRelease) => {
this.showDetails(item);
};
showDetails = (item: HelmRelease) => {
this.props.navigateToHelmReleases({
name: item.getName(),
namespace: item.getNs(),
});
};
hideDetails = () => {
this.props.navigateToHelmReleases();
};
renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
const releaseNames = selectedItems.map(item => item.getName()).join(", ");
return (
<div>
<>
Remove
{" "}
<b>{releaseNames}</b>
?
</>
<p className="warning">
Note: StatefulSet Volumes won&apos;t be deleted automatically
</p>
</div>
);
}
render() {
const releases = this.props.releases;
const releasesArePending = this.props.releasesArePending;
// TODO: Implement ItemListLayout without stateful stores
const legacyReleaseStore: ItemListStore<RemovableHelmRelease, false> = {
get isLoaded() {
return !releasesArePending.get();
},
failedLoading: false,
getTotalCount: () => releases.get().length,
toggleSelection: (release) => release.toggle(),
isSelectedAll: (releases) => (
releases.length > 0
&& releases.every((release) => release.isSelected)
),
toggleSelectionAll: (releases) => {
let selected = false;
if (!legacyReleaseStore.isSelectedAll(releases)) {
selected = true;
}
for (const release of releases) {
if (release.isSelected !== selected) {
release.toggle();
}
}
},
isSelected: (release) => release.isSelected,
removeSelectedItems: async () => {
await Promise.all(
releases.get()
.filter((release) => release.isSelected)
.map(release => release.delete()),
);
},
pickOnlySelected: (releases) => releases.filter(release => release.isSelected),
};
return (
<SiblingsInTabLayout>
<ItemListLayout<RemovableHelmRelease, false>
store={legacyReleaseStore}
getItems={() => releases.get()}
preloadStores={false}
isConfigurable
tableId="helm_releases"
className="HelmReleases"
customizeTableRowProps={(item) => ({ testId: `helm-release-row-for-${item.getId()}` })}
sortingCallbacks={{
[columnId.name]: release => release.getName(),
[columnId.namespace]: release => release.getNs(),
[columnId.revision]: release => release.getRevision(),
[columnId.chart]: release => release.getChart(),
[columnId.status]: release => release.getStatus(),
[columnId.updated]: release => release.getUpdated(false, false),
}}
searchFilters={[
release => release.getName(),
release => release.getNs(),
release => release.getChart(),
release => release.getStatus(),
release => release.getVersion(),
]}
customizeHeader={({ filters, searchProps, ...headerPlaceholders }) => ({
filters: (
<>
{filters}
<NamespaceSelectFilter id="namespace-select-filter" />
</>
),
searchProps: {
...searchProps,
placeholder: "Search Releases...",
},
...headerPlaceholders,
})}
renderHeaderTitle="Releases"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
{ title: "Chart", className: "chart", sortBy: columnId.chart, id: columnId.chart },
{ title: "Revision", className: "revision", sortBy: columnId.revision, id: columnId.revision },
{ title: "Version", className: "version", id: columnId.version },
{ title: "App Version", className: "app-version", id: columnId.appVersion },
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
{ title: "Updated", className: "updated", sortBy: columnId.updated, id: columnId.updated },
]}
renderTableContents={release => [
release.getName(),
<NamespaceSelectBadge
key="namespace"
namespace={release.getNs()}
/>,
release.getChart(),
release.getRevision(),
release.getVersion(),
release.appVersion,
{ title: release.getStatus(), className: kebabCase(release.getStatus()) },
release.getUpdated(),
]}
renderItemMenu={release => (
<HelmReleaseMenu
release={release}
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
/>
)}
customizeRemoveDialog={selectedItems => ({
message: this.renderRemoveDialogMessage(selectedItems),
})}
onDetails={this.onDetails}
spinnerTestId="helm-releases-spinner"
/>
<ReleaseRollbackDialog/>
</SiblingsInTabLayout>
);
}
}
export const HelmReleases = withInjectables<Dependencies>(NonInjectedHelmReleases, {
getProps: (di) => ({
releases: di.inject(removableReleasesInjectable),
releasesArePending: di.inject(releasesInjectable).pending,
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
...di.inject(helmReleasesRouteParametersInjectable),
}),
});
@@ -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 } 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({
id: "removable-releases",
instantiate: (di) =>
removableReleases({
releases: di.inject(releasesInjectable),
deleteRelease: di.inject(deleteReleaseInjectable),
releaseSelectionStatus: observable.map<string, boolean>(),
}),
});
export default removableReleasesInjectable;
@@ -0,0 +1,49 @@
/**
* 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 type { ObservableMap } from "mobx";
import { computed } 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,26 @@
/**
* 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 requestHelmReleaseRollbackInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-rollback.injectable";
import releasesInjectable from "../releases.injectable";
export type RollbackRelease = (name: string, namespace: string, revision: number) => Promise<void>;
const rollbackReleaseInjectable = getInjectable({
id: "rollback-release",
instantiate: (di): RollbackRelease => {
const releases = di.inject(releasesInjectable);
const requestHelmReleaseRollback = di.inject(requestHelmReleaseRollbackInjectable);
return async (name, namespace, revision) => {
await requestHelmReleaseRollback(name, namespace, revision);
releases.invalidate();
};
},
});
export default rollbackReleaseInjectable;
@@ -0,0 +1,90 @@
/**
* 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 { capitalize } from "lodash";
import { when } from "mobx";
import helmChartVersionsInjectable from "../helm-charts/helm-charts/versions.injectable";
import type { HelmRelease, HelmReleaseDto } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { getMillisecondsFromUnixEpoch } from "../../../common/utils/date/get-current-date-time";
import { formatDuration } from "@k8slens/utilities";
export type ToHelmRelease = (release: HelmReleaseDto) => HelmRelease;
const toHelmReleaseInjectable = getInjectable({
id: "to-helm-release",
instantiate: (di): ToHelmRelease => {
const helmChartVersions = (release: HelmRelease) => di.inject(helmChartVersionsInjectable, release);
return (release) => ({
...release,
getId() {
return `${this.namespace}/${this.name}`;
},
getName() {
return this.name;
},
getNs() {
return this.namespace;
},
getChart(withVersion = false) {
let chart = this.chart;
if (!withVersion && this.getVersion() != "") {
const search = new RegExp(`-${this.getVersion()}`);
chart = chart.replace(search, "");
}
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 updatedDate = new Date(updated).getTime();
const diff = getMillisecondsFromUnixEpoch() - updatedDate;
if (humanize) {
return formatDuration(diff, compact);
}
return diff;
},
// Helm does not store from what repository the release is installed,
// so we have to try to guess it by searching charts
async getRepo() {
const versionsComputed = helmChartVersions(this);
const version = this.getVersion();
await when(() => !versionsComputed.pending.get());
return versionsComputed.value
.get()
.find((chartVersion) => chartVersion.version === version)?.repo
?? "";
},
});
},
});
export default toHelmReleaseInjectable;
@@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import releasesInjectable from "../releases.injectable";
import type { RequestHelmReleaseUpdate } from "../../../../common/k8s-api/endpoints/helm-releases.api/request-update.injectable";
import requestHelmReleaseUpdateInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-update.injectable";
const updateReleaseInjectable = getInjectable({
id: "update-release",
instantiate: (di): RequestHelmReleaseUpdate => {
const releases = di.inject(releasesInjectable);
const requestHelmReleaseUpdate = di.inject(requestHelmReleaseUpdateInjectable);
return async (
name,
namespace,
payload,
) => {
const result = await requestHelmReleaseUpdate(name, namespace, payload);
releases.invalidate();
return result;
};
},
});
export default updateReleaseInjectable;