1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Helm components should always use version information (#949)

* clean up code to catch rejections

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-09-24 09:54:26 -04:00 committed by GitHub
parent d20f890ddb
commit 5401c99298
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 112 additions and 75 deletions

View File

@ -49,4 +49,4 @@
.chart-description { .chart-description {
margin-top: $margin * 2; margin-top: $margin * 2;
} }
} }

View File

@ -3,8 +3,8 @@ import "./helm-chart-details.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api"; import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { autorun, observable } from "mobx"; import { observable, toJS } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer"; import { Drawer, DrawerItem } from "../drawer";
import { autobind, stopPropagation } from "../../utils"; import { autobind, stopPropagation } from "../../utils";
import { MarkdownViewer } from "../markdown-viewer"; import { MarkdownViewer } from "../markdown-viewer";
@ -25,39 +25,41 @@ interface Props {
export class HelmChartDetails extends Component<Props> { export class HelmChartDetails extends Component<Props> {
@observable chartVersions: HelmChart[]; @observable chartVersions: HelmChart[];
@observable selectedChart: HelmChart; @observable selectedChart: HelmChart;
@observable description: string = null; @observable readme: string = null;
@observable error: string = null;
private chartPromise: CancelablePromise<{ readme: string; versions: HelmChart[] }>; private chartPromise: CancelablePromise<{ readme: string; versions: HelmChart[] }>;
@disposeOnUnmount async componentDidMount() {
chartSelector = autorun(async () => { const { chart: { name, repo, version } } = this.props
if (!this.props.chart) return;
this.chartVersions = null;
this.selectedChart = null;
this.description = null;
this.loadChartData();
this.chartPromise.then(data => {
this.description = data.readme;
this.chartVersions = data.versions;
this.selectedChart = data.versions[0];
});
});
loadChartData(version?: string) { try {
const { chart: { name, repo } } = this.props; const { readme, versions } = await (this.chartPromise = helmChartsApi.get(repo, name, version))
if (this.chartPromise) this.chartPromise.cancel(); this.readme = readme
this.chartPromise = helmChartsApi.get(repo, name, version); this.chartVersions = versions
this.selectedChart = versions[0]
} catch (error) {
this.error = error
}
}
componentWillUnmount() {
this.chartPromise?.cancel();
} }
@autobind() @autobind()
onVersionChange(opt: SelectOption) { async onVersionChange({ value: version }: SelectOption) {
const version = opt.value;
this.selectedChart = this.chartVersions.find(chart => chart.version === version); this.selectedChart = this.chartVersions.find(chart => chart.version === version);
this.description = null; this.readme = null;
this.loadChartData(version);
this.chartPromise.then(data => { try {
this.description = data.readme this.chartPromise?.cancel();
}); const { chart: { name, repo } } = this.props;
const { readme } = await (this.chartPromise = helmChartsApi.get(repo, name, version))
this.readme = readme;
} catch (error) {
this.error = error;
}
} }
@autobind() @autobind()
@ -79,7 +81,7 @@ export class HelmChartDetails extends Component<Props> {
<div className="intro-contents box grow"> <div className="intro-contents box grow">
<div className="description flex align-center justify-space-between"> <div className="description flex align-center justify-space-between">
{selectedChart.getDescription()} {selectedChart.getDescription()}
<Button primary label={_i18n._(t`Install`)} onClick={this.install}/> <Button primary label={_i18n._(t`Install`)} onClick={this.install} />
</div> </div>
<DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}> <DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}>
<Select <Select
@ -95,12 +97,12 @@ export class HelmChartDetails extends Component<Props> {
</DrawerItem> </DrawerItem>
<DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers"> <DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers">
{selectedChart.getMaintainers().map(({ name, email, url }) => {selectedChart.getMaintainers().map(({ name, email, url }) =>
<a key={name} href={url ? url : `mailto:${email}`} target="_blank">{name}</a> <a key={name} href={url || `mailto:${email}`} target="_blank">{name}</a>
)} )}
</DrawerItem> </DrawerItem>
{selectedChart.getKeywords().length > 0 && ( {selectedChart.getKeywords().length > 0 && (
<DrawerItem name={_i18n._(t`Keywords`)} labelsOnly> <DrawerItem name={_i18n._(t`Keywords`)} labelsOnly>
{selectedChart.getKeywords().map(key => <Badge key={key} label={key}/>)} {selectedChart.getKeywords().map(key => <Badge key={key} label={key} />)}
</DrawerItem> </DrawerItem>
)} )}
</div> </div>
@ -108,14 +110,35 @@ export class HelmChartDetails extends Component<Props> {
); );
} }
renderReadme() {
if (this.readme === null) {
return <Spinner center />
}
return (
<div className="chart-description">
<MarkdownViewer markdown={this.readme} />
</div>
)
}
renderContent() { renderContent() {
if (this.selectedChart === null || this.description === null) return <Spinner center/>; if (!this.selectedChart) {
return <Spinner center />;
}
if (this.error) {
return (
<div className="box grow">
<p className="error">{this.error}</p>
</div>
)
}
return ( return (
<div className="box grow"> <div className="box grow">
{this.renderIntroduction()} {this.renderIntroduction()}
<div className="chart-description"> {this.renderReadme()}
<MarkdownViewer markdown={this.description}/>
</div>
</div> </div>
); );
} }
@ -135,4 +158,4 @@ export class HelmChartDetails extends Component<Props> {
</Drawer> </Drawer>
); );
} }
} }

View File

@ -72,7 +72,7 @@ export class HelmCharts extends Component<Props> {
(items: HelmChart[]) => items.filter(item => !item.deprecated) (items: HelmChart[]) => items.filter(item => !item.deprecated)
]} ]}
customizeHeader={() => ( customizeHeader={() => (
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)}/> <SearchInput placeholder={_i18n._(t`Search Helm Charts`)} />
)} )}
renderTableHeader={[ renderTableHeader={[
{ className: "icon" }, { className: "icon" },
@ -99,11 +99,13 @@ export class HelmCharts extends Component<Props> {
detailsItem={this.selectedChart} detailsItem={this.selectedChart}
onDetails={this.showDetails} onDetails={this.showDetails}
/> />
<HelmChartDetails {this.selectedChart && (
chart={this.selectedChart} <HelmChartDetails
hideDetails={this.hideDetails} chart={this.selectedChart}
/> hideDetails={this.hideDetails}
/>
)}
</> </>
); );
} }
} }

View File

@ -5,6 +5,7 @@ import { t } from "@lingui/macro";
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api"; import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
import { IReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api"; import { IReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { Notifications } from "../notifications";
export interface IChartInstallData { export interface IChartInstallData {
name: string; name: string;
@ -27,45 +28,50 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
}); });
autorun(() => { autorun(() => {
const { selectedTab, isOpen } = dockStore; const { selectedTab, isOpen } = dockStore;
if (!isInstallChartTab(selectedTab)) return; if (isInstallChartTab(selectedTab) && isOpen) {
if (isOpen) { this.loadData()
this.loadData(); .catch(err => Notifications.error(String(err)))
} }
}, { delay: 250 }) }, { delay: 250 })
} }
@action @action
async loadData(tabId = dockStore.selectedTabId) { async loadData(tabId = dockStore.selectedTabId) {
const { values } = this.getData(tabId); const promises = []
const versions = this.versions.getData(tabId);
return Promise.all([ if (!this.getData(tabId).values) {
!versions && this.loadVersions(tabId), promises.push(this.loadValues(tabId))
!values && this.loadValues(tabId), }
])
if (!this.versions.getData(tabId)) {
promises.push(this.loadVersions(tabId))
}
await Promise.all(promises)
} }
@action @action
async loadVersions(tabId: TabId) { async loadVersions(tabId: TabId) {
const { repo, name } = this.getData(tabId); const { repo, name, version } = this.getData(tabId);
this.versions.clearData(tabId); // reset this.versions.clearData(tabId); // reset
const charts = await helmChartsApi.get(repo, name); const charts = await helmChartsApi.get(repo, name, version);
const versions = charts.versions.map(chartVersion => chartVersion.version); const versions = charts.versions.map(chartVersion => chartVersion.version);
this.versions.setData(tabId, versions); this.versions.setData(tabId, versions);
} }
@action @action
async loadValues(tabId: TabId) { async loadValues(tabId: TabId) {
const data = this.getData(tabId); const data = this.getData(tabId)
const { repo, name, version } = data; const { repo, name, version } = data
let values = "";
const fetchValues = async (retry = 1, maxRetries = 3) => { // This loop is for "retrying" the "getValues" call
values = await helmChartsApi.getValues(repo, name, version); for (const _ of Array(3)) {
if (values || retry == maxRetries) return; const values = await helmChartsApi.getValues(repo, name, version)
await fetchValues(retry + 1); if (values) {
}; this.setData(tabId, { ...data, values })
this.setData(tabId, { ...data, values: undefined }); // reset return
await fetchValues(); }
this.setData(tabId, { ...data, values }); }
} }
} }

View File

@ -107,15 +107,15 @@ export class InstallChart extends Component<Props> {
render() { render() {
const { tabId, chartData, values, versions, install } = this; const { tabId, chartData, values, versions, install } = this;
if (!chartData || chartData.values === undefined || !versions) { if (chartData?.values === undefined || !versions) {
return <Spinner center/>; return <Spinner center />;
} }
if (this.releaseDetails) { if (this.releaseDetails) {
return ( return (
<div className="InstallChartDone flex column gaps align-center justify-center"> <div className="InstallChartDone flex column gaps align-center justify-center">
<p> <p>
<Icon material="check" big sticker/> <Icon material="check" big sticker />
</p> </p>
<p><Trans>Installation complete!</Trans></p> <p><Trans>Installation complete!</Trans></p>
<div className="flex gaps align-center"> <div className="flex gaps align-center">
@ -144,7 +144,7 @@ export class InstallChart extends Component<Props> {
const panelControls = ( const panelControls = (
<div className="install-controls flex gaps align-center"> <div className="install-controls flex gaps align-center">
<span><Trans>Chart</Trans></span> <span><Trans>Chart</Trans></span>
<Badge label={`${repo}/${name}`} title={_i18n._(t`Repo/Name`)}/> <Badge label={`${repo}/${name}`} title={_i18n._(t`Repo/Name`)} />
<span><Trans>Version</Trans></span> <span><Trans>Version</Trans></span>
<Select <Select
className="chart-version" className="chart-version"
@ -191,4 +191,4 @@ export class InstallChart extends Component<Props> {
</div> </div>
); );
} }
} }

View File

@ -8,10 +8,16 @@ import { JsonApiErrorParsed } from "../../api/json-api";
export type IMessageId = string | number; export type IMessageId = string | number;
export type IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed; export type IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
export enum NotificationStatus {
OK = "ok",
ERROR = "error",
INFO = "info",
}
export interface INotification { export interface INotification {
id?: IMessageId; id?: IMessageId;
message: IMessage; message: IMessage;
status?: "ok" | "error" | "info"; status?: NotificationStatus;
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
} }

View File

@ -5,7 +5,7 @@ import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react" import { disposeOnUnmount, observer } from "mobx-react"
import { JsonApiErrorParsed } from "../../api/json-api"; import { JsonApiErrorParsed } from "../../api/json-api";
import { cssNames, prevDefault } from "../../utils"; import { cssNames, prevDefault } from "../../utils";
import { IMessage, INotification, notificationsStore } from "./notifications.store"; import { IMessage, INotification, notificationsStore, NotificationStatus } from "./notifications.store";
import { Animate } from "../animate"; import { Animate } from "../animate";
import { Icon } from "../icon" import { Icon } from "../icon"
@ -17,7 +17,7 @@ export class Notifications extends React.Component {
notificationsStore.add({ notificationsStore.add({
message: message, message: message,
timeout: 2500, timeout: 2500,
status: "ok" status: NotificationStatus.OK
}) })
} }
@ -25,13 +25,13 @@ export class Notifications extends React.Component {
notificationsStore.add({ notificationsStore.add({
message: message, message: message,
timeout: 10000, timeout: 10000,
status: "error" status: NotificationStatus.ERROR
}); });
} }
static info(message: IMessage, customOpts: Partial<INotification> = {}) { static info(message: IMessage, customOpts: Partial<INotification> = {}) {
return notificationsStore.add({ return notificationsStore.add({
status: "info", status: NotificationStatus.INFO,
timeout: 0, timeout: 0,
message: message, message: message,
...customOpts, ...customOpts,
@ -78,7 +78,7 @@ export class Notifications extends React.Component {
onMouseLeave={() => addAutoHideTimer(notification)} onMouseLeave={() => addAutoHideTimer(notification)}
onMouseEnter={() => removeAutoHideTimer(notification)}> onMouseEnter={() => removeAutoHideTimer(notification)}>
<div className="box center"> <div className="box center">
<Icon material="info_outline"/> <Icon material="info_outline" />
</div> </div>
<div className="message box grow">{msgText}</div> <div className="message box grow">{msgText}</div>
<div className="box center"> <div className="box center">