Commit 5798d6a4 authored by Phil Hughes's avatar Phil Hughes

Merge branch '31391-update-sparkline-chart-deployment-widget' into 'master'

Improve sparkline chart in MR widget deployment

See merge request gitlab-org/gitlab!20085
parents 2d0d3ef5 72247ff2
......@@ -169,12 +169,6 @@ export default {
<p v-if="shouldShowMetricsUnavailable" class="usage-info js-usage-info usage-info-unavailable">
{{ s__('mrWidget|Deployment statistics are not available currently') }}
</p>
<memory-graph
v-if="shouldShowMemoryGraph"
:metrics="memoryMetrics"
:deployment-time="deploymentTime"
height="25"
width="100"
/>
<memory-graph v-if="shouldShowMemoryGraph" :metrics="memoryMetrics" :height="25" :width="110" />
</div>
</template>
<script>
import { __, sprintf } from '~/locale';
import { getTimeago } from '../../lib/utils/datetime_utility';
import { formatDate, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import { GlSparklineChart } from '@gitlab/ui/dist/charts';
export default {
name: 'MemoryGraph',
components: {
GlSparklineChart,
},
props: {
metrics: { type: Array, required: true },
deploymentTime: { type: Number, required: true },
width: { type: String, required: true },
height: { type: String, required: true },
},
data() {
return {
pathD: '',
pathViewBox: '',
dotX: '',
dotY: '',
};
width: { type: Number, required: true },
height: { type: Number, required: true },
},
computed: {
getFormattedMedian() {
const deployedSince = getTimeago().format(this.deploymentTime * 1000);
return sprintf(__('Deployed %{deployedSince}'), { deployedSince });
chartData() {
return this.metrics.map(([x, y]) => [
this.getFormattedDeploymentTime(x),
this.getMemoryUsage(y),
]);
},
},
mounted() {
this.renderGraph(this.deploymentTime, this.metrics);
},
methods: {
/**
* Returns metric value index in metrics array
* with timestamp closest to matching median
*/
getMedianMetricIndex(median, metrics) {
let matchIndex = 0;
let timestampDiff = 0;
let smallestDiff = 0;
const metricTimestamps = metrics.map(v => v[0]);
// Find metric timestamp which is closest to deploymentTime
timestampDiff = Math.abs(metricTimestamps[0] - median);
metricTimestamps.forEach((timestamp, index) => {
if (index === 0) {
// Skip first element
return;
}
smallestDiff = Math.abs(timestamp - median);
if (smallestDiff < timestampDiff) {
matchIndex = index;
timestampDiff = smallestDiff;
}
});
return matchIndex;
getFormattedDeploymentTime(timestamp) {
return formatDate(new Date(secondsToMilliseconds(timestamp)), 'mmm dd yyyy HH:MM:s');
},
/**
* Get Graph Plotting values to render Line and Dot
*/
getGraphPlotValues(median, metrics) {
const renderData = metrics.map(v => v[1]);
const medianMetricIndex = this.getMedianMetricIndex(median, metrics);
let cx = 0;
let cy = 0;
// Find Maximum and Minimum values from `renderData` array
const maxMemory = Math.max.apply(null, renderData);
const minMemory = Math.min.apply(null, renderData);
// Find difference between extreme ends
const diff = maxMemory - minMemory;
const lineWidth = renderData.length;
// Iterate over metrics values and perform following
// 1. Find x & y co-ords for deploymentTime's memory value
// 2. Return line path against maxMemory
const linePath = renderData.map((y, x) => {
if (medianMetricIndex === x) {
cx = x;
cy = maxMemory - y;
}
return `${x} ${maxMemory - y}`;
});
return {
pathD: linePath,
pathViewBox: {
lineWidth,
diff,
},
dotX: cx,
dotY: cy,
};
},
/**
* Render Graph based on provided median and metrics values
*/
renderGraph(median, metrics) {
const { pathD, pathViewBox, dotX, dotY } = this.getGraphPlotValues(median, metrics);
// Set props and update graph on UI.
this.pathD = `M ${pathD}`;
this.pathViewBox = `0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`;
this.dotX = dotX;
this.dotY = dotY;
getMemoryUsage(MBs) {
return Number(MBs).toFixed(2);
},
},
};
</script>
<template>
<div class="memory-graph-container">
<svg
:title="getFormattedMedian"
:width="width"
<div class="memory-graph-container p-1" :style="{ width: `${width}px` }">
<gl-sparkline-chart
:height="height"
class="has-tooltip"
xmlns="http://www.w3.org/2000/svg"
>
<path :d="pathD" :viewBox="pathViewBox" />
<circle :cx="dotX" :cy="dotY" r="1.5" transform="translate(0 -1)" />
</svg>
:tooltip-label="__('MB')"
:show-last-y-value="false"
:data="chartData"
/>
</div>
</template>
.memory-graph-container {
svg {
background: $white-light;
border: 1px solid $gray-200;
}
path {
fill: none;
stroke: $blue-500;
stroke-width: 2px;
}
circle {
stroke: $blue-700;
fill: $blue-700;
stroke-width: 4px;
}
background: $white-light;
border: 1px solid $gray-200;
}
......@@ -949,7 +949,6 @@
.deployment-info {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 100px;
......
---
title: Improve sparkline chart in MR widget deployment
merge_request: 20085
author:
type: other
......@@ -5692,9 +5692,6 @@ msgstr ""
msgid "Deployed"
msgstr ""
msgid "Deployed %{deployedSince}"
msgstr ""
msgid "Deployed to"
msgstr ""
......@@ -10390,6 +10387,9 @@ msgstr ""
msgid "Logs|To see the pod logs, deploy your code to an environment."
msgstr ""
msgid "MB"
msgstr ""
msgid "MD5"
msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MemoryGraph Render chart should draw container with chart 1`] = `
<div
class="memory-graph-container p-1"
style="width: 100px;"
>
<glsparklinechart-stub
data="Nov 12 2019 19:17:33,2.87,Nov 12 2019 19:18:33,2.78,Nov 12 2019 19:19:33,2.78,Nov 12 2019 19:20:33,3.01"
height="25"
tooltiplabel="MB"
variant="gray900"
/>
</div>
`;
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
import { GlSparklineChart } from '@gitlab/ui/dist/charts';
describe('MemoryGraph', () => {
const Component = Vue.extend(MemoryGraph);
let wrapper;
const metrics = [
[1573586253.853, '2.87'],
[1573586313.853, '2.77734375'],
[1573586373.853, '2.77734375'],
[1573586433.853, '3.0066964285714284'],
];
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
wrapper = shallowMount(Component, {
propsData: {
metrics,
width: 100,
height: 25,
},
});
});
describe('chartData', () => {
it('should calculate chartData', () => {
expect(wrapper.vm.chartData.length).toEqual(metrics.length);
});
it('should format date & MB values', () => {
const formattedData = [
['Nov 12 2019 19:17:33', '2.87'],
['Nov 12 2019 19:18:33', '2.78'],
['Nov 12 2019 19:19:33', '2.78'],
['Nov 12 2019 19:20:33', '3.01'],
];
expect(wrapper.vm.chartData).toEqual(formattedData);
});
});
describe('Render chart', () => {
it('should draw container with chart', () => {
expect(wrapper.element).toMatchSnapshot();
expect(wrapper.find('.memory-graph-container').exists()).toBe(true);
expect(wrapper.find(GlSparklineChart).exists()).toBe(true);
});
});
});
......@@ -185,6 +185,7 @@ describe('MemoryUsage', () => {
vm.loadingMetrics = false;
vm.hasMetrics = true;
vm.loadFailed = false;
vm.memoryMetrics = metricsMockData.metrics.memory_values[0].values;
Vue.nextTick(() => {
expect(el.querySelector('.memory-graph-container')).toBeDefined();
......
import Vue from 'vue';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
import { mockMetrics, mockMedian, mockMedianIndex } from './mock_data';
const defaultHeight = '25';
const defaultWidth = '100';
const createComponent = () => {
const Component = Vue.extend(MemoryGraph);
return new Component({
el: document.createElement('div'),
propsData: {
metrics: [],
deploymentTime: 0,
width: '',
height: '',
pathD: '',
pathViewBox: '',
dotX: '',
dotY: '',
},
});
};
describe('MemoryGraph', () => {
let vm;
let el;
beforeEach(() => {
vm = createComponent();
el = vm.$el;
});
describe('data', () => {
it('should have default data', () => {
const data = MemoryGraph.data();
const dataValidator = (dataItem, expectedType, defaultVal) => {
expect(typeof dataItem).toBe(expectedType);
expect(dataItem).toBe(defaultVal);
};
dataValidator(data.pathD, 'string', '');
dataValidator(data.pathViewBox, 'string', '');
dataValidator(data.dotX, 'string', '');
dataValidator(data.dotY, 'string', '');
});
});
describe('computed', () => {
describe('getFormattedMedian', () => {
it('should show human readable median value based on provided median timestamp', () => {
vm.deploymentTime = mockMedian;
const formattedMedian = vm.getFormattedMedian;
expect(formattedMedian.indexOf('Deployed')).toBeGreaterThan(-1);
expect(formattedMedian.indexOf('ago')).toBeGreaterThan(-1);
});
});
});
describe('methods', () => {
describe('getMedianMetricIndex', () => {
it('should return index of closest metric timestamp to that of median', () => {
const matchingIndex = vm.getMedianMetricIndex(mockMedian, mockMetrics);
expect(matchingIndex).toBe(mockMedianIndex);
});
});
describe('getGraphPlotValues', () => {
it('should return Object containing values to plot graph', () => {
const plotValues = vm.getGraphPlotValues(mockMedian, mockMetrics);
expect(plotValues.pathD).toBeDefined();
expect(Array.isArray(plotValues.pathD)).toBeTruthy();
expect(plotValues.pathViewBox).toBeDefined();
expect(typeof plotValues.pathViewBox).toBe('object');
expect(plotValues.dotX).toBeDefined();
expect(typeof plotValues.dotX).toBe('number');
expect(plotValues.dotY).toBeDefined();
expect(typeof plotValues.dotY).toBe('number');
});
});
});
describe('template', () => {
it('should render template elements correctly', () => {
expect(el.classList.contains('memory-graph-container')).toBeTruthy();
expect(el.querySelector('svg')).toBeDefined();
});
it('should render graph when renderGraph is called internally', done => {
const { pathD, pathViewBox, dotX, dotY } = vm.getGraphPlotValues(mockMedian, mockMetrics);
vm.height = defaultHeight;
vm.width = defaultWidth;
vm.pathD = `M ${pathD}`;
vm.pathViewBox = `0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`;
vm.dotX = dotX;
vm.dotY = dotY;
Vue.nextTick(() => {
const svgEl = el.querySelector('svg');
expect(svgEl).toBeDefined();
expect(svgEl.getAttribute('height')).toBe(defaultHeight);
expect(svgEl.getAttribute('width')).toBe(defaultWidth);
const pathEl = el.querySelector('path');
expect(pathEl).toBeDefined();
expect(pathEl.getAttribute('d')).toBe(`M ${pathD}`);
expect(pathEl.getAttribute('viewBox')).toBe(
`0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`,
);
const circleEl = el.querySelector('circle');
expect(circleEl).toBeDefined();
expect(circleEl.getAttribute('r')).toBe('1.5');
expect(circleEl.getAttribute('transform')).toBe('translate(0 -1)');
expect(circleEl.getAttribute('cx')).toBe(`${dotX}`);
expect(circleEl.getAttribute('cy')).toBe(`${dotY}`);
done();
});
});
});
});
export const mockMetrics = [
[1493716685, '4.30859375'],
[1493716745, '4.30859375'],
[1493716805, '4.30859375'],
[1493716865, '4.30859375'],
[1493716925, '4.30859375'],
[1493716985, '4.30859375'],
[1493717045, '4.30859375'],
[1493717105, '4.30859375'],
[1493717165, '4.30859375'],
[1493717225, '4.30859375'],
[1493717285, '4.30859375'],
[1493717345, '4.30859375'],
[1493717405, '4.30859375'],
[1493717465, '4.30859375'],
[1493717525, '4.30859375'],
[1493717585, '4.30859375'],
[1493717645, '4.30859375'],
[1493717705, '4.30859375'],
[1493717765, '4.30859375'],
[1493717825, '4.30859375'],
[1493717885, '4.30859375'],
[1493717945, '4.30859375'],
[1493718005, '4.30859375'],
[1493718065, '4.30859375'],
[1493718125, '4.30859375'],
[1493718185, '4.30859375'],
[1493718245, '4.30859375'],
[1493718305, '4.234375'],
[1493718365, '4.234375'],
[1493718425, '4.234375'],
[1493718485, '4.234375'],
[1493718545, '4.243489583333333'],
[1493718605, '4.2109375'],
[1493718665, '4.2109375'],
[1493718725, '4.2109375'],
[1493718785, '4.26171875'],
[1493718845, '4.26171875'],
[1493718905, '4.26171875'],
[1493718965, '4.26171875'],
[1493719025, '4.26171875'],
[1493719085, '4.26171875'],
[1493719145, '4.26171875'],
[1493719205, '4.26171875'],
[1493719265, '4.26171875'],
[1493719325, '4.26171875'],
[1493719385, '4.26171875'],
[1493719445, '4.26171875'],
[1493719505, '4.26171875'],
[1493719565, '4.26171875'],
[1493719625, '4.26171875'],
[1493719685, '4.26171875'],
[1493719745, '4.26171875'],
[1493719805, '4.26171875'],
[1493719865, '4.26171875'],
[1493719925, '4.26171875'],
[1493719985, '4.26171875'],
[1493720045, '4.26171875'],
[1493720105, '4.26171875'],
[1493720165, '4.26171875'],
[1493720225, '4.26171875'],
[1493720285, '4.26171875'],
];
export const mockMedian = 1493718485;
export const mockMedianIndex = 30;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment