diff --git a/web/app/js/components/ResourceDetail.jsx b/web/app/js/components/ResourceDetail.jsx
new file mode 100644
index 000000000..1a292523b
--- /dev/null
+++ b/web/app/js/components/ResourceDetail.jsx
@@ -0,0 +1,120 @@
+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 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 './../../css/list.css';
+import 'whatwg-fetch';
+
+const getResourceFromUrl = (match, pathPrefix) => {
+ let resource = {
+ namespace: match.params.namespace
+ };
+ let regExp = RegExp(`${pathPrefix || ""}/namespaces/${match.params.namespace}/([^/]+)/([^/]+)`);
+ let urlParts = match.url.match(regExp);
+
+ resource.type = singularResource(urlParts[1]);
+ resource.name = urlParts[2];
+
+ if (match.params[resource.type] !== resource.name) {
+ console.error("Failed to extract resource from URL");
+ }
+ return resource;
+};
+
+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.state = this.getInitialState(props.match, props.pathPrefix);
+ }
+
+ getInitialState(match, pathPrefix) {
+ let resource = getResourceFromUrl(match, pathPrefix);
+ return {
+ namespace: resource.namespace,
+ resourceName: resource.name,
+ resourceType: resource.type
+ };
+ }
+
+ banner = () => {
+ const {error} = this.props;
+
+ if (!error) {
+ return;
+ }
+
+ return ;
+ }
+
+ content = () => {
+ const {data, loading, error} = this.props;
+
+ if (loading && !error) {
+ return ;
+ }
+
+ let processedMetrics = [];
+ if (_.has(data, '[0].ok')) {
+ processedMetrics = processSingleResourceRollup(data[0]);
+ }
+
+ return (
+
+ );
+ }
+
+ render() {
+ const {loading, api} = this.props;
+ let resourceBreadcrumb = (
+
+ {this.state.namespace} > {`${this.state.resourceType}/${this.state.resourceName}`}
+
+ );
+
+ return (
+
+
+ {this.banner()}
+ {loading ? null :
}
+ {resourceBreadcrumb}
+ {this.content()}
+
+
+ );
+ }
+}
+
+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'],
+ },
+);
diff --git a/web/app/js/components/util/Utils.js b/web/app/js/components/util/Utils.js
index 2738b8a19..2bbf991ce 100644
--- a/web/app/js/components/util/Utils.js
+++ b/web/app/js/components/util/Utils.js
@@ -122,3 +122,33 @@ export const friendlyTitle = resource => {
}
return titles;
};
+
+/*
+ Get a singular resource name from a plural resource
+*/
+export const singularResource = resource => {
+ if (resource === "authorities") {
+ return "authority";
+ } else {return resource.replace(/s$/, "");}
+};
+
+/*
+ produce octets given an ip address
+*/
+const decodeIPToOctets = ip => {
+ ip = parseInt(ip, 10);
+ return [
+ ip >> 24 & 255,
+ ip >> 16 & 255,
+ ip >> 8 & 255,
+ ip & 255
+ ];
+};
+
+/*
+ converts an address to an ipv4 formatted host:port pair
+*/
+export const publicAddressToString = (ipv4, port) => {
+ let octets = decodeIPToOctets(ipv4);
+ return octets.join(".") + ":" + port;
+};
diff --git a/web/app/js/index.js b/web/app/js/index.js
index d9c610f4b..c3ce766ad 100644
--- a/web/app/js/index.js
+++ b/web/app/js/index.js
@@ -5,6 +5,7 @@ import Namespace from './components/Namespace.jsx';
import NoMatch from './components/NoMatch.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
+import ResourceDetail from './components/ResourceDetail.jsx';
import ResourceList from './components/ResourceList.jsx';
import ServiceMesh from './components/ServiceMesh.jsx';
import Sidebar from './components/Sidebar.jsx';
@@ -41,7 +42,10 @@ let applicationHtml = (
-
+
+
+
+