mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 09:41:57 +00:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user