From da07d5db1423027f69c162ef35d96b34c311913c Mon Sep 17 00:00:00 2001 From: Risha Mars Date: Tue, 21 Aug 2018 11:38:34 -0700 Subject: [PATCH] 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. --- web/app/js/components/ResourceDetail.jsx | 159 +++++++++++++++++------ web/app/js/components/ResourceList.jsx | 2 +- web/app/js/components/util/Utils.js | 15 +++ 3 files changed, 135 insertions(+), 41 deletions(-) diff --git a/web/app/js/components/ResourceDetail.jsx b/web/app/js/components/ResourceDetail.jsx index d9044162a..71ddc0905 100644 --- a/web/app/js/components/ResourceDetail.jsx +++ b/web/app/js/components/ResourceDetail.jsx @@ -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 ; + this.setState({ + loaded: true, + pendingRequests: false, + error: e + }); + } + + banner = () => { + if (!this.state.error) { + return; + } + + return ; } content = () => { - const {data, loading, error} = this.props; - - if (loading && !error) { + if (!this.state.loaded && !this.state.error) { return ; } - let processedMetrics = []; - if (_.has(data, '[0].ok')) { - processedMetrics = processSingleResourceRollup(data[0]); - } - return ( - +
+
+ +
+ + { + this.state.resource.type === "pod" ? null : ( +
+

Pods

+ +
+ ) + } +
); } render() { - const {loading, api} = this.props; let resourceBreadcrumb = ( - {this.state.namespace} > {`${this.state.resourceType}/${this.state.resourceName}`} + + {this.state.namespace} + > {`${this.state.resource.type}/${this.state.resource.name}`} ); @@ -98,8 +186,8 @@ export class ResourceDetailBase extends React.Component {
{this.banner()} - {loading ? null : } {resourceBreadcrumb} + {this.content()}
@@ -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); diff --git a/web/app/js/components/ResourceList.jsx b/web/app/js/components/ResourceList.jsx index b0bf839a0..fd7fd67d3 100644 --- a/web/app/js/components/ResourceList.jsx +++ b/web/app/js/components/ResourceList.jsx @@ -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]); } diff --git a/web/app/js/components/util/Utils.js b/web/app/js/components/util/Utils.js index 2bbf991ce..6db69be03 100644 --- a/web/app/js/components/util/Utils.js +++ b/web/app/js/components/util/Utils.js @@ -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 */