mirror of https://github.com/linkerd/linkerd2.git
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
This commit is contained in:
parent
a3bd861667
commit
1bf280b105
|
@ -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 <ErrorBanner message={error} />;
|
||||
}
|
||||
|
||||
content = () => {
|
||||
const {data, loading, error} = this.props;
|
||||
|
||||
if (loading && !error) {
|
||||
return <Spin size="large" />;
|
||||
}
|
||||
|
||||
let processedMetrics = [];
|
||||
if (_.has(data, '[0].ok')) {
|
||||
processedMetrics = processSingleResourceRollup(data[0]);
|
||||
}
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
resource={this.state.resourceType}
|
||||
metrics={processedMetrics} />
|
||||
);
|
||||
}
|
||||
|
||||
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}`}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page-content">
|
||||
<div>
|
||||
{this.banner()}
|
||||
{loading ? null : <PageHeader header={`${this.state.resourceType}/${this.state.resourceName}`} />}
|
||||
{resourceBreadcrumb}
|
||||
{this.content()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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'],
|
||||
},
|
||||
);
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 = (
|
|||
<Switch>
|
||||
<Redirect exact from={`${pathPrefix}/`} to={`${pathPrefix}/servicemesh`} />
|
||||
<Route path={`${pathPrefix}/servicemesh`} component={ServiceMesh} />
|
||||
<Route path={`${pathPrefix}/namespaces/:namespace`} component={Namespace} />
|
||||
<Route exact path={`${pathPrefix}/namespaces/:namespace`} component={Namespace} />
|
||||
<Route path={`${pathPrefix}/namespaces/:namespace/pods/:pod`} component={ResourceDetail} />
|
||||
<Route path={`${pathPrefix}/namespaces/:namespace/deployments/:deployment`} component={ResourceDetail} />
|
||||
<Route path={`${pathPrefix}/namespaces/:namespace/replicationcontrollers/:replicationcontroller`} component={ResourceDetail} />
|
||||
<Route path={`${pathPrefix}/tap`} component={Tap} />
|
||||
<Route path={`${pathPrefix}/top`} component={Top} />
|
||||
<Route
|
||||
|
|
|
@ -90,6 +90,9 @@ func NewServer(addr, templateDir, staticDir, uuid, controllerNamespace, webpackD
|
|||
server.router.GET("/replicationcontrollers", handler.handleIndex)
|
||||
server.router.GET("/pods", handler.handleIndex)
|
||||
server.router.GET("/authorities", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace/pods/:pod", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace/deployments/:deployment", handler.handleIndex)
|
||||
server.router.GET("/namespaces/:namespace/replicationcontrollers/:replicationcontroller", handler.handleIndex)
|
||||
server.router.GET("/tap", handler.handleIndex)
|
||||
server.router.GET("/top", handler.handleIndex)
|
||||
server.router.ServeFiles(
|
||||
|
|
Loading…
Reference in New Issue