mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
1 parent
e810eab39f
commit
b7974827d2
786 files changed
+14567
-24066
No files matched your search
@@ -0,0 +1,2 @@
|
||||
export * from "./releases";
|
||||
export * from "./release.route";
|
||||
@@ -0,0 +1,86 @@
|
||||
@import "release.mixins";
|
||||
|
||||
.ReleaseDetails {
|
||||
&.light {
|
||||
.AceEditor {
|
||||
border: 1px solid gainsboro;
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
.DrawerItem {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status {
|
||||
.Badge {
|
||||
@include release-status-bgs;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
@include custom-scrollbar();
|
||||
|
||||
.TableHead {
|
||||
border-bottom: none;
|
||||
|
||||
.TableCell {
|
||||
background-color: var(--drawerSubtitleBackground);
|
||||
word-break: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
text-overflow: unset;
|
||||
word-break: break-word;
|
||||
min-width: 100px;
|
||||
|
||||
&.name {
|
||||
flex-basis: auto;
|
||||
flex-grow: 0;
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
&.volume {
|
||||
flex-basis: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notes {
|
||||
white-space: pre-line;
|
||||
font-family: "RobotoMono", monospace;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.values {
|
||||
.AceEditor {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.AceEditor + .Button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import "./release-details.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { Link } from "react-router-dom";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { HelmRelease, helmReleasesApi, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
|
||||
import { HelmReleaseMenu } from "./release-menu";
|
||||
import { Drawer, DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { cssNames, stopPropagation } from "../../utils";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Spinner } from "../spinner";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { AceEditor } from "../ace-editor";
|
||||
import { Button } from "../button";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { Notifications } from "../notifications";
|
||||
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Secret } from "../../api/endpoints";
|
||||
|
||||
interface Props {
|
||||
release: HelmRelease;
|
||||
hideDetails(): void;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ReleaseDetails extends Component<Props> {
|
||||
@observable details: IReleaseDetails;
|
||||
@observable values = "";
|
||||
@observable saving = false;
|
||||
@observable releaseSecret: Secret;
|
||||
|
||||
@disposeOnUnmount
|
||||
releaseSelector = reaction(() => this.props.release, release => {
|
||||
if (!release) return;
|
||||
this.loadDetails();
|
||||
this.loadValues();
|
||||
this.releaseSecret = null;
|
||||
}
|
||||
);
|
||||
|
||||
@disposeOnUnmount
|
||||
secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
|
||||
if (!this.props.release) return;
|
||||
const { getReleaseSecret } = releaseStore;
|
||||
const { release } = this.props;
|
||||
const secret = getReleaseSecret(release);
|
||||
if (this.releaseSecret) {
|
||||
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
|
||||
this.loadDetails();
|
||||
}
|
||||
this.releaseSecret = secret;
|
||||
});
|
||||
|
||||
async loadDetails() {
|
||||
const { release } = this.props;
|
||||
this.details = null;
|
||||
this.details = await helmReleasesApi.get(release.getName(), release.getNs());
|
||||
}
|
||||
|
||||
async loadValues() {
|
||||
const { release } = this.props;
|
||||
this.values = "";
|
||||
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs());
|
||||
}
|
||||
|
||||
updateValues = async () => {
|
||||
const { release } = this.props;
|
||||
const name = release.getName();
|
||||
const namespace = release.getNs()
|
||||
const data = {
|
||||
chart: release.getChart(),
|
||||
repo: await release.getRepo(),
|
||||
version: release.getVersion(),
|
||||
values: this.values
|
||||
};
|
||||
this.saving = true;
|
||||
try {
|
||||
await releaseStore.update(name, namespace, data);
|
||||
Notifications.ok(
|
||||
<p>Release <b>{name}</b> successfully updated!</p>
|
||||
);
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
upgradeVersion = () => {
|
||||
const { release, hideDetails } = this.props;
|
||||
createUpgradeChartTab(release);
|
||||
hideDetails();
|
||||
}
|
||||
|
||||
renderValues() {
|
||||
const { values, saving } = this;
|
||||
return (
|
||||
<div className="values">
|
||||
<DrawerTitle title={_i18n._(t`Values`)}/>
|
||||
<div className="flex column gaps">
|
||||
<AceEditor
|
||||
mode="yaml"
|
||||
value={values}
|
||||
onChange={values => this.values = values}
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
label={_i18n._(t`Save`)}
|
||||
waiting={saving}
|
||||
onClick={this.updateValues}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderNotes() {
|
||||
if (!this.details.info?.notes) return null;
|
||||
const { notes } = this.details.info;
|
||||
return (
|
||||
<div className="notes">
|
||||
{notes}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderResources() {
|
||||
const { resources } = this.details;
|
||||
if (!resources) return null;
|
||||
const groups = groupBy(resources, item => item.kind);
|
||||
const tables = Object.entries(groups).map(([kind, items]) => {
|
||||
return (
|
||||
<React.Fragment key={kind}>
|
||||
<SubTitle title={kind}/>
|
||||
<Table scrollable={false}>
|
||||
<TableHead sticky={false}>
|
||||
<TableCell className="name">Name</TableCell>
|
||||
{items[0].getNs() && <TableCell className="namespace">Namespace</TableCell>}
|
||||
<TableCell className="age">Age</TableCell>
|
||||
</TableHead>
|
||||
{items.map(item => {
|
||||
const name = item.getName();
|
||||
const namespace = item.getNs();
|
||||
const api = apiManager.getApi(item.metadata.selfLink);
|
||||
const detailsUrl = api ? getDetailsUrl(api.getUrl({
|
||||
name,
|
||||
namespace,
|
||||
})) : "";
|
||||
return (
|
||||
<TableRow key={item.getId()}>
|
||||
<TableCell className="name">
|
||||
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
|
||||
</TableCell>
|
||||
{namespace && <TableCell className="namespace">{namespace}</TableCell>}
|
||||
<TableCell className="age">{item.getAge()}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="resources">
|
||||
{tables}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { release } = this.props;
|
||||
const { details } = this;
|
||||
if (!release) return null;
|
||||
if (!details) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<DrawerItem name={<Trans>Chart</Trans>} className="chart">
|
||||
<div className="flex gaps align-center">
|
||||
<span>{release.getChart()}</span>
|
||||
<Button
|
||||
primary
|
||||
label={_i18n._(t`Upgrade`)}
|
||||
className="box right upgrade"
|
||||
onClick={this.upgradeVersion}
|
||||
/>
|
||||
</div>
|
||||
</DrawerItem>
|
||||
<DrawerItem name={<Trans>Updated</Trans>}>
|
||||
{release.getUpdated()} <Trans>ago</Trans> ({release.updated})
|
||||
</DrawerItem>
|
||||
<DrawerItem name={<Trans>Namespace</Trans>}>
|
||||
{release.getNs()}
|
||||
</DrawerItem>
|
||||
<DrawerItem name={<Trans>Version</Trans>} onClick={stopPropagation}>
|
||||
<div className="version flex gaps align-center">
|
||||
<span>
|
||||
{release.getVersion()}
|
||||
</span>
|
||||
</div>
|
||||
</DrawerItem>
|
||||
<DrawerItem name={<Trans>Status</Trans>} className="status" labelsOnly>
|
||||
<Badge
|
||||
label={release.getStatus()}
|
||||
className={cssNames("status", kebabCase(release.getStatus()))}
|
||||
/>
|
||||
</DrawerItem>
|
||||
{this.renderValues()}
|
||||
<DrawerTitle title={_i18n._(t`Notes`)}/>
|
||||
{this.renderNotes()}
|
||||
<DrawerTitle title={_i18n._(t`Resources`)}/>
|
||||
{this.renderResources()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { release, hideDetails } = this.props
|
||||
const title = release ? <Trans>Release: {release.getName()}</Trans> : ""
|
||||
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>
|
||||
return (
|
||||
<Drawer
|
||||
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
|
||||
usePortal={true}
|
||||
open={!!release}
|
||||
title={title}
|
||||
onClose={hideDetails}
|
||||
toolbar={toolbar}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { HelmRelease } from "../../api/endpoints/helm-releases.api";
|
||||
import { autobind, cssNames } from "../../utils";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
|
||||
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
|
||||
import { _i18n } from "../../i18n";
|
||||
|
||||
interface Props extends MenuActionsProps {
|
||||
release: HelmRelease;
|
||||
hideDetails?(): void;
|
||||
}
|
||||
|
||||
export class HelmReleaseMenu extends React.Component<Props> {
|
||||
@autobind()
|
||||
remove() {
|
||||
return releaseStore.remove(this.props.release);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
upgrade() {
|
||||
const { release, hideDetails } = this.props;
|
||||
createUpgradeChartTab(release);
|
||||
hideDetails && hideDetails();
|
||||
}
|
||||
|
||||
@autobind()
|
||||
rollback() {
|
||||
ReleaseRollbackDialog.open(this.props.release);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { release, toolbar } = this.props;
|
||||
if (!release) return;
|
||||
const hasRollback = release && release.getRevision() > 1;
|
||||
return (
|
||||
<>
|
||||
{hasRollback && (
|
||||
<MenuItem onClick={this.rollback}>
|
||||
<Icon material="history" interactive={toolbar} title={_i18n._(t`Rollback`)}/>
|
||||
<span className="title"><Trans>Rollback</Trans></span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, release, ...menuProps } = this.props;
|
||||
return (
|
||||
<MenuActions
|
||||
{...menuProps}
|
||||
className={cssNames("HelmReleaseMenu", className)}
|
||||
removeAction={this.remove}
|
||||
children={this.renderContent()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.ReleaseRollbackDialog {
|
||||
.WizardStep {
|
||||
text-align: center;
|
||||
|
||||
.step-content {
|
||||
padding: var(--wizard-spacing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import "./release-rollback-dialog.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { HelmRelease, helmReleasesApi, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { Notifications } from "../notifications";
|
||||
import orderBy from "lodash/orderBy"
|
||||
|
||||
interface Props extends DialogProps {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
@observable static isOpen = false;
|
||||
@observable.ref static release: HelmRelease = null;
|
||||
|
||||
@observable isLoading = false;
|
||||
@observable revision: IReleaseRevision;
|
||||
@observable revisions = observable.array<IReleaseRevision>();
|
||||
|
||||
static open(release: HelmRelease) {
|
||||
ReleaseRollbackDialog.isOpen = true;
|
||||
ReleaseRollbackDialog.release = release;
|
||||
}
|
||||
|
||||
static close() {
|
||||
ReleaseRollbackDialog.isOpen = false;
|
||||
}
|
||||
|
||||
get release(): HelmRelease {
|
||||
return ReleaseRollbackDialog.release;
|
||||
}
|
||||
|
||||
onOpen = async () => {
|
||||
this.isLoading = true;
|
||||
const currentRevision = this.release.getRevision();
|
||||
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs());
|
||||
releases = releases.filter(item => item.revision !== currentRevision); // remove current
|
||||
releases = orderBy(releases, "revision", "desc"); // sort
|
||||
this.revisions.replace(releases);
|
||||
this.revision = this.revisions[0];
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
rollback = async () => {
|
||||
const revisionNumber = this.revision.revision;
|
||||
try {
|
||||
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber);
|
||||
this.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
close = () => {
|
||||
ReleaseRollbackDialog.close();
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { revision, revisions } = this;
|
||||
if (!revision) {
|
||||
return <p><Trans>No revisions to rollback.</Trans></p>
|
||||
}
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<b><Trans>Revision</Trans></b>
|
||||
<Select
|
||||
themeName="light"
|
||||
value={revision}
|
||||
options={revisions}
|
||||
formatOptionLabel={({ value }: SelectOption<IReleaseRevision>) => `${value.revision} - ${value.chart}`}
|
||||
onChange={({ value }: SelectOption<IReleaseRevision>) => this.revision = value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
const releaseName = this.release ? this.release.getName() : "";
|
||||
const header = <h5><Trans>Rollback <b>{releaseName}</b></Trans></h5>
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="ReleaseRollbackDialog"
|
||||
isOpen={ReleaseRollbackDialog.isOpen}
|
||||
onOpen={this.onOpen}
|
||||
close={this.close}
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<WizardStep
|
||||
scrollable={false}
|
||||
nextLabel={<Trans>Rollback</Trans>}
|
||||
next={this.rollback}
|
||||
loading={this.isLoading}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
$release-status-color-list: (
|
||||
deployed: $colorSuccess,
|
||||
failed: $colorError,
|
||||
deleting: $colorWarning,
|
||||
pendingInstall: $colorInfo,
|
||||
pendingUpgrade: $colorInfo,
|
||||
pendingRollback: $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,14 @@
|
||||
import { RouteProps } from "react-router"
|
||||
import { appsRoute } from "../+apps/apps.route";
|
||||
import { buildURL } from "../../navigation";
|
||||
|
||||
export const releaseRoute: RouteProps = {
|
||||
path: appsRoute.path + "/releases/:namespace?/:name?"
|
||||
}
|
||||
|
||||
export interface IReleaseRouteParams {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export const releaseURL = buildURL<IReleaseRouteParams>(releaseRoute.path);
|
||||
@@ -0,0 +1,112 @@
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { action, observable, when, IReactionDisposer, reaction } from "mobx";
|
||||
import { autobind } from "../../utils";
|
||||
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
|
||||
import { ItemStore } from "../../item.store";
|
||||
import { configStore } from "../../config.store";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Secret } from "../../api/endpoints";
|
||||
|
||||
@autobind()
|
||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
@observable releaseSecrets: Secret[] = [];
|
||||
@observable secretWatcher: IReactionDisposer;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
when(() => secretsStore.isLoaded, () => {
|
||||
this.releaseSecrets = this.getReleaseSecrets();
|
||||
});
|
||||
}
|
||||
|
||||
watch() {
|
||||
this.secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
|
||||
if (this.isLoading) return;
|
||||
const secrets = this.getReleaseSecrets();
|
||||
const amountChanged = secrets.length !== this.releaseSecrets.length;
|
||||
const labelsChanged = this.releaseSecrets.some(item => {
|
||||
const secret = secrets.find(secret => secret.getId() == item.getId());
|
||||
if (!secret) return;
|
||||
return !isEqual(item.getLabels(), secret.getLabels());
|
||||
});
|
||||
if (amountChanged || labelsChanged) {
|
||||
this.loadAll();
|
||||
}
|
||||
this.releaseSecrets = [...secrets];
|
||||
})
|
||||
}
|
||||
|
||||
unwatch() {
|
||||
this.secretWatcher();
|
||||
}
|
||||
|
||||
getReleaseSecrets() {
|
||||
return secretsStore.getByLabel({ owner: "helm" });
|
||||
}
|
||||
|
||||
getReleaseSecret(release: HelmRelease) {
|
||||
const labels = {
|
||||
owner: "helm",
|
||||
name: release.getName()
|
||||
}
|
||||
return secretsStore.getByLabel(labels)
|
||||
.filter(secret => secret.getNs() == release.getNs())[0];
|
||||
}
|
||||
|
||||
@action
|
||||
async loadAll() {
|
||||
this.isLoading = true;
|
||||
let items;
|
||||
try {
|
||||
const { isClusterAdmin, allowedNamespaces } = configStore;
|
||||
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
|
||||
} finally {
|
||||
if (items) {
|
||||
items = this.sortItems(items);
|
||||
this.items.replace(items);
|
||||
}
|
||||
this.isLoaded = true;
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async loadItems(namespaces?: string[]) {
|
||||
if (!namespaces) {
|
||||
return helmReleasesApi.list();
|
||||
}
|
||||
else {
|
||||
return Promise
|
||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||
.then(items => items.flat());
|
||||
}
|
||||
}
|
||||
|
||||
async create(payload: IReleaseCreatePayload) {
|
||||
const response = await helmReleasesApi.create(payload);
|
||||
if (this.isLoaded) this.loadAll();
|
||||
return response;
|
||||
}
|
||||
|
||||
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
||||
const response = await helmReleasesApi.update(name, namespace, payload);
|
||||
if (this.isLoaded) this.loadAll();
|
||||
return response;
|
||||
}
|
||||
|
||||
async rollback(name: string, namespace: string, revision: number) {
|
||||
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
||||
if (this.isLoaded) this.loadAll();
|
||||
return response;
|
||||
}
|
||||
|
||||
async remove(release: HelmRelease) {
|
||||
return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.getNs()));
|
||||
}
|
||||
|
||||
async removeSelectedItems() {
|
||||
if (!this.selectedItems.length) return;
|
||||
return Promise.all(this.selectedItems.map(this.remove));
|
||||
}
|
||||
}
|
||||
|
||||
export const releaseStore = new ReleaseStore();
|
||||
@@ -0,0 +1,19 @@
|
||||
@import "release.mixins";
|
||||
|
||||
.HelmReleases {
|
||||
.TableCell {
|
||||
&.status {
|
||||
@include release-status-colors;
|
||||
}
|
||||
|
||||
&.version {
|
||||
> .Icon {
|
||||
margin-left: $margin;
|
||||
|
||||
&.new-version {
|
||||
color: $colorInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import "./releases.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { IReleaseRouteParams, releaseURL } from "./release.route";
|
||||
import { HelmRelease } from "../../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";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
revision = "revision",
|
||||
chart = "chart",
|
||||
status = "status",
|
||||
updated = "update"
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<IReleaseRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class HelmReleases extends Component<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
// Watch for secrets associated with releases and react to their changes
|
||||
releaseStore.watch();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
releaseStore.unwatch();
|
||||
}
|
||||
|
||||
get selectedRelease() {
|
||||
const { match: { params: { name, namespace } } } = this.props;
|
||||
return releaseStore.items.find(release => {
|
||||
return release.getName() == name && release.getNs() == namespace;
|
||||
});
|
||||
}
|
||||
|
||||
showDetails = (item: HelmRelease) => {
|
||||
if (!item) {
|
||||
navigation.merge(releaseURL())
|
||||
}
|
||||
else {
|
||||
navigation.merge(releaseURL({
|
||||
params: {
|
||||
name: item.getName(),
|
||||
namespace: item.getNs()
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
hideDetails = () => {
|
||||
this.showDetails(null);
|
||||
}
|
||||
|
||||
renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
|
||||
const releaseNames = selectedItems.map(item => item.getName()).join(", ");
|
||||
return (
|
||||
<div>
|
||||
<Trans>Remove <b>{releaseNames}</b>?</Trans>
|
||||
<p className="warning">
|
||||
<Trans>Note: StatefulSet Volumes won't be deleted automatically</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<ItemListLayout
|
||||
className="HelmReleases"
|
||||
store={releaseStore}
|
||||
dependentStores={[secretsStore]}
|
||||
sortingCallbacks={{
|
||||
[sortBy.name]: (release: HelmRelease) => release.getName(),
|
||||
[sortBy.namespace]: (release: HelmRelease) => release.getNs(),
|
||||
[sortBy.revision]: (release: HelmRelease) => release.getRevision(),
|
||||
[sortBy.chart]: (release: HelmRelease) => release.getChart(),
|
||||
[sortBy.status]: (release: HelmRelease) => release.getStatus(),
|
||||
[sortBy.updated]: (release: HelmRelease) => release.getUpdated(false, false),
|
||||
}}
|
||||
searchFilters={[
|
||||
(release: HelmRelease) => release.getName(),
|
||||
(release: HelmRelease) => release.getNs(),
|
||||
(release: HelmRelease) => release.getChart(),
|
||||
(release: HelmRelease) => release.getStatus(),
|
||||
(release: HelmRelease) => release.getVersion(),
|
||||
]}
|
||||
renderHeaderTitle={<Trans>Releases</Trans>}
|
||||
renderTableHeader={[
|
||||
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
|
||||
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
|
||||
{ title: <Trans>Chart</Trans>, className: "chart", sortBy: sortBy.chart },
|
||||
{ title: <Trans>Revision</Trans>, className: "revision", sortBy: sortBy.revision },
|
||||
{ title: <Trans>Version</Trans>, className: "version" },
|
||||
{ title: <Trans>App Version</Trans>, className: "app-version" },
|
||||
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
|
||||
{ title: <Trans>Updated</Trans>, className: "updated", sortBy: sortBy.updated },
|
||||
]}
|
||||
renderTableContents={(release: HelmRelease) => {
|
||||
const version = release.getVersion();
|
||||
return [
|
||||
release.getName(),
|
||||
release.getNs(),
|
||||
release.getChart(),
|
||||
release.getRevision(),
|
||||
<>
|
||||
{version}
|
||||
</>,
|
||||
release.appVersion,
|
||||
{ title: release.getStatus(), className: kebabCase(release.getStatus()) },
|
||||
release.getUpdated(),
|
||||
]
|
||||
}}
|
||||
renderItemMenu={(release: HelmRelease) => {
|
||||
return (
|
||||
<HelmReleaseMenu
|
||||
release={release}
|
||||
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
customizeRemoveDialog={(selectedItems: HelmRelease[]) => ({
|
||||
message: this.renderRemoveDialogMessage(selectedItems)
|
||||
})}
|
||||
detailsItem={this.selectedRelease}
|
||||
onDetails={this.showDetails}
|
||||
/>
|
||||
<ReleaseDetails
|
||||
release={this.selectedRelease}
|
||||
hideDetails={this.hideDetails}
|
||||
/>
|
||||
<ReleaseRollbackDialog/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user