mirror of https://github.com/linkerd/linkerd2.git
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:
parent
efabd90ff7
commit
da07d5db14
|
@ -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> > {`${this.state.resourceType}/${this.state.resourceName}`}
|
||||
<this.api.PrefixedLink to={"/namespaces/" + this.state.namespace}>
|
||||
{this.state.namespace}
|
||||
</this.api.PrefixedLink> > {`${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);
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue