Link scope-ui graphs clickable to prometheus queries

scope-app:
- Adds `-app.metrics-graph` cli flag for configuring the base url to
  use for graph links; supports `:orgID` and `:query` placeholders
- Renders `metric_links` in node detail API response

scope-ui:
- Extends `<CloudFeature />` with option `alwaysShow` and adds
  boolean `isCloud` property
- Links metric graphs in the ui's node details view for all k8s
  toplogies; or displays placeholder graph if no metrics available
This commit is contained in:
Roland Schilter
2017-06-29 11:11:33 +02:00
parent 0b283b0698
commit 702220f2ce
13 changed files with 396 additions and 40 deletions

View File

@@ -13,10 +13,16 @@ class CloudFeature extends React.Component {
if (process.env.WEAVE_CLOUD) {
return React.cloneElement(React.Children.only(this.props.children), {
params: this.context.router.params,
router: this.context.router
router: this.context.router,
isCloud: true
});
}
// also show if not in weave cloud?
if (this.props.alwaysShow) {
return React.cloneElement(React.Children.only(this.props.children), {isCloud: false});
}
return null;
}
}

View File

@@ -165,6 +165,14 @@ class NodeDetails extends React.Component {
}
};
// collect by metric id (id => link)
const metricLinks = (details.metric_links || [])
.reduce((agg, link) => Object.assign(agg, {[link.id]: link}), {});
// collect links with no corresponding metric
const unattachedLinks = Object.assign({}, metricLinks);
(details.metrics || []).forEach(metric => delete unattachedLinks[metric.id]);
return (
<div className="node-details">
{tools}
@@ -190,9 +198,13 @@ class NodeDetails extends React.Component {
</div>}
<div className="node-details-content">
{details.metrics && <div className="node-details-content-section">
{Object.keys(metricLinks).length > 0 && <div className="node-details-content-section">
<div className="node-details-content-section-header">Status</div>
<NodeDetailsHealth metrics={details.metrics} />
<NodeDetailsHealth
metrics={details.metrics}
metricLinks={metricLinks}
unattachedLinks={unattachedLinks}
/>
</div>}
{details.metadata && <div className="node-details-content-section">
<div className="node-details-content-section-header">Info</div>

View File

@@ -6,13 +6,17 @@ import { formatMetric } from '../../utils/string-utils';
function NodeDetailsHealthItem(props) {
return (
<div className="node-details-health-item">
<div className="node-details-health-item-value">{formatMetric(props.value, props)}</div>
<div className="node-details-health-item-sparkline">
{props.value !== undefined && <div className="node-details-health-item-value">{formatMetric(props.value, props)}</div>}
{props.samples && <div className="node-details-health-item-sparkline">
<Sparkline
data={props.samples} max={props.max} format={props.format}
first={props.first} last={props.last} />
</div>}
{!props.samples && <div className="node-details-health-item-placeholder"><span className="fa fa-circle-thin" /></div>}
<div className="node-details-health-item-label">
{props.label}
{props.icon && <span className={`fa ${props.icon}`} />}
</div>
<div className="node-details-health-item-label">{props.label}</div>
</div>
);
}

View File

@@ -0,0 +1,47 @@
import React from 'react';
import NodeDetailsHealthItem from './node-details-health-item';
export default class NodeDetailsHealthLinkItem extends React.Component {
constructor(props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
this.buildHref = this.buildHref.bind(this);
}
handleClick(ev, href) {
ev.preventDefault();
if (!href) return;
const {router} = this.props;
if (router && href[0] === '/') {
router.push(href);
} else {
location.href = href;
}
}
buildHref(url) {
if (!url || !this.props.isCloud) return url;
return url.replace(/:orgid/gi, encodeURIComponent(this.props.params.orgId));
}
render() {
const {links, withoutGraph, ...props} = this.props;
const href = this.buildHref(links[props.id] && links[props.id].url);
if (!href) return <NodeDetailsHealthItem {...props} />;
return (
<a
className="node-details-health-link-item"
href={href}
onClick={e => this.handleClick(e, href)}>
{!withoutGraph && <NodeDetailsHealthItem {...props} icon="fa-expand" />}
{withoutGraph && <NodeDetailsHealthItem label={props.label} icon="fa-expand" />}
</a>
);
}
}

View File

@@ -2,7 +2,8 @@ import React from 'react';
import ShowMore from '../show-more';
import NodeDetailsHealthOverflow from './node-details-health-overflow';
import NodeDetailsHealthItem from './node-details-health-item';
import NodeDetailsHealthLinkItem from './node-details-health-link-item';
import CloudFeature from '../cloud-feature';
export default class NodeDetailsHealth extends React.Component {
@@ -21,6 +22,9 @@ export default class NodeDetailsHealth extends React.Component {
render() {
const metrics = this.props.metrics || [];
const metricLinks = this.props.metricLinks || {};
const unattachedLinks = this.props.unattachedLinks || {};
const hasUnattached = Object.keys(unattachedLinks).length > 0;
const primeCutoff = metrics.length > 3 && !this.state.expanded ? 2 : metrics.length;
const primeMetrics = metrics.slice(0, primeCutoff);
const overflowMetrics = metrics.slice(primeCutoff);
@@ -32,7 +36,12 @@ export default class NodeDetailsHealth extends React.Component {
return (
<div className="node-details-health" style={{flexWrap, justifyContent}}>
<div className="node-details-health-wrapper">
{primeMetrics.map(item => <NodeDetailsHealthItem key={item.id} {...item} />)}
{primeMetrics.map(item => <CloudFeature alwaysShow key={item.id}>
<NodeDetailsHealthLinkItem
{...item}
links={metricLinks}
/>
</CloudFeature>)}
{showOverflow && <NodeDetailsHealthOverflow
items={overflowMetrics}
handleClick={this.handleClickMore}
@@ -41,6 +50,16 @@ export default class NodeDetailsHealth extends React.Component {
<ShowMore
handleClick={this.handleClickMore} collection={this.props.metrics}
expanded={this.state.expanded} notShown={notShown} hideNumber />
{hasUnattached && <div className="node-details-health-wrapper">
{Object.keys(unattachedLinks).map(id => <CloudFeature alwaysShow key={id}>
<NodeDetailsHealthLinkItem
withoutGraph
{...unattachedLinks[id]}
links={unattachedLinks}
/>
</CloudFeature>)}
</div>}
</div>
);
}