From 1bf280b10505282b87a4dc698a152ea30b142998 Mon Sep 17 00:00:00 2001 From: Risha Mars Date: Fri, 17 Aug 2018 10:53:05 -0700 Subject: [PATCH] Add resource detail skeleton page (#1476) Add a barebones resource detail page. We'll use the kubernetes REST style api for locating specific resource pages. Example URLS: http://localhost:8084/namespaces/emojivoto/pods/emoji-7578f4f846-m6872 http://localhost:8084/namespaces/emojivoto/deployments/voting --- web/app/js/components/ResourceDetail.jsx | 120 +++++++++++++++++++++++ web/app/js/components/util/Utils.js | 30 ++++++ web/app/js/index.js | 6 +- web/srv/server.go | 3 + 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 web/app/js/components/ResourceDetail.jsx 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 = ( - + + + +