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 */