Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Link scope-ui graphs clickable to prometheus queries #2664

Merged
merged 31 commits into from
Aug 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fe98802
Link scope-ui graphs clickable to prometheus queries
rndstr Jun 29, 2017
14bf0c6
Fix linting, cleanup, and add example to flag
rndstr Jun 29, 2017
aff3295
Remove unnecessary fixture dependency of tests
rndstr Jun 29, 2017
d20e7be
Use ugorji over core json
rndstr Jun 29, 2017
bcc0617
Track health graph click in mixpanel (&& cleanup)
rndstr Jun 29, 2017
c483ef1
Initialize props with immutable map/list
rndstr Jun 29, 2017
2f5fe24
Improve comments; fix missing links w/o metrics
rndstr Jul 4, 2017
4985b45
New design for hover states and overflow handling
rndstr Jul 11, 2017
c895b78
Expand on the prom query source in comments
rndstr Jul 11, 2017
6ac2479
Fix linting errors
rndstr Jul 11, 2017
b52701b
Avoid global by passing metricsGraphURL down
rndstr Jul 12, 2017
42f2054
Update Deployment and DaemonSet queries
rndstr Jul 13, 2017
3722d55
Switch to migrated rules in kube-state-metrics
rndstr Jul 17, 2017
5334099
Append links directly to metrics
rndstr Jul 19, 2017
9372498
Pass `layout` when tracking mixpanel event
rndstr Jul 24, 2017
b34e111
Revert "Avoid global by passing metricsGraphURL down"
rndstr Jul 24, 2017
34dd118
Fix query typo
rndstr Jul 25, 2017
484b18e
Color metric label on hover
rndstr Jul 27, 2017
92d4341
JS feedback from David
rndstr Jul 28, 2017
8a07205
Replace templates with simple string replacement
rndstr Jul 28, 2017
9c610bf
Do not mix immutablejs with plain array functions
rndstr Aug 1, 2017
60a04db
No preview for overflow health items
rndstr Aug 7, 2017
ca45b90
Use percentage and MB for CPU/Memory urls
rndstr Aug 10, 2017
0c167e0
Pass timetravel timestamp to cortex in deeplink
rndstr Aug 11, 2017
bdab286
Remove grouped containers since queries are incorrect
rndstr Aug 14, 2017
bcb0694
Revert "Revert "Avoid global by passing metricsGraphURL down""
rndstr Aug 9, 2017
f6af178
Get rid of metricsGraphURL singleton
rndstr Aug 9, 2017
4779557
Introduce WebReporter to hold web-related options
rndstr Aug 11, 2017
b47cfbd
Update tests to new code
rndstr Aug 14, 2017
c059f9a
Decode WS with %20 instead +
rndstr Aug 14, 2017
44c23fa
Disable net traffic metric links
rndstr Aug 15, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/api_topologies.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ func captureReporter(rep Reporter, f reporterHandler) CtxHandlerFunc {
}
}

type rendererHandler func(context.Context, render.Renderer, render.Decorator, report.Report, http.ResponseWriter, *http.Request)
type rendererHandler func(context.Context, render.Renderer, render.Decorator, report.RenderContext, http.ResponseWriter, *http.Request)

func (r *Registry) captureRenderer(rep Reporter, f rendererHandler) CtxHandlerFunc {
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
Expand All @@ -579,6 +579,6 @@ func (r *Registry) captureRenderer(rep Reporter, f rendererHandler) CtxHandlerFu
respondWith(w, http.StatusInternalServerError, err)
return
}
f(ctx, renderer, decorator, rpt, w, req)
f(ctx, renderer, decorator, RenderContextForReporter(rep, rpt), w, req)
}
}
2 changes: 1 addition & 1 deletion app/api_topologies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func getTestContainerLabelFilterTopologySummary(t *testing.T, exclude bool) (det
return nil, err
}

return detailed.Summaries(fixture.Report, renderer.Render(fixture.Report, decorator)), nil
return detailed.Summaries(report.RenderContext{Report: fixture.Report}, renderer.Render(fixture.Report, decorator)), nil
}

func TestAPITopologyAddsKubernetes(t *testing.T) {
Expand Down
16 changes: 8 additions & 8 deletions app/api_topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,27 @@ type APINode struct {
}

// Full topology.
func handleTopology(ctx context.Context, renderer render.Renderer, decorator render.Decorator, report report.Report, w http.ResponseWriter, r *http.Request) {
func handleTopology(ctx context.Context, renderer render.Renderer, decorator render.Decorator, rc report.RenderContext, w http.ResponseWriter, r *http.Request) {
respondWith(w, http.StatusOK, APITopology{
Nodes: detailed.Summaries(report, renderer.Render(report, decorator)),
Nodes: detailed.Summaries(rc, renderer.Render(rc.Report, decorator)),
})
}

// Individual nodes.
func handleNode(ctx context.Context, renderer render.Renderer, decorator render.Decorator, report report.Report, w http.ResponseWriter, r *http.Request) {
func handleNode(ctx context.Context, renderer render.Renderer, decorator render.Decorator, rc report.RenderContext, w http.ResponseWriter, r *http.Request) {
var (
vars = mux.Vars(r)
topologyID = vars["topology"]
nodeID = vars["id"]
preciousRenderer = render.PreciousNodeRenderer{PreciousNodeID: nodeID, Renderer: renderer}
rendered = preciousRenderer.Render(report, decorator)
rendered = preciousRenderer.Render(rc.Report, decorator)
node, ok = rendered[nodeID]
)
if !ok {
http.NotFound(w, r)
return
}
respondWith(w, http.StatusOK, APINode{Node: detailed.MakeNode(topologyID, report, rendered, node)})
respondWith(w, http.StatusOK, APINode{Node: detailed.MakeNode(topologyID, rc, rendered, node)})
}

// Websocket for the full topology.
Expand Down Expand Up @@ -113,17 +113,17 @@ func handleWebsocket(
// might be interested in implementing in the future.
timestampDelta := time.Since(channelOpenedAt)
reportTimestamp := startReportingAt.Add(timestampDelta)
report, err := rep.Report(ctx, reportTimestamp)
re, err := rep.Report(ctx, reportTimestamp)
if err != nil {
log.Errorf("Error generating report: %v", err)
return
}
renderer, decorator, err := topologyRegistry.RendererForTopology(topologyID, r.Form, report)
renderer, decorator, err := topologyRegistry.RendererForTopology(topologyID, r.Form, re)
if err != nil {
log.Errorf("Error generating report: %v", err)
return
}
newTopo := detailed.Summaries(report, renderer.Render(report, decorator))
newTopo := detailed.Summaries(RenderContextForReporter(rep, re), renderer.Render(re, decorator))
diff := detailed.TopoDiff(previousTopo, newTopo)
previousTopo = newTopo

Expand Down
17 changes: 17 additions & 0 deletions app/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ type Reporter interface {
UnWait(context.Context, chan struct{})
}

// WebReporter is a reporter that creates reports whose data is eventually
// displayed on websites. It carries fields that will be forwarded to the
// report.RenderContext
type WebReporter struct {
Reporter
MetricsGraphURL string
}

// RenderContextForReporter creates the rendering context for the given reporter.
func RenderContextForReporter(rep Reporter, r report.Report) report.RenderContext {
rc := report.RenderContext{Report: r}
if wrep, ok := rep.(WebReporter); ok {
rc.MetricsGraphURL = wrep.MetricsGraphURL
}
return rc
}

// Adder is something that can accept reports. It's a convenient interface for
// parts of the app, and several experimental components. It takes the following
// arguments:
Expand Down
5 changes: 1 addition & 4 deletions client/app/scripts/charts/nodes-grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function getColumns(nodes) {
.toList()
.flatMap((n) => {
const metrics = (n.get('metrics') || makeList())
.filter(m => !m.get('valueEmpty'))
.map(m => makeMap({ id: m.get('id'), label: m.get('label'), dataType: 'number' }));
return metrics;
})
Expand Down Expand Up @@ -87,10 +88,6 @@ class NodesGrid extends React.Component {
}

onClickRow(ev, node) {
// TODO: do this better
if (ev.target.className === 'node-details-table-node-link') {
return;
}
trackMixpanelEvent('scope.node.click', {
layout: TABLE_VIEW_MODE,
topologyId: this.props.currentTopology.get('id'),
Expand Down
5 changes: 5 additions & 0 deletions client/app/scripts/components/cloud-feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ class CloudFeature extends React.Component {
});
}

// also show if not in weave cloud?
if (this.props.alwaysShow) {
return React.cloneElement(React.Children.only(this.props.children));
}

return null;
}
}
Expand Down
72 changes: 72 additions & 0 deletions client/app/scripts/components/cloud-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { connect } from 'react-redux';
import filterInvalidDOMProps from 'filter-invalid-dom-props';

import CloudFeature from './cloud-feature';

/**
* CloudLink provides an anchor that allows to set a target
* that is comprised of Weave Cloud related pieces.
*
* We support here relative links with a leading `/` that rewrite
* the browser url as well as cloud-related placeholders (:orgId).
*
* If no `url` is given, only the children is rendered (no anchor).
*
* If you want to render the content even if not on the cloud, set
* the `alwaysShow` property. A location redirect will be made for
* clicks instead.
*/
const CloudLink = ({ alwaysShow, ...props }) => (
<CloudFeature alwaysShow={alwaysShow}>
<LinkWrapper {...props} />
</CloudFeature>
);

class LinkWrapper 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, onClick } = this.props;

if (onClick) {
onClick();
}

if (router && href[0] === '/') {
router.push(href);
} else {
location.href = href;
}
}

buildHref(url) {
const { params } = this.props;
if (!url || !params || !params.orgId) return url;
return url.replace(/:orgid/gi, encodeURIComponent(this.props.params.orgId));
}

render() {
const { url, children, ...props } = this.props;
if (!url) {
return children;
}

const href = this.buildHref(url);
return (
<a {...filterInvalidDOMProps(props)} href={href} onClick={e => this.handleClick(e, href)}>
{children}
</a>
);
}
}

export default connect()(CloudLink);
7 changes: 5 additions & 2 deletions client/app/scripts/components/node-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class NodeDetails extends React.Component {
}

renderDetails() {
const { details, nodeControlStatus, nodeMatches = makeMap() } = this.props;
const { details, nodeControlStatus, nodeMatches = makeMap(), topologyId } = this.props;
const showControls = details.controls && details.controls.length > 0;
const nodeColor = getNodeColorDark(details.rank, details.label, details.pseudo);
const {error, pending} = nodeControlStatus ? nodeControlStatus.toJS() : {};
Expand Down Expand Up @@ -199,7 +199,10 @@ class NodeDetails extends React.Component {
<div className="node-details-content">
{details.metrics && <div className="node-details-content-section">
<div className="node-details-content-section-header">Status</div>
<NodeDetailsHealth metrics={details.metrics} />
<NodeDetailsHealth
metrics={details.metrics}
topologyId={topologyId}
/>
</div>}
{details.metadata && <div className="node-details-content-section">
<div className="node-details-content-section-header">Info</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import moment from 'moment';

import { appendTime } from '../node-details-health-link-item';


describe('NodeDetailsHealthLinkItem', () => {
describe('appendTime', () => {
const time = moment.unix(1496275200);

it('returns url for empty url or time', () => {
expect(appendTime('', time)).toEqual('');
expect(appendTime('foo', null)).toEqual('foo');
expect(appendTime('', null)).toEqual('');
});

it('appends as json for cloud link', () => {
const url = appendTime('/prom/:orgid/notebook/new/%7B%22cells%22%3A%5B%7B%22queries%22%3A%5B%22go_goroutines%22%5D%7D%5D%7D', time);
expect(url).toContain(time.unix());

const payload = JSON.parse(decodeURIComponent(url.substr(url.indexOf('new/') + 4)));
expect(payload.time.queryEnd).toEqual(time.unix());
});

it('appends as GET parameter', () => {
expect(appendTime('http://example.test?q=foo', time)).toEqual('http://example.test?q=foo&time=1496275200');
expect(appendTime('http://example.test/q=foo/', time)).toEqual('http://example.test/q=foo/?time=1496275200');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ import Sparkline from '../sparkline';
import { formatMetric } from '../../utils/string-utils';

function NodeDetailsHealthItem(props) {
const labelStyle = { color: props.labelColor };
return (
<div className="node-details-health-item">
<div className="node-details-health-item-value">{formatMetric(props.value, props)}</div>
{!props.valueEmpty && <div className="node-details-health-item-value" style={labelStyle}>{formatMetric(props.value, props)}</div>}
<div className="node-details-health-item-sparkline">
<Sparkline
data={props.samples} max={props.max} format={props.format}
first={props.first} last={props.last} />
first={props.first} last={props.last} hoverColor={props.metricColor}
hovered={props.hovered}
/>
</div>
<div className="node-details-health-item-label" style={labelStyle}>
{props.label}
</div>
<div className="node-details-health-item-label">{props.label}</div>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import { connect } from 'react-redux';

import NodeDetailsHealthItem from './node-details-health-item';
import CloudLink from '../cloud-link';
import { getMetricColor } from '../../utils/metric-utils';
import { darkenColor } from '../../utils/color-utils';
import { trackMixpanelEvent } from '../../utils/tracking-utils';

/**
* @param {string} url
* @param {Moment} time
* @returns {string}
*/
export function appendTime(url, time) {
if (!url || !time) return url;

// rudimentary check whether we have a cloud link
const cloudLinkPathEnd = 'notebook/new/';
const pos = url.indexOf(cloudLinkPathEnd);
if (pos !== -1) {
let payload;
const json = decodeURIComponent(url.substr(pos + cloudLinkPathEnd.length));
try {
payload = JSON.parse(json);
payload.time = { queryEnd: time.unix() };
} catch (e) {
return url;
}

return `${url.substr(0, pos + cloudLinkPathEnd.length)}${encodeURIComponent(JSON.stringify(payload) || '')}`;
}

if (url.indexOf('?') !== -1) {
return `${url}&time=${time.unix()}`;
}
return `${url}?time=${time.unix()}`;
}

class NodeDetailsHealthLinkItem extends React.Component {

constructor(props) {
super(props);
this.state = {
hovered: false
};

this.onMouseOver = this.onMouseOver.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
this.onClick = this.onClick.bind(this);
}

onMouseOver() {
this.setState({hovered: true});
}

onMouseOut() {
this.setState({hovered: false});
}

onClick() {
trackMixpanelEvent('scope.node.metric.click', { topologyId: this.props.topologyId });
}

render() {
const { id, url, pausedAt, ...props } = this.props;
const metricColor = getMetricColor(id);
const labelColor = this.state.hovered && !props.valueEmpty && darkenColor(metricColor);

const timedUrl = appendTime(url, pausedAt);

return (
<CloudLink
alwaysShow
className="node-details-health-link-item"
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
onClick={this.onClick}
url={timedUrl}
>
<NodeDetailsHealthItem
{...props}
hovered={this.state.hovered}
labelColor={labelColor}
metricColor={metricColor}
/>
</CloudLink>
);
}
}

function mapStateToProps(state) {
return {
pausedAt: state.get('pausedAt'),
};
}

export default connect(mapStateToProps)(NodeDetailsHealthLinkItem);

This file was deleted.

Loading