Add pod table to resource detail page (#1491)

Add a pod table to the Resource Detail page showing metrics 
for pods belonging to a resource.

In the future, I think we'll modify the stat summary endpoint to 
take multiple resources as arguments, and have the resource detail page 
first query for the pods associated with the resource and then 
query for stats for those pods.

See #1467 for discussion.

This PR also modifies the queries to not use the withREST component, in anticipation of the above changes.
This commit is contained in:
Risha Mars 2018-08-21 11:38:34 -07:00 committed by GitHub
parent efabd90ff7
commit da07d5db14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 135 additions and 41 deletions

View File

@ -1,14 +1,13 @@
import _ from 'lodash';
import { apiErrorPropType } from './util/ApiHelpers.jsx';
import ErrorBanner from './ErrorBanner.jsx';
import MetricsTable from './MetricsTable.jsx';
import PageHeader from './PageHeader.jsx';
import { processSingleResourceRollup } from './util/MetricUtils.js';
import PropTypes from 'prop-types';
import React from 'react';
import { singularResource } from './util/Utils.js';
import { Spin } from 'antd';
import withREST from './util/withREST.jsx';
import { metricsPropType, processSingleResourceRollup } from './util/MetricUtils.js';
import { withContext } from './util/AppContext.jsx';
import { resourceTypeToCamelCase, singularResource } from './util/Utils.js';
import 'whatwg-fetch';
const getResourceFromUrl = (match, pathPrefix) => {
@ -28,23 +27,19 @@ const getResourceFromUrl = (match, pathPrefix) => {
};
export class ResourceDetailBase extends React.Component {
static defaultProps = {
error: null
}
static propTypes = {
api: PropTypes.shape({
PrefixedLink: PropTypes.func.isRequired,
}).isRequired,
data: PropTypes.arrayOf(metricsPropType.isRequired).isRequired,
error: apiErrorPropType,
loading: PropTypes.bool.isRequired,
match: PropTypes.shape({}).isRequired,
pathPrefix: PropTypes.string.isRequired
}
constructor(props) {
super(props);
this.api = this.props.api;
this.handleApiError = this.handleApiError.bind(this);
this.loadFromServer = this.loadFromServer.bind(this);
this.state = this.getInitialState(props.match, props.pathPrefix);
}
@ -53,44 +48,137 @@ export class ResourceDetailBase extends React.Component {
return {
namespace: resource.namespace,
resourceName: resource.name,
resourceType: resource.type
resourceType: resource.type,
resource,
pollingInterval: 2000,
resourceMetrics: [],
podMetrics: [], // metrics for all pods whose owner is this resource
pendingRequests: false,
loaded: false,
error: null
};
}
banner = () => {
const {error} = this.props;
componentDidMount() {
this.loadFromServer();
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
}
if (!error) {
componentWillReceiveProps(newProps) {
// React won't unmount this component when switching resource pages so we need to clear state
this.api.cancelCurrentRequests();
this.setState(this.getInitialState(newProps.match, newProps.pathPrefix));
}
componentWillUnmount() {
window.clearInterval(this.timerId);
this.api.cancelCurrentRequests();
}
loadFromServer() {
if (this.state.pendingRequests) {
return; // don't make more requests if the ones we sent haven't completed
}
this.setState({ pendingRequests: true });
let { resource } = this.state;
this.api.setCurrentRequests([
// inbound stats for this resource
this.api.fetchMetrics(
`${this.api.urlsForResource(resource.type, resource.namespace)}&resource_name=${resource.name}`
),
// list of all pods in this namespace (hack since we can't currently query for all pods in a resource)
this.api.fetchPods(resource.namespace),
// metrics for all pods in this namespace (hack, continued)
this.api.fetchMetrics(
`${this.api.urlsForResource("pod", resource.namespace)}`
),
]);
Promise.all(this.api.getCurrentPromises())
.then(([resourceRsp, podListRsp, podRsp]) => {
let resourceMetrics = processSingleResourceRollup(resourceRsp);
let podMetrics = processSingleResourceRollup(podRsp);
// INEFFICIENT: get metrics for all the pods belonging to this resource.
// Do this by querying for metrics for all pods in this namespace and then filtering
// out those pods whose owner is not this resource
// TODO: fix (#1467)
let podBelongsToResource = _.reduce(podListRsp.pods, (mem, pod) => {
if (_.get(pod, resourceTypeToCamelCase(resource.type)) === resource.namespace + "/" + resource.name) {
mem[pod.name] = true;
}
return mem;
}, {});
let podMetricsForResource = _.filter(podMetrics, pod => podBelongsToResource[pod.namespace + "/" + pod.name]);
this.setState({
resourceMetrics,
podMetrics: podMetricsForResource,
loaded: true,
pendingRequests: false,
error: null
});
})
.catch(this.handleApiError);
}
handleApiError = e => {
if (e.isCanceled) {
return;
}
return <ErrorBanner message={error} />;
this.setState({
loaded: true,
pendingRequests: false,
error: e
});
}
banner = () => {
if (!this.state.error) {
return;
}
return <ErrorBanner message={this.state.error} />;
}
content = () => {
const {data, loading, error} = this.props;
if (loading && !error) {
if (!this.state.loaded && !this.state.error) {
return <Spin size="large" />;
}
let processedMetrics = [];
if (_.has(data, '[0].ok')) {
processedMetrics = processSingleResourceRollup(data[0]);
}
return (
<MetricsTable
resource={this.state.resourceType}
metrics={processedMetrics} />
<div>
<div className="page-section">
<MetricsTable
resource={this.state.resource.type}
metrics={this.state.resourceMetrics} />
</div>
{
this.state.resource.type === "pod" ? null : (
<div className="page-section">
<h2 className="subsection-header">Pods</h2>
<MetricsTable
resource="pod"
metrics={this.state.podMetrics} />
</div>
)
}
</div>
);
}
render() {
const {loading, api} = this.props;
let resourceBreadcrumb = (
<React.Fragment>
<api.PrefixedLink to={"/namespaces/" + this.state.namespace}>{this.state.namespace}</api.PrefixedLink> &gt; {`${this.state.resourceType}/${this.state.resourceName}`}
<this.api.PrefixedLink to={"/namespaces/" + this.state.namespace}>
{this.state.namespace}
</this.api.PrefixedLink> &gt; {`${this.state.resource.type}/${this.state.resource.name}`}
</React.Fragment>
);
@ -98,8 +186,8 @@ export class ResourceDetailBase extends React.Component {
<div className="page-content">
<div>
{this.banner()}
{loading ? null : <PageHeader header={`${this.state.resourceType}/${this.state.resourceName}`} />}
{resourceBreadcrumb}
<PageHeader header={`${this.state.resource.type}/${this.state.resource.name}`} />
{this.content()}
</div>
</div>
@ -107,13 +195,4 @@ export class ResourceDetailBase extends React.Component {
}
}
export default withREST(
ResourceDetailBase,
({api, match, pathPrefix}) => {
let resource = getResourceFromUrl(match, pathPrefix);
return [api.fetchMetrics(api.urlsForResource(resource.type, resource.namespace) + "&resource_name=" + resource.name)];
},
{
resetProps: ['resource'],
},
);
export default withContext(ResourceDetailBase);

View File

@ -41,7 +41,7 @@ export class ResourceListBase extends React.Component {
}
let processedMetrics = [];
if (_.has(data, '[0].ok')) {
if (_.has(data, '[0]')) {
processedMetrics = processSingleResourceRollup(data[0]);
}

View File

@ -132,6 +132,21 @@ export const singularResource = resource => {
} else {return resource.replace(/s$/, "");}
};
/*
Get the resource type from the /pods response, whose json
is camelCased.
*/
const camelCaseLookUp = {
"replicaset": "replicaSet",
"replicationcontroller": "replicationController",
"statefulset": "statefulSet",
"daemonset": "daemonSet"
};
export const resourceTypeToCamelCase = resource => {
return camelCaseLookUp[resource] || resource;
};
/*
produce octets given an ip address
*/