Add Prometheus memory sparkline to MR widget
This commit is contained in:
parent
7ea12d8067
commit
f66006169b
|
|
@ -108,8 +108,6 @@ export default {
|
|||
</div>
|
||||
<mr-widget-memory-usage
|
||||
v-if="deployment.metrics_url"
|
||||
:mr="mr"
|
||||
:service="service"
|
||||
:metricsUrl="deployment.metrics_url"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import MRWidgetService from '../services/mr_widget_service';
|
|||
export default {
|
||||
name: 'MemoryUsage',
|
||||
props: {
|
||||
mr: { type: Object, required: true },
|
||||
service: { type: Object, required: true },
|
||||
metricsUrl: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
|
|
@ -14,6 +12,7 @@ export default {
|
|||
// memoryFrom: 0,
|
||||
// memoryTo: 0,
|
||||
memoryMetrics: [],
|
||||
deploymentTime: 0,
|
||||
hasMetrics: false,
|
||||
loadFailed: false,
|
||||
loadingMetrics: true,
|
||||
|
|
@ -23,8 +22,22 @@ export default {
|
|||
components: {
|
||||
'mr-memory-graph': MemoryGraph,
|
||||
},
|
||||
computed: {
|
||||
shouldShowLoading() {
|
||||
return this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
|
||||
},
|
||||
shouldShowMemoryGraph() {
|
||||
return !this.loadingMetrics && this.hasMetrics && !this.loadFailed;
|
||||
},
|
||||
shouldShowLoadFailure() {
|
||||
return !this.loadingMetrics && !this.hasMetrics && this.loadFailed;
|
||||
},
|
||||
shouldShowMetricsUnavailable() {
|
||||
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
computeGraphData(metrics) {
|
||||
computeGraphData(metrics, deploymentTime) {
|
||||
this.loadingMetrics = false;
|
||||
const { memory_values } = metrics;
|
||||
// if (memory_previous.length > 0) {
|
||||
|
|
@ -38,70 +51,73 @@ export default {
|
|||
if (memory_values.length > 0) {
|
||||
this.hasMetrics = true;
|
||||
this.memoryMetrics = memory_values[0].values;
|
||||
this.deploymentTime = deploymentTime;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$props.loadingMetrics = true;
|
||||
gl.utils.backOff((next, stop) => {
|
||||
MRWidgetService.fetchMetrics(this.$props.metricsUrl)
|
||||
.then((res) => {
|
||||
if (res.status === statusCodes.NO_CONTENT) {
|
||||
this.backOffRequestCounter = this.backOffRequestCounter += 1;
|
||||
if (this.backOffRequestCounter < 3) {
|
||||
next();
|
||||
loadMetrics() {
|
||||
gl.utils.backOff((next, stop) => {
|
||||
MRWidgetService.fetchMetrics(this.metricsUrl)
|
||||
.then((res) => {
|
||||
if (res.status === statusCodes.NO_CONTENT) {
|
||||
this.backOffRequestCounter = this.backOffRequestCounter += 1;
|
||||
/* eslint-disable no-unused-expressions */
|
||||
this.backOffRequestCounter < 3 ? next() : stop(res);
|
||||
} else {
|
||||
stop(res);
|
||||
}
|
||||
} else {
|
||||
stop(res);
|
||||
})
|
||||
.catch(stop);
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === statusCodes.NO_CONTENT) {
|
||||
return res;
|
||||
}
|
||||
})
|
||||
.catch(stop);
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === statusCodes.NO_CONTENT) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((res) => {
|
||||
this.computeGraphData(res.metrics);
|
||||
return res;
|
||||
})
|
||||
.catch(() => {
|
||||
this.$props.loadFailed = true;
|
||||
});
|
||||
return res.json();
|
||||
})
|
||||
.then((res) => {
|
||||
this.computeGraphData(res.metrics, res.deployment_time);
|
||||
return res;
|
||||
})
|
||||
.catch(() => {
|
||||
this.loadFailed = true;
|
||||
this.loadingMetrics = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadingMetrics = true;
|
||||
this.loadMetrics();
|
||||
},
|
||||
template: `
|
||||
<div class="mr-info-list mr-memory-usage">
|
||||
<div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
|
||||
<div class="legend"></div>
|
||||
<p
|
||||
v-if="loadingMetrics"
|
||||
class="usage-info usage-info-loading">
|
||||
v-if="shouldShowLoading"
|
||||
class="usage-info js-usage-info usage-info-loading">
|
||||
<i
|
||||
class="fa fa-spinner fa-spin usage-info-load-spinner"
|
||||
aria-hidden="true" />Loading deployment statistics.
|
||||
</p>
|
||||
<p
|
||||
v-if="!hasMetrics && !loadingMetrics"
|
||||
class="usage-info usage-info-loading">
|
||||
Deployment statistics are not available currently.
|
||||
</p>
|
||||
<p
|
||||
v-if="hasMetrics"
|
||||
class="usage-info">
|
||||
v-if="shouldShowMemoryGraph"
|
||||
class="usage-info js-usage-info">
|
||||
Deployment memory usage:
|
||||
</p>
|
||||
<p
|
||||
v-if="loadFailed"
|
||||
class="usage-info">
|
||||
v-if="shouldShowLoadFailure"
|
||||
class="usage-info js-usage-info usage-info-failed">
|
||||
Failed to load deployment statistics.
|
||||
</p>
|
||||
<p
|
||||
v-if="shouldShowMetricsUnavailable"
|
||||
class="usage-info js-usage-info usage-info-unavailable">
|
||||
Deployment statistics are not available currently.
|
||||
</p>
|
||||
<mr-memory-graph
|
||||
v-if="hasMetrics"
|
||||
v-if="shouldShowMemoryGraph"
|
||||
:metrics="memoryMetrics"
|
||||
:deploymentTime="deploymentTime"
|
||||
height="25"
|
||||
width="100" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export default {
|
|||
name: 'MemoryGraph',
|
||||
props: {
|
||||
metrics: { type: Array, required: true },
|
||||
deploymentTime: { type: Number, required: true },
|
||||
width: { type: String, required: true },
|
||||
height: { type: String, required: true },
|
||||
},
|
||||
|
|
@ -9,27 +10,105 @@ export default {
|
|||
return {
|
||||
pathD: '',
|
||||
pathViewBox: '',
|
||||
// dotX: '',
|
||||
// dotY: '',
|
||||
dotX: '',
|
||||
dotY: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
getFormattedMedian() {
|
||||
const deployedSince = gl.utils.getTimeago().format(this.deploymentTime * 1000);
|
||||
return `Deployed ${deployedSince}`;
|
||||
},
|
||||
},
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const renderData = this.$props.metrics.map(v => v[1]);
|
||||
const maxMemory = Math.max.apply(null, renderData);
|
||||
const minMemory = Math.min.apply(null, renderData);
|
||||
const diff = maxMemory - minMemory;
|
||||
// const cx = 0;
|
||||
// const cy = 0;
|
||||
const lineWidth = renderData.length;
|
||||
const linePath = renderData.map((y, x) => `${x} ${maxMemory - y}`);
|
||||
this.pathD = `M ${linePath}`;
|
||||
this.pathViewBox = `0 0 ${lineWidth} ${diff}`;
|
||||
this.renderGraph(this.deploymentTime, this.metrics);
|
||||
},
|
||||
template: `
|
||||
<div class="memory-graph-container">
|
||||
<svg :width="width" :height="height" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="has-tooltip" :title="getFormattedMedian" :width="width" :height="height" xmlns="http://www.w3.org/2000/svg">
|
||||
<path :d="pathD" :viewBox="pathViewBox" />
|
||||
<!--<circle r="0.8" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" /> -->
|
||||
<circle r="1.5" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" />
|
||||
</svg>
|
||||
</div>
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
.memory-graph-container {
|
||||
svg {
|
||||
background: $white-light;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 4px $gray-darkest inset;
|
||||
}
|
||||
}
|
||||
|
||||
path {
|
||||
fill: none;
|
||||
stroke: $blue-500;
|
||||
stroke-width: 1px;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
circle {
|
||||
stroke: $blue-700;
|
||||
fill: $blue-700;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,8 +182,7 @@
|
|||
}
|
||||
|
||||
&.mr-memory-usage {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin: 5px 0 10px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -511,7 +510,12 @@
|
|||
|
||||
.mr-info-list.mr-memory-usage {
|
||||
.legend {
|
||||
height: 75%;
|
||||
height: 65%;
|
||||
top: 0;
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
|
|
@ -731,13 +735,15 @@
|
|||
}
|
||||
|
||||
.mr-memory-usage {
|
||||
p.usage-info-loading {
|
||||
margin-bottom: 6px;
|
||||
p.usage-info-loading,
|
||||
p.usage-info-unavailable,
|
||||
p.usage-info-failed {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.usage-info-load-spinner {
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
p.usage-info-loading .usage-info-load-spinner {
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-md-min) {
|
||||
|
|
|
|||
|
|
@ -410,10 +410,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
|
||||
metrics_url =
|
||||
if can?(current_user, :read_environment, environment) && environment.has_metrics?
|
||||
metrics_namespace_project_environment_path(environment.project.namespace,
|
||||
environment.project,
|
||||
environment,
|
||||
deployment)
|
||||
metrics_namespace_project_environment_deployment_path(environment.project.namespace,
|
||||
environment.project,
|
||||
environment,
|
||||
deployment)
|
||||
end
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const deploymentMockData = [
|
|||
name: 'review/diplo',
|
||||
url: '/root/acets-review-apps/environments/15',
|
||||
stop_url: '/root/acets-review-apps/environments/15/stop',
|
||||
metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
|
||||
external_url: 'http://diplo.',
|
||||
external_url_formatted: 'diplo.',
|
||||
deployed_at: '2017-03-22T22:44:42.258Z',
|
||||
|
|
@ -156,6 +157,7 @@ describe('MRWidgetDeployment', () => {
|
|||
expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deployment.external_url);
|
||||
expect(el.querySelector('.js-deploy-url').innerText).toContain(deployment.external_url_formatted);
|
||||
expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.formatDate(deployment.deployed_at));
|
||||
expect(el.querySelector('.js-mr-memory-usage')).toBeDefined();
|
||||
expect(el.querySelector('button')).toBeDefined();
|
||||
});
|
||||
|
||||
|
|
@ -165,6 +167,7 @@ describe('MRWidgetDeployment', () => {
|
|||
|
||||
Vue.nextTick(() => {
|
||||
expect(el.querySelectorAll('.ci-widget').length).toEqual(3);
|
||||
expect(el.querySelectorAll('.js-mr-memory-usage').length).toEqual(3);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
@ -176,6 +179,7 @@ describe('MRWidgetDeployment', () => {
|
|||
expect(el.querySelectorAll('.js-deploy-meta').length).toEqual(0);
|
||||
expect(el.querySelectorAll('.js-deploy-url').length).toEqual(0);
|
||||
expect(el.querySelectorAll('.js-deploy-time').length).toEqual(0);
|
||||
expect(el.querySelectorAll('.js-mr-memory-usage').length).toEqual(0);
|
||||
expect(el.querySelectorAll('.button').length).toEqual(0);
|
||||
done();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
import Vue from 'vue';
|
||||
import memoryUsageComponent from '~/vue_merge_request_widget/components/mr_widget_memory_usage';
|
||||
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
|
||||
|
||||
const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
|
||||
|
||||
const metricsMockData = {
|
||||
success: true,
|
||||
metrics: {
|
||||
memory_values: [
|
||||
{
|
||||
metric: {},
|
||||
values: [
|
||||
[1493716685, '4.30859375'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
last_update: '2017-05-02T12:34:49.628Z',
|
||||
deployment_time: 1493718485,
|
||||
};
|
||||
|
||||
const createComponent = () => {
|
||||
const Component = Vue.extend(memoryUsageComponent);
|
||||
|
||||
return new Component({
|
||||
el: document.createElement('div'),
|
||||
propsData: {
|
||||
metricsUrl: url,
|
||||
memoryMetrics: [],
|
||||
deploymentTime: 0,
|
||||
hasMetrics: false,
|
||||
loadFailed: false,
|
||||
loadingMetrics: true,
|
||||
backOffRequestCounter: 0,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const messages = {
|
||||
loadingMetrics: 'Loading deployment statistics.',
|
||||
hasMetrics: 'Deployment memory usage:',
|
||||
loadFailed: 'Failed to load deployment statistics.',
|
||||
metricsUnavailable: 'Deployment statistics are not available currently.',
|
||||
};
|
||||
|
||||
describe('MemoryUsage', () => {
|
||||
let vm;
|
||||
let el;
|
||||
|
||||
beforeEach(() => {
|
||||
vm = createComponent();
|
||||
el = vm.$el;
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
it('should have props with defaults', () => {
|
||||
const { metricsUrl } = memoryUsageComponent.props;
|
||||
const MetricsUrlTypeClass = metricsUrl.type;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(new MetricsUrlTypeClass() instanceof String).toBeTruthy();
|
||||
expect(metricsUrl.required).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data', () => {
|
||||
it('should have default data', () => {
|
||||
const data = memoryUsageComponent.data();
|
||||
|
||||
expect(Array.isArray(data.memoryMetrics)).toBeTruthy();
|
||||
expect(data.memoryMetrics.length).toBe(0);
|
||||
|
||||
expect(typeof data.deploymentTime).toBe('number');
|
||||
expect(data.deploymentTime).toBe(0);
|
||||
|
||||
expect(typeof data.hasMetrics).toBe('boolean');
|
||||
expect(data.hasMetrics).toBeFalsy();
|
||||
|
||||
expect(typeof data.loadFailed).toBe('boolean');
|
||||
expect(data.loadFailed).toBeFalsy();
|
||||
|
||||
expect(typeof data.loadingMetrics).toBe('boolean');
|
||||
expect(data.loadingMetrics).toBeTruthy();
|
||||
|
||||
expect(typeof data.backOffRequestCounter).toBe('number');
|
||||
expect(data.backOffRequestCounter).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
const { metrics, deployment_time } = metricsMockData;
|
||||
|
||||
describe('computeGraphData', () => {
|
||||
it('should populate sparkline graph', () => {
|
||||
vm.computeGraphData(metrics, deployment_time);
|
||||
const { hasMetrics, memoryMetrics, deploymentTime } = vm;
|
||||
|
||||
expect(hasMetrics).toBeTruthy();
|
||||
expect(memoryMetrics.length > 0).toBeTruthy();
|
||||
expect(deploymentTime).toEqual(deployment_time);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadMetrics', () => {
|
||||
const returnServicePromise = () => new Promise((resolve) => {
|
||||
resolve({
|
||||
json() {
|
||||
return metricsMockData;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should load metrics data using MRWidgetService', (done) => {
|
||||
spyOn(MRWidgetService, 'fetchMetrics').and.returnValue(returnServicePromise(true));
|
||||
spyOn(vm, 'computeGraphData');
|
||||
|
||||
vm.loadMetrics();
|
||||
setTimeout(() => {
|
||||
expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
|
||||
expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
|
||||
done();
|
||||
}, 333);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('should render template elements correctly', () => {
|
||||
expect(el.classList.contains('mr-memory-usage')).toBeTruthy();
|
||||
expect(el.querySelector('.js-usage-info')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show loading metrics message while metrics are being loaded', (done) => {
|
||||
vm.loadingMetrics = true;
|
||||
vm.hasMetrics = false;
|
||||
vm.loadFailed = false;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
|
||||
expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
|
||||
expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show deployment memory usage when metrics are loaded', (done) => {
|
||||
vm.loadingMetrics = false;
|
||||
vm.hasMetrics = true;
|
||||
vm.loadFailed = false;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(el.querySelector('.memory-graph-container')).toBeDefined();
|
||||
expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show failure message when metrics loading failed', (done) => {
|
||||
vm.loadingMetrics = false;
|
||||
vm.hasMetrics = false;
|
||||
vm.loadFailed = true;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
|
||||
expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show metrics unavailable message when metrics loading failed', (done) => {
|
||||
vm.loadingMetrics = false;
|
||||
vm.hasMetrics = false;
|
||||
vm.loadFailed = false;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
|
||||
expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import Vue from 'vue';
|
||||
import memoryGraphComponent from '~/vue_shared/components/memory_graph';
|
||||
import { mockMetrics, mockMedian, mockMedianIndex } from './mock_data';
|
||||
|
||||
const defaultHeight = '25';
|
||||
const defaultWidth = '100';
|
||||
|
||||
const createComponent = () => {
|
||||
const Component = Vue.extend(memoryGraphComponent);
|
||||
|
||||
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('props', () => {
|
||||
it('should have props with defaults', (done) => {
|
||||
const { metrics, deploymentTime, width, height } = memoryGraphComponent.props;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const typeClassMatcher = (propItem, expectedType) => {
|
||||
const PropItemTypeClass = propItem.type;
|
||||
expect(new PropItemTypeClass() instanceof expectedType).toBeTruthy();
|
||||
expect(propItem.required).toBeTruthy();
|
||||
};
|
||||
|
||||
typeClassMatcher(metrics, Array);
|
||||
typeClassMatcher(deploymentTime, Number);
|
||||
typeClassMatcher(width, String);
|
||||
typeClassMatcher(height, String);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data', () => {
|
||||
it('should have default data', () => {
|
||||
const data = memoryGraphComponent.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') > -1).toBeTruthy();
|
||||
expect(formattedMedian.indexOf('ago') > -1).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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('tranform')).toBe('translate(0 -1)');
|
||||
expect(circleEl.getAttribute('cx')).toBe(`${dotX}`);
|
||||
expect(circleEl.getAttribute('cy')).toBe(`${dotY}`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/* eslint-disable */
|
||||
|
||||
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;
|
||||
Loading…
Reference in New Issue