Merge branch 'jivl-add-empty-state-graphs-null-values' into 'master'
Add empty state for graphs with no values Closes #53018 See merge request gitlab-org/gitlab-ce!22630
This commit is contained in:
commit
a6664ad61d
7 changed files with 149 additions and 26 deletions
|
@ -105,6 +105,9 @@ export default {
|
||||||
deploymentFlagData() {
|
deploymentFlagData() {
|
||||||
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
|
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
|
||||||
},
|
},
|
||||||
|
shouldRenderData() {
|
||||||
|
return this.graphData.queries.filter(s => s.result.length > 0).length > 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
hoverData() {
|
hoverData() {
|
||||||
|
@ -120,17 +123,17 @@ export default {
|
||||||
},
|
},
|
||||||
draw() {
|
draw() {
|
||||||
const breakpointSize = bp.getBreakpointSize();
|
const breakpointSize = bp.getBreakpointSize();
|
||||||
const query = this.graphData.queries[0];
|
|
||||||
const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
|
const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
|
||||||
|
|
||||||
this.margin = measurements.large.margin;
|
this.margin = measurements.large.margin;
|
||||||
|
|
||||||
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
|
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
|
||||||
this.graphHeight = 300;
|
this.graphHeight = 300;
|
||||||
this.margin = measurements.small.margin;
|
this.margin = measurements.small.margin;
|
||||||
this.measurements = measurements.small;
|
this.measurements = measurements.small;
|
||||||
}
|
}
|
||||||
this.unitOfDisplay = query.unit || '';
|
|
||||||
this.yAxisLabel = this.graphData.y_label || 'Values';
|
this.yAxisLabel = this.graphData.y_label || 'Values';
|
||||||
this.legendTitle = query.label || 'Average';
|
|
||||||
this.graphWidth = svgWidth - this.margin.left - this.margin.right;
|
this.graphWidth = svgWidth - this.margin.left - this.margin.right;
|
||||||
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
|
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
|
||||||
this.baseGraphHeight = this.graphHeight - 50;
|
this.baseGraphHeight = this.graphHeight - 50;
|
||||||
|
@ -139,8 +142,15 @@ export default {
|
||||||
// pixel offsets inside the svg and outside are not 1:1
|
// pixel offsets inside the svg and outside are not 1:1
|
||||||
this.realPixelRatio = svgWidth / this.baseGraphWidth;
|
this.realPixelRatio = svgWidth / this.baseGraphWidth;
|
||||||
|
|
||||||
this.renderAxesPaths();
|
// set the legends on the axes
|
||||||
this.formatDeployments();
|
const [query] = this.graphData.queries;
|
||||||
|
this.legendTitle = query ? query.label : 'Average';
|
||||||
|
this.unitOfDisplay = query ? query.unit : '';
|
||||||
|
|
||||||
|
if (this.shouldRenderData) {
|
||||||
|
this.renderAxesPaths();
|
||||||
|
this.formatDeployments();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleMouseOverGraph(e) {
|
handleMouseOverGraph(e) {
|
||||||
let point = this.$refs.graphData.createSVGPoint();
|
let point = this.$refs.graphData.createSVGPoint();
|
||||||
|
@ -266,7 +276,7 @@ export default {
|
||||||
:y-axis-label="yAxisLabel"
|
:y-axis-label="yAxisLabel"
|
||||||
:unit-of-display="unitOfDisplay"
|
:unit-of-display="unitOfDisplay"
|
||||||
/>
|
/>
|
||||||
<svg ref="graphData" :viewBox="innerViewBox" class="graph-data">
|
<svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data">
|
||||||
<slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
|
<slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
|
||||||
<graph-path
|
<graph-path
|
||||||
v-for="(path, index) in timeSeries"
|
v-for="(path, index) in timeSeries"
|
||||||
|
@ -293,8 +303,14 @@ export default {
|
||||||
@mousemove="handleMouseOverGraph($event);"
|
@mousemove="handleMouseOverGraph($event);"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
<svg v-else :viewBox="innerViewBox" class="js-no-data-to-display">
|
||||||
|
<text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">
|
||||||
|
{{ s__('Metrics|No data to display') }}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
</svg>
|
</svg>
|
||||||
<graph-flag
|
<graph-flag
|
||||||
|
v-if="shouldRenderData"
|
||||||
:real-pixel-ratio="realPixelRatio"
|
:real-pixel-ratio="realPixelRatio"
|
||||||
:current-x-coordinate="currentXCoordinate"
|
:current-x-coordinate="currentXCoordinate"
|
||||||
:current-data="currentData"
|
:current-data="currentData"
|
||||||
|
|
|
@ -7,10 +7,29 @@ function sortMetrics(metrics) {
|
||||||
.value();
|
.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkQueryEmptyData(query) {
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
result: query.result.filter(timeSeries => {
|
||||||
|
const newTimeSeries = timeSeries;
|
||||||
|
const hasValue = series =>
|
||||||
|
!Number.isNaN(series.value) && (series.value !== null || series.value !== undefined);
|
||||||
|
const hasNonNullValue = timeSeries.values.find(hasValue);
|
||||||
|
|
||||||
|
newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
|
||||||
|
|
||||||
|
return newTimeSeries.values.length > 0;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTimeSeriesNoData(queries) {
|
||||||
|
return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeMetrics(metrics) {
|
function normalizeMetrics(metrics) {
|
||||||
return metrics.map(metric => ({
|
return metrics.map(metric => {
|
||||||
...metric,
|
const queries = metric.queries.map(query => ({
|
||||||
queries: metric.queries.map(query => ({
|
|
||||||
...query,
|
...query,
|
||||||
result: query.result.map(result => ({
|
result: query.result.map(result => ({
|
||||||
...result,
|
...result,
|
||||||
|
@ -19,8 +38,13 @@ function normalizeMetrics(metrics) {
|
||||||
value: Number(value),
|
value: Number(value),
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
})),
|
}));
|
||||||
}));
|
|
||||||
|
return {
|
||||||
|
...metric,
|
||||||
|
queries: removeTimeSeriesNoData(queries),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MonitoringStore {
|
export default class MonitoringStore {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add empty state for graphs with no values
|
||||||
|
merge_request: 22630
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -4067,6 +4067,9 @@ msgstr ""
|
||||||
msgid "Metrics|Learn about environments"
|
msgid "Metrics|Learn about environments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Metrics|No data to display"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Metrics|No deployed environments"
|
msgid "Metrics|No deployed environments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
deploymentData,
|
deploymentData,
|
||||||
convertDatesMultipleSeries,
|
convertDatesMultipleSeries,
|
||||||
singleRowMetricsMultipleSeries,
|
singleRowMetricsMultipleSeries,
|
||||||
|
queryWithoutData,
|
||||||
} from './mock_data';
|
} from './mock_data';
|
||||||
|
|
||||||
const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags';
|
const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags';
|
||||||
|
@ -104,4 +105,23 @@ describe('Graph', () => {
|
||||||
|
|
||||||
expect(component.currentData).toBe(component.timeSeries[0].values[10]);
|
expect(component.currentData).toBe(component.timeSeries[0].values[10]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Without data to display', () => {
|
||||||
|
it('shows a "no data to display" empty state on a graph', done => {
|
||||||
|
const component = createComponent({
|
||||||
|
graphData: queryWithoutData,
|
||||||
|
deploymentData,
|
||||||
|
tagsPath,
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(
|
||||||
|
component.$el.querySelector('.js-no-data-to-display text').textContent.trim(),
|
||||||
|
).toEqual('No data to display');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const metricsGroupsAPIResponse = {
|
||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20',
|
query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20',
|
||||||
y_label: 'Memory',
|
label: 'Memory',
|
||||||
unit: 'MiB',
|
unit: 'MiB',
|
||||||
result: [
|
result: [
|
||||||
{
|
{
|
||||||
|
@ -324,12 +324,15 @@ export const metricsGroupsAPIResponse = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 6,
|
||||||
title: 'CPU usage',
|
title: 'CPU usage',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
query_range:
|
query_range:
|
||||||
'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100',
|
'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100',
|
||||||
|
label: 'Core Usage',
|
||||||
|
unit: 'Cores',
|
||||||
result: [
|
result: [
|
||||||
{
|
{
|
||||||
metric: {},
|
metric: {},
|
||||||
|
@ -639,6 +642,39 @@ export const metricsGroupsAPIResponse = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: 'NGINX',
|
||||||
|
priority: 2,
|
||||||
|
metrics: [
|
||||||
|
{
|
||||||
|
id: 100,
|
||||||
|
title: 'Http Error Rate',
|
||||||
|
weight: 100,
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
query_range:
|
||||||
|
'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
|
||||||
|
label: '5xx errors',
|
||||||
|
unit: '%',
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
metric: {},
|
||||||
|
values: [
|
||||||
|
[1495700554.925, NaN],
|
||||||
|
[1495700614.925, NaN],
|
||||||
|
[1495700674.925, NaN],
|
||||||
|
[1495700734.925, NaN],
|
||||||
|
[1495700794.925, NaN],
|
||||||
|
[1495700854.925, NaN],
|
||||||
|
[1495700914.925, NaN],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
last_update: '2017-05-25T13:18:34.949Z',
|
last_update: '2017-05-25T13:18:34.949Z',
|
||||||
};
|
};
|
||||||
|
@ -6526,6 +6562,21 @@ export const singleRowMetricsMultipleSeries = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const queryWithoutData = {
|
||||||
|
title: 'HTTP Error rate',
|
||||||
|
weight: 10,
|
||||||
|
y_label: 'Http Error Rate',
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
query_range:
|
||||||
|
'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
|
||||||
|
label: '5xx errors',
|
||||||
|
unit: '%',
|
||||||
|
result: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export function convertDatesMultipleSeries(multipleSeries) {
|
export function convertDatesMultipleSeries(multipleSeries) {
|
||||||
const convertedMultiple = multipleSeries;
|
const convertedMultiple = multipleSeries;
|
||||||
multipleSeries.forEach((column, index) => {
|
multipleSeries.forEach((column, index) => {
|
||||||
|
|
|
@ -1,31 +1,35 @@
|
||||||
import MonitoringStore from '~/monitoring/stores/monitoring_store';
|
import MonitoringStore from '~/monitoring/stores/monitoring_store';
|
||||||
import MonitoringMock, { deploymentData, environmentData } from './mock_data';
|
import MonitoringMock, { deploymentData, environmentData } from './mock_data';
|
||||||
|
|
||||||
describe('MonitoringStore', function() {
|
describe('MonitoringStore', () => {
|
||||||
this.store = new MonitoringStore();
|
const store = new MonitoringStore();
|
||||||
this.store.storeMetrics(MonitoringMock.data);
|
store.storeMetrics(MonitoringMock.data);
|
||||||
|
|
||||||
it('contains one group that contains two queries sorted by priority', () => {
|
it('contains two groups that contains, one of which has two queries sorted by priority', () => {
|
||||||
expect(this.store.groups).toBeDefined();
|
expect(store.groups).toBeDefined();
|
||||||
expect(this.store.groups.length).toEqual(1);
|
expect(store.groups.length).toEqual(2);
|
||||||
expect(this.store.groups[0].metrics.length).toEqual(2);
|
expect(store.groups[0].metrics.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gets the metrics count for every group', () => {
|
it('gets the metrics count for every group', () => {
|
||||||
expect(this.store.getMetricsCount()).toEqual(2);
|
expect(store.getMetricsCount()).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains deployment data', () => {
|
it('contains deployment data', () => {
|
||||||
this.store.storeDeploymentData(deploymentData);
|
store.storeDeploymentData(deploymentData);
|
||||||
|
|
||||||
expect(this.store.deploymentData).toBeDefined();
|
expect(store.deploymentData).toBeDefined();
|
||||||
expect(this.store.deploymentData.length).toEqual(3);
|
expect(store.deploymentData.length).toEqual(3);
|
||||||
expect(typeof this.store.deploymentData[0]).toEqual('object');
|
expect(typeof store.deploymentData[0]).toEqual('object');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only stores environment data that contains deployments', () => {
|
it('only stores environment data that contains deployments', () => {
|
||||||
this.store.storeEnvironmentsData(environmentData);
|
store.storeEnvironmentsData(environmentData);
|
||||||
|
|
||||||
expect(this.store.environmentsData.length).toEqual(2);
|
expect(store.environmentsData.length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the data if all the values from a query are not defined', () => {
|
||||||
|
expect(store.groups[1].metrics[0].queries[0].result.length).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue