static_src/components/usage_and_limits.jsx
import PropTypes from "prop-types";
import React from "react";
import Action from "./action.jsx";
import { Form, FormNumber } from "./form";
import FormStore from "../stores/form_store";
import Loading from "./loading.jsx";
import PanelActions from "./panel_actions.jsx";
import PanelGroup from "./panel_group.jsx";
import PanelBlock from "./panel_block.jsx";
import ResourceUsage from "./resource_usage.jsx";
import { appHealth } from "../util/health";
import { entityHealth } from "../constants";
import appActions from "../actions/app_actions.js";
import formatBytes from "../util/format_bytes";
// Calculates the running average based on a fixed n number of items To average
// across instances you can do something like `average.bind(null, // numberOfInstances)`
function average(n, avg, value) {
return avg + value / n;
}
// Calculate the cumulative sum
function sum(s, value) {
return s + value;
}
function getStat(statName, props, accumulator = sum) {
if (statName.indexOf("quota") > -1) {
return (
(props.app.app_instances &&
props.app.app_instances.length &&
props.app.app_instances[0].stats &&
props.app.app_instances[0].stats[statName]) ||
0
);
}
// For usage, sometimes we want an average across instances, sometimes we
// want the total. Use the accumulator to delegate this to the caller
return (props.app.app_instances || [])
.map(instance => (instance.stats && instance.stats.usage[statName]) || 0)
.reduce((cumulative, value) => accumulator(cumulative, value || 0), 0);
}
function formGuid(app) {
return `app-${app.guid}-usage-and-limits`;
}
const propTypes = {
app: PropTypes.object,
editing: PropTypes.bool,
quota: PropTypes.object
};
const defaultProps = {
app: {},
editing: false,
quota: {}
};
export default class UsageAndLimits extends React.Component {
constructor(props) {
super(props);
// Create the form instance in the store
const { instances, memory, disk_quota } = this.props.app;
const form = FormStore.create(formGuid(props.app), {
memory,
disk_quota,
instances
});
this.state = {
editing: !!props.editing,
form
};
this.getStat = this.getStat.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleToggleEdit = this.handleToggleEdit.bind(this);
}
componentDidMount() {
FormStore.addChangeListener(this.handleChange);
}
componentWillUnmount() {
FormStore.removeChangeListener(this.handleChange);
}
getStat(statName, accumulator) {
return getStat(statName, this.props, accumulator);
}
handleChange() {
const form = FormStore.get(formGuid(this.props.app));
this.setState({ form });
}
handleToggleEdit() {
this.setState({ editing: !this.state.editing });
}
get disk() {
const disk = this.state.editing
? this.state.form.fields.disk_quota.value
: this.props.app.disk_quota;
// For instance usage, we average the instances together
return (
<div className="stat-group">
<ResourceUsage
title="Instance disk used"
amountUsed={this.getStat(
"disk",
average.bind(null, this.props.app.running_instances)
)}
amountTotal={this.getStat("disk_quota")}
/>
<ResourceUsage
title="Instance disk allocation"
editable={this.state.editing}
formGuid={formGuid(this.props.app)}
max={2 * 1024}
min={1}
name="disk_quota"
amountTotal={disk * 1024 * 1024}
/>
</div>
);
}
get memory() {
const memory = this.state.editing
? this.state.form.fields.memory.value
: this.props.app.memory;
const maxMemory = Math.floor(
this.props.quota.memory_limit / this.state.form.fields.instances.value
);
// For instance usage, we average the instances together
return (
<div className="stat-group">
<ResourceUsage
title="Instance memory used"
amountUsed={this.getStat(
"mem",
average.bind(null, this.props.app.running_instances)
)}
amountTotal={this.getStat("mem_quota")}
/>
<ResourceUsage
title="Instance memory allocation"
editable={this.state.editing}
formGuid={formGuid(this.props.app)}
min={1}
max={maxMemory}
name="memory"
amountTotal={memory * 1024 * 1024}
/>
</div>
);
}
get totalDisk() {
// There is no org/space level disk quota, so only show single stat
return (
<ResourceUsage
title="Total disk used"
amountTotal={this.getStat("disk", sum)}
/>
);
}
get totalMemory() {
const amountTotal = this.props.quota.memory_limit * 1024 * 1024;
const amountUsed = this.getStat("mem", sum);
const title = amountUsed ? "Total memory used" : "Total memory available";
const secondaryInfo = `${formatBytes(amountTotal)} quota`;
return (
<ResourceUsage
title={title}
amountTotal={amountUsed}
secondaryInfo={secondaryInfo}
/>
);
}
get scale() {
const instanceCount = this.state.editing
? this.state.form.fields.instances.value
: this.props.app.instances;
let instances = <span className="stat-primary">{instanceCount}</span>;
if (this.state.editing) {
instances = (
<FormNumber
className="stat-input stat-input-text stat-input-text-scale"
formGuid={formGuid(this.props.app)}
id="scale"
inline
min={1}
max={256}
name="instances"
value={instanceCount}
/>
);
}
return (
<div className="stat stat-single_box">
<h2 className="stat-header">Instances</h2>
{instances}
<br />
<span className="subtext">Instance applies to memory and disk</span>
</div>
);
}
handleSubmit(errors, form) {
if (errors.length) {
return;
}
// Parse the form properties are mapped directly to API
// https://apidocs.cloudfoundry.org/246/apps/updating_an_app.html
const partialApp = {
disk_quota: parseInt(form.disk_quota.value, 10),
instances: parseInt(form.instances.value, 10),
memory: parseInt(form.memory.value, 10)
};
appActions.updateApp(this.props.app.guid, partialApp);
this.setState({ editing: false });
}
render() {
let content = <div />;
let controls;
controls = (
<Action
disabled={appHealth(this.props.app) !== entityHealth.ok}
style="primary"
type="outline"
label="Modify allocation and scale"
clickHandler={this.handleToggleEdit}
>
<span>Modify allocation and scale</span>
</Action>
);
if (this.props.app.updating) {
controls = <Loading text="Updating app" style="inline" />;
}
if (this.state.editing) {
controls = (
<div>
<Action style="finish" type="button" label="OK">
<span>OK</span>
</Action>
<Action
type="outline"
label="Cancel"
clickHandler={this.handleToggleEdit}
>
<span>Cancel</span>
</Action>
</div>
);
}
if (this.props.app) {
content = (
<div>
<PanelGroup>
<PanelGroup columns={6}>
{this.memory}
{this.disk}
</PanelGroup>
<PanelGroup columns={3}>
<PanelBlock>{this.scale}</PanelBlock>
</PanelGroup>
<PanelGroup columns={3}>
{this.totalMemory}
{this.totalDisk}
</PanelGroup>
</PanelGroup>
<PanelActions align="right">{controls}</PanelActions>
</div>
);
}
if (this.props.app && this.state.editing) {
// Wrap content in a form element
content = (
<Form guid={formGuid(this.props.app)} onSubmit={this.handleSubmit}>
{content}
</Form>
);
}
return content;
}
}
UsageAndLimits.propTypes = propTypes;
UsageAndLimits.defaultProps = defaultProps;