Skip to content
Snippets Groups Projects
Commit 1dc051fb authored by Mike Greiling's avatar Mike Greiling Committed by Tim Zallmann
Browse files

Resolve "Add graph value to hover"

parent 78b044a1
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -69,8 +69,8 @@
currentFlagPosition: 0,
showFlag: false,
showFlagContent: false,
showDeployInfo: true,
timeSeries: [],
realPixelRatio: 1,
};
},
 
Loading
Loading
@@ -87,10 +87,7 @@
},
 
innerViewBox() {
if ((this.baseGraphWidth - 150) > 0) {
return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
}
return '0 0 0 0';
return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
},
 
axisTransform() {
Loading
Loading
@@ -102,6 +99,10 @@
paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
};
},
deploymentFlagData() {
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
},
},
 
methods: {
Loading
Loading
@@ -122,6 +123,10 @@
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight;
this.baseGraphWidth = this.graphWidth;
// pixel offsets inside the svg and outside are not 1:1
this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth);
this.renderAxesPaths();
this.formatDeployments();
},
Loading
Loading
@@ -261,6 +266,11 @@
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
<graph-deployment
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
Loading
Loading
@@ -269,24 +279,21 @@
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
<graph-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-width="graphWidth"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<graph-flag
v-if="showFlag"
:current-x-coordinate="currentXCoordinate"
:current-data="currentData"
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
/>
</svg>
</svg>
<graph-flag
:real-pixel-ratio="realPixelRatio"
:current-x-coordinate="currentXCoordinate"
:current-data="currentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
/>
</div>
</div>
</template>
<script>
import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters';
import Icon from '../../../vue_shared/components/icon.vue';
export default {
props: {
showDeployInfo: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
Loading
Loading
@@ -20,14 +13,6 @@
type: Number,
required: true,
},
graphWidth: {
type: Number,
required: true,
},
},
components: {
Icon,
},
 
computed: {
Loading
Loading
@@ -37,52 +22,17 @@
},
 
methods: {
refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 8);
},
formatTime(deploymentTime) {
return timeFormat(deploymentTime);
},
formatDate(deploymentTime) {
return dateFormatWithName(deploymentTime);
},
nameDeploymentClass(deployment) {
return `deploy-info-${deployment.id}`;
},
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
},
positionFlag(deployment) {
let xPosition = 3;
if (deployment.xPos > (this.graphWidth - 225)) {
xPosition = -142;
}
return xPosition;
},
svgContainerHeight(tag) {
let svgHeight = 80;
if (!tag) {
svgHeight -= 20;
}
return svgHeight;
return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
},
},
};
</script>
<template>
<g
class="deploy-info"
v-if="showDeployInfo">
<g class="deploy-info">
<g
v-for="(deployment, index) in deploymentData"
:key="index"
:class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)">
<rect
x="0"
Loading
Loading
@@ -99,81 +49,6 @@
:y2="calculatedHeight"
stroke="#000">
</line>
<svg
v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box"
:x="positionFlag(deployment)"
y="0"
width="134"
:height="svgContainerHeight(deployment.tag)">
<rect
class="rect-text-metric deploy-info-rect rect-metric"
x="1"
y="1"
rx="2"
width="132"
:height="svgContainerHeight(deployment.tag) - 2">
</rect>
<text
class="deploy-info-text text-metric-bold"
transform="translate(5, 2)">
Deployed
</text>
<!--The date info-->
<g transform="translate(5, 20)">
<text class="deploy-info-text">
{{formatDate(deployment.time)}}
</text>
<text
class="deploy-info-text text-metric-bold"
x="62">
{{formatTime(deployment.time)}}
</text>
</g>
<line
class="divider-line"
x1="0"
y1="38"
x2="132"
:y2="38"
stroke="#000">
</line>
<!--Commit information-->
<g transform="translate(5, 40)">
<icon
name="commit"
:width="12"
:height="12"
:y="3">
</icon>
<a :xlink:href="deployment.commitUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)">
{{refText(deployment)}}
</text>
</a>
</g>
<!--Tag information-->
<g
transform="translate(5, 55)"
v-if="deployment.tag">
<icon
name="label"
:width="12"
:height="12"
:y="5">
</icon>
<a :xlink:href="deployment.tagUrl">
<text
class="deploy-info-text deploy-info-text-link"
transform="translate(20, 2)"
y="2">
{{deployment.tag}}
</text>
</a>
</g>
</svg>
</g>
<svg
height="0"
Loading
Loading
<script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import Icon from '../../../vue_shared/components/icon.vue';
 
export default {
props: {
Loading
Loading
@@ -7,14 +9,15 @@
type: Number,
required: true,
},
currentFlagPosition: {
type: Number,
required: true,
},
currentData: {
type: Object,
required: true,
},
deploymentFlagData: {
type: Object,
required: false,
default: null,
},
graphHeight: {
type: Number,
required: true,
Loading
Loading
@@ -23,71 +26,173 @@
type: Number,
required: true,
},
realPixelRatio: {
type: Number,
required: true,
},
showFlagContent: {
type: Boolean,
required: true,
},
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: {
type: String,
required: true,
},
},
 
data() {
return {
circleColorRgb: '#8fbce8',
};
components: {
Icon,
},
 
computed: {
formatTime() {
return timeFormat(this.currentData.time);
return this.deploymentFlagData ?
timeFormat(this.deploymentFlagData.time) :
timeFormat(this.currentData.time);
},
 
formatDate() {
return dateFormat(this.currentData.time);
return this.deploymentFlagData ?
dateFormat(this.deploymentFlagData.time) :
dateFormat(this.currentData.time);
},
cursorStyle() {
const xCoordinate = this.deploymentFlagData ?
this.deploymentFlagData.xPos :
this.currentXCoordinate;
const offsetTop = 20 * this.realPixelRatio;
const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
return {
top: `${offsetTop}px`,
left: `${offsetLeft}px`,
height: `${height}px`,
};
},
flagOrientation() {
if (this.currentXCoordinate * this.realPixelRatio > 120) {
return 'left';
}
return 'right';
},
},
methods: {
seriesMetricValue(series) {
const index = this.deploymentFlagData ?
this.deploymentFlagData.seriesIndex :
this.currentDataIndex;
const value = series.values[index] &&
series.values[index].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
},
 
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
seriesMetricLabel(index, series) {
if (this.timeSeries.length < 2) {
return this.legendTitle;
}
if (series.metricTag) {
return series.metricTag;
}
return `series ${index + 1}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
},
},
};
</script>
<template>
<g class="mouse-over-flag">
<line
class="selected-metric-line"
:x1="currentXCoordinate"
:y1="0"
:x2="currentXCoordinate"
:y2="calculatedHeight"
transform="translate(-5, 20)">
</line>
<svg
<div
class="prometheus-graph-cursor"
:style="cursorStyle"
>
<div
v-if="showFlagContent"
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
<rect
class="rect-metric"
x="4"
y="1"
rx="2"
width="90"
height="40"
transform="translate(-3, 20)">
</rect>
<text
class="text-metric text-metric-bold"
x="16"
y="35"
transform="translate(-5, 20)">
{{formatTime}}
</text>
<text
class="text-metric"
x="16"
y="15"
transform="translate(-5, 20)">
{{formatDate}}
</text>
</svg>
</g>
class="prometheus-graph-flag popover"
:class="flagOrientation"
>
<div class="arrow"></div>
<div class="popover-title">
<h5 v-if="this.deploymentFlagData">
Deployed
</h5>
{{formatDate}} at
<strong>{{formatTime}}</strong>
</div>
<div
v-if="this.deploymentFlagData"
class="popover-content deploy-meta-content"
>
<div>
<icon
name="commit"
:size="12">
</icon>
<a :href="deploymentFlagData.commitUrl">
{{deploymentFlagData.sha.slice(0, 8)}}
</a>
</div>
<div
v-if="deploymentFlagData.tag">
<icon
name="label"
:size="12">
</icon>
<a :href="deploymentFlagData.tagUrl">
{{deploymentFlagData.ref}}
</a>
</div>
</div>
<div class="popover-content">
<table>
<tr
v-for="(series, index) in timeSeries"
:key="index"
>
<td>
<svg width="15" height="6">
<line
:stroke="series.lineColor"
:stroke-dasharray="strokeDashArray(series.lineStyle)"
stroke-width="4"
x1="0"
x2="15"
y1="2"
y2="2">
</line>
</svg>
</td>
<td>{{seriesMetricLabel(index, series)}}</td>
<td>
<strong>{{seriesMetricValue(series)}}</strong>
</td>
</tr>
</table>
</div>
</div>
</div>
</template>
Loading
Loading
@@ -29,15 +29,18 @@ const mixins = {
time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
 
if (xPos >= 0) {
const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1);
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
tag: deployment.tag,
tagUrl: `${this.tagsPath}/${deployment.tag}`,
tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
ref: deployment.ref.name,
xPos,
seriesIndex,
showDeploymentFlag: false,
});
}
Loading
Loading
Loading
Loading
@@ -14,7 +14,7 @@ const d3 = {
timeYear,
};
 
export const dateFormat = d3.time('%b %-d, %Y');
export const dateFormat = d3.time('%a, %b %-d');
export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
Loading
Loading
Loading
Loading
@@ -248,6 +248,73 @@
}
}
 
.prometheus-graph-cursor {
position: absolute;
background: $theme-gray-600;
width: 1px;
}
.prometheus-graph-flag {
display: block;
min-width: 160px;
h5 {
padding: 0;
margin: 0;
font-size: 14px;
line-height: 1.2;
}
table {
border-collapse: collapse;
padding: 0;
margin: 0;
}
td {
vertical-align: middle;
+ td {
padding-left: 5px;
vertical-align: top;
}
}
.deploy-meta-content {
border-bottom: 1px solid $white-dark;
svg {
height: 15px;
vertical-align: bottom;
}
}
&.popover {
&.left {
left: auto;
right: 0;
margin-right: 10px;
}
&.right {
left: 0;
right: auto;
margin-left: 10px;
}
> .arrow {
top: 40px;
}
> .popover-title,
> .popover-content {
padding: 5px 8px;
font-size: 12px;
white-space: nowrap;
}
}
}
.prometheus-svg-container {
position: relative;
height: 0;
Loading
Loading
---
title: Display graph values on hover within monitoring page
merge_request: 16261
author:
type: changed
Loading
Loading
@@ -11,168 +11,38 @@ const createComponent = (propsData) => {
};
 
describe('MonitoringDeployment', () => {
const reducedDeploymentData = [deploymentData[0]];
reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name;
reducedDeploymentData[0].xPos = 10;
reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at);
describe('Methods', () => {
it('refText shows the ref when a tag is available', () => {
reducedDeploymentData[0].tag = '1.0';
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.refText(reducedDeploymentData[0]),
).toEqual(reducedDeploymentData[0].ref);
});
it('refText shows the sha when no tag is available', () => {
reducedDeploymentData[0].tag = null;
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.refText(reducedDeploymentData[0]),
).toContain('f5bcd1');
});
it('nameDeploymentClass creates a class with the prefix deploy-info-', () => {
it('should contain a hidden gradient', () => {
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
showDeployInfo: true,
deploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
 
expect(
component.nameDeploymentClass(reducedDeploymentData[0]),
).toContain('deploy-info');
expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
});
 
it('transformDeploymentGroup translates an available deployment', () => {
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
deploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
 
expect(
component.transformDeploymentGroup(reducedDeploymentData[0]),
component.transformDeploymentGroup({ xPos: 16 }),
).toContain('translate(11, 20)');
});
 
it('hides the deployment flag', () => {
reducedDeploymentData[0].showDeploymentFlag = false;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull();
});
it('positions the flag to the left when the xPos is too far right', () => {
reducedDeploymentData[0].showDeploymentFlag = false;
reducedDeploymentData[0].xPos = 250;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphWidth: 440,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.positionFlag(reducedDeploymentData[0]),
).toBeLessThan(0);
});
it('shows the deployment flag', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.$el.querySelector('.js-deploy-info-box').style.display,
).not.toEqual('display: none;');
});
it('contains date, refs and the "deployed" text', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.$el.querySelectorAll('.deploy-info-text'),
).toContainText('Deployed');
expect(
component.$el.querySelectorAll('.deploy-info-text'),
).toContainText('Wed, May 31');
expect(
component.$el.querySelectorAll('.deploy-info-text'),
).toContainText(component.refText(reducedDeploymentData[0]));
});
it('contains a link to the commit contents', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(
component.$el.querySelectorAll('.deploy-info-text-link')[0].parentElement.getAttribute('xlink:href'),
).not.toEqual('');
});
it('should contain a hidden gradient', () => {
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
});
expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
});
describe('Computed props', () => {
it('calculatedHeight', () => {
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
deploymentData,
graphHeight: 300,
graphWidth: 440,
graphHeightOffset: 120,
Loading
Loading
import Vue from 'vue';
import GraphFlag from '~/monitoring/components/graph/flag.vue';
import { deploymentData } from '../mock_data';
 
const createComponent = (propsData) => {
const Component = Vue.extend(GraphFlag);
Loading
Loading
@@ -9,11 +10,6 @@ const createComponent = (propsData) => {
}).$mount();
};
 
function getCoordinate(component, selector, coordinate) {
const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate);
return parseInt(coordinateVal, 10);
}
const defaultValuesComponent = {
currentXCoordinate: 200,
currentYCoordinate: 100,
Loading
Loading
@@ -25,31 +21,111 @@ const defaultValuesComponent = {
graphHeight: 300,
graphHeightOffset: 120,
showFlagContent: true,
realPixelRatio: 1,
timeSeries: [{
values: [{
time: new Date('2017-06-04T18:17:33.501Z'),
value: '1.49609375',
}],
}],
unitOfDisplay: 'ms',
currentDataIndex: 0,
legendTitle: 'Average',
};
const deploymentFlagData = {
...deploymentData[0],
ref: deploymentData[0].ref.name,
xPos: 10,
time: new Date(deploymentData[0].created_at),
};
 
describe('GraphFlag', () => {
it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => {
const component = createComponent(defaultValuesComponent);
let component;
 
expect(getCoordinate(component, '.selected-metric-line', 'x1'))
.toEqual(component.currentXCoordinate);
expect(getCoordinate(component, '.selected-metric-line', 'x2'))
.toEqual(component.currentXCoordinate);
it('has a line at the currentXCoordinate', () => {
component = createComponent(defaultValuesComponent);
expect(component.$el.style.left)
.toEqual(`${70 + component.currentXCoordinate}px`);
});
 
it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => {
const component = createComponent(defaultValuesComponent);
describe('Deployment flag', () => {
it('shows a deployment flag when deployment data provided', () => {
const deploymentFlagComponent = createComponent({
...defaultValuesComponent,
deploymentFlagData,
});
expect(
deploymentFlagComponent.$el.querySelector('.popover-title'),
).toContainText('Deployed');
});
it('contains the ref when a tag is available', () => {
const deploymentFlagComponent = createComponent({
...defaultValuesComponent,
deploymentFlagData: {
...deploymentFlagData,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
tag: true,
ref: '1.0',
},
});
expect(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
).toContainText('f5bcd1d9');
expect(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
).toContainText('1.0');
});
it('does not contain the ref when a tag is unavailable', () => {
const deploymentFlagComponent = createComponent({
...defaultValuesComponent,
deploymentFlagData: {
...deploymentFlagData,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
tag: false,
ref: '1.0',
},
});
expect(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
).toContainText('f5bcd1d9');
 
const svg = component.$el.querySelector('.rect-text-metric');
expect(svg.tagName).toEqual('svg');
expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition);
expect(
deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
).not.toContainText('1.0');
});
});
 
describe('Computed props', () => {
it('calculatedHeight', () => {
const component = createComponent(defaultValuesComponent);
beforeEach(() => {
component = createComponent(defaultValuesComponent);
});
it('formatTime', () => {
expect(component.formatTime).toMatch(/\d:17PM/);
});
it('formatDate', () => {
expect(component.formatDate).toEqual('Sun, Jun 4');
});
it('cursorStyle', () => {
expect(component.cursorStyle).toEqual({
top: '20px',
left: '270px',
height: '180px',
});
});
 
expect(component.calculatedHeight).toEqual(180);
it('flagOrientation', () => {
expect(component.flagOrientation).toEqual('left');
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment