Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Add cost optimisation nudges. #3089

Merged
merged 5 commits into from
May 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions ui/src/app/shared/components/cost-optimisation-nudge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import {Notice} from './notice';

interface Props {
name: string;
}
interface State {
closed: boolean;
}

export class CostOptimisationNudge extends React.Component<Props, State> {
constructor(props: Readonly<Props>) {
super(props);
this.state = {closed: localStorage.getItem(this.key) !== null};
}

public render() {
return (
!this.state.closed && (
<Notice>
<i className='fa fa-money-bill-alt status-icon--pending' /> {this.props.children}{' '}
<a href='https://github.com/argoproj/argo/blob/master/docs/cost-optimisation.md'>Learn more</a>
<span className='fa-pull-right'>
<a onClick={() => this.close()}>
<i className='fa fa-times' />
</a>{' '}
</span>
</Notice>
)
);
}

private get key() {
return 'cost-optimization-nude/' + this.props.name;
}

private close() {
this.setState({closed: true});
localStorage.setItem(this.key, '{}');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, was curious as to how you would persist the closure.

}
}
13 changes: 13 additions & 0 deletions ui/src/app/shared/components/notice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';

export class Notice extends React.Component {
public render() {
return (
<div style={{marginTop: 20, marginBottom: 20}}>
<div className='white-box' style={{padding: 20}}>
{this.props.children}
</div>
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {uiUrl} from '../../../shared/base';
import {services} from '../../../shared/services';

import {WorkflowArtifacts, WorkflowDag, WorkflowLogsViewer, WorkflowNodeInfo, WorkflowSummaryPanel, WorkflowTimeline, WorkflowYamlViewer} from '..';
import {CostOptimisationNudge} from '../../../shared/components/cost-optimisation-nudge';
import {hasWarningConditionBadge} from '../../../shared/conditions-panel';
import {Consumer, ContextApis} from '../../../shared/context';
import {Utils} from '../../../shared/utils';
Expand Down Expand Up @@ -360,6 +361,27 @@ export class WorkflowDetails extends React.Component<RouteComponentProps<any>, W
this.appContext.router.history.push(`${this.props.match.url}?${params.toString()}`);
}

private renderCostOptimisations() {
const recommendations: string[] = [];
if (!this.state.workflow.spec.activeDeadlineSeconds) {
recommendations.push('activeDeadlineSeconds');
}
if (!this.state.workflow.spec.ttlStrategy) {
recommendations.push('ttlStrategy');
}
if (!this.state.workflow.spec.podGC) {
recommendations.push('podGC');
}
if (recommendations.length === 0) {
return;
}
return (
<CostOptimisationNudge name='workflow'>
You do not have {recommendations.join('/')} enabled for this workflow. Enabling these will reduce your costs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a link to the cost optimization docs here would be invaluable. I think we should wait until #2972 is merged and add the permalink to the doc here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contains that - so this PR depends on that PR

</CostOptimisationNudge>
);
}

private renderSummaryTab() {
if (!this.state.workflow) {
return <div>Loading...</div>;
Expand All @@ -368,6 +390,7 @@ export class WorkflowDetails extends React.Component<RouteComponentProps<any>, W
<div className='argo-container'>
<div className='workflow-details__content'>
<WorkflowSummaryPanel workflow={this.state.workflow} />
{this.renderCostOptimisations()}
{this.state.workflow.spec.arguments && this.state.workflow.spec.arguments.parameters && (
<React.Fragment>
<h6>Parameters</h6>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Subscription} from 'rxjs';

import {Autocomplete, Page, SlidingPanel} from 'argo-ui';
import * as models from '../../../../models';
import {Workflow} from '../../../../models';
import {labels, Workflow} from '../../../../models';
import {uiUrl} from '../../../shared/base';
import {Consumer} from '../../../shared/context';
import {services} from '../../../shared/services';
Expand All @@ -19,6 +19,7 @@ import {Utils} from '../../../shared/utils';

import {Ticker} from 'argo-ui/src/index';
import * as classNames from 'classnames';
import {CostOptimisationNudge} from '../../../shared/components/cost-optimisation-nudge';
import {PaginationPanel} from '../../../shared/components/pagination-panel';
import {Timestamp} from '../../../shared/components/timestamp';
import {formatDuration, wfDuration} from '../../../shared/duration';
Expand Down Expand Up @@ -225,6 +226,18 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
this.fetchWorkflows(namespace, selectedPhases, selectedLabels, pagination);
}

private countsByCompleted() {
const counts = {complete: 0, incomplete: 0};
this.state.workflows.forEach(wf => {
if (wf.metadata.labels && wf.metadata.labels[labels.completed] === 'true') {
counts.complete++;
} else {
counts.incomplete++;
}
});
return counts;
}

private renderWorkflows() {
if (!this.state.workflows) {
return <Loading />;
Expand All @@ -238,8 +251,15 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
);
}

const counts = this.countsByCompleted();

return (
<>
{(counts.complete > 100 || counts.incomplete > 100) && (
<CostOptimisationNudge name='workflow-list'>
You have at least {counts.incomplete} incomplete, and {counts.complete} complete workflows. Reducing these amounts will reduce your costs.
alexec marked this conversation as resolved.
Show resolved Hide resolved
</CostOptimisationNudge>
)}
<div className='argo-table-list'>
<div className='row argo-table-list__head'>
<div className='columns small-1' />
Expand Down
21 changes: 21 additions & 0 deletions ui/src/models/workflows.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as kubernetes from 'argo-ui/src/models/kubernetes';

export const labels = {
completed: 'workflows.argoproj.io/completed'
};

/**
* Arguments to a template
*/
Expand Down Expand Up @@ -804,6 +808,23 @@ export interface WorkflowList {
* WorkflowSpec is the specification of a Workflow.
*/
export interface WorkflowSpec {
/**
* Optional duration in seconds relative to the workflow start time which the workflow is
* allowed to run before the controller terminates the workflow. A value of zero is used to
* terminate a Running workflow
*/
activeDeadlineSeconds?: number;
/**
* TTLStrategy limits the lifetime of a Workflow that has finished execution depending on if it
* Succeeded or Failed. If this struct is set, once the Workflow finishes, it will be
* deleted after the time to live expires. If this field is unset,
* the controller config map will hold the default values.
*/
ttlStrategy?: {};
/**
* PodGC describes the strategy to use when to deleting completed pods
*/
podGC?: {};
/**
* Affinity sets the scheduling constraints for all pods in the workflow. Can be overridden by an affinity specified in the template
*/
Expand Down