This commit adds a new time series component
Adds a time series component for line and area charts. Displays new charts in the dashboard. - Use dynamic components for line/area swapping - Add new line charts to dashboard in 2 panels
This commit is contained in:
parent
0a4d4c0a58
commit
f2619e21be
9 changed files with 711 additions and 10 deletions
|
@ -12,6 +12,9 @@ import { graphDataValidatorForValues } from '../../utils';
|
|||
|
||||
let debouncedResize;
|
||||
|
||||
// TODO: Remove this component in favor of the more general time_series.vue
|
||||
// Please port all changes here to time_series.vue as well.
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAreaChart,
|
||||
|
|
|
@ -0,0 +1,334 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import { mapState } from 'vuex';
|
||||
import { GlLink, GlButton } from '@gitlab/ui';
|
||||
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
|
||||
import dateFormat from 'dateformat';
|
||||
import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
|
||||
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants';
|
||||
import { makeDataSeries } from '~/helpers/monitor_helper';
|
||||
import { graphDataValidatorForValues } from '../../utils';
|
||||
|
||||
let debouncedResize;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAreaChart,
|
||||
GlLineChart,
|
||||
GlButton,
|
||||
GlChartSeriesLabel,
|
||||
GlLink,
|
||||
Icon,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
graphData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: graphDataValidatorForValues.bind(null, false),
|
||||
},
|
||||
containerWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
deploymentData: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
showBorder: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
thresholds: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tooltip: {
|
||||
title: '',
|
||||
content: [],
|
||||
commitUrl: '',
|
||||
isDeployment: false,
|
||||
sha: '',
|
||||
},
|
||||
width: 0,
|
||||
height: chartHeight,
|
||||
svgs: {},
|
||||
primaryColor: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']),
|
||||
chartData() {
|
||||
// Transforms & supplements query data to render appropriate labels & styles
|
||||
// Input: [{ queryAttributes1 }, { queryAttributes2 }]
|
||||
// Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
|
||||
return this.graphData.queries.reduce((acc, query) => {
|
||||
const { appearance } = query;
|
||||
const lineType =
|
||||
appearance && appearance.line && appearance.line.type
|
||||
? appearance.line.type
|
||||
: lineTypes.default;
|
||||
const lineWidth =
|
||||
appearance && appearance.line && appearance.line.width
|
||||
? appearance.line.width
|
||||
: undefined;
|
||||
const areaStyle = {
|
||||
opacity:
|
||||
appearance && appearance.area && typeof appearance.area.opacity === 'number'
|
||||
? appearance.area.opacity
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const series = makeDataSeries(query.result, {
|
||||
name: this.formatLegendLabel(query),
|
||||
lineStyle: {
|
||||
type: lineType,
|
||||
width: lineWidth,
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined,
|
||||
});
|
||||
|
||||
return acc.concat(series);
|
||||
}, []);
|
||||
},
|
||||
chartOptions() {
|
||||
return {
|
||||
xAxis: {
|
||||
name: __('Time'),
|
||||
type: 'time',
|
||||
axisLabel: {
|
||||
formatter: date => dateFormat(date, dateFormats.timeOfDay),
|
||||
},
|
||||
axisPointer: {
|
||||
snap: true,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
name: this.yAxisLabel,
|
||||
axisLabel: {
|
||||
formatter: num => roundOffFloat(num, 3).toString(),
|
||||
},
|
||||
},
|
||||
series: this.scatterSeries,
|
||||
dataZoom: this.dataZoomConfig,
|
||||
};
|
||||
},
|
||||
dataZoomConfig() {
|
||||
const handleIcon = this.svgs['scroll-handle'];
|
||||
|
||||
return handleIcon ? { handleIcon } : {};
|
||||
},
|
||||
earliestDatapoint() {
|
||||
return this.chartData.reduce((acc, series) => {
|
||||
const { data } = series;
|
||||
const { length } = data;
|
||||
if (!length) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const [first] = data[0];
|
||||
const [last] = data[length - 1];
|
||||
const seriesEarliest = first < last ? first : last;
|
||||
|
||||
return seriesEarliest < acc || acc === null ? seriesEarliest : acc;
|
||||
}, null);
|
||||
},
|
||||
glChartComponent() {
|
||||
const chartTypes = {
|
||||
'area-chart': GlAreaChart,
|
||||
'line-chart': GlLineChart,
|
||||
};
|
||||
return chartTypes[this.graphData.type] || GlAreaChart;
|
||||
},
|
||||
isMultiSeries() {
|
||||
return this.tooltip.content.length > 1;
|
||||
},
|
||||
recentDeployments() {
|
||||
return this.deploymentData.reduce((acc, deployment) => {
|
||||
if (deployment.created_at >= this.earliestDatapoint) {
|
||||
const { id, created_at, sha, ref, tag } = deployment;
|
||||
acc.push({
|
||||
id,
|
||||
createdAt: created_at,
|
||||
sha,
|
||||
commitUrl: `${this.projectPath}/commit/${sha}`,
|
||||
tag,
|
||||
tagUrl: tag ? `${this.tagsPath}/${ref.name}` : null,
|
||||
ref: ref.name,
|
||||
showDeploymentFlag: false,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
scatterSeries() {
|
||||
return {
|
||||
type: graphTypes.deploymentData,
|
||||
data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]),
|
||||
symbol: this.svgs.rocket,
|
||||
symbolSize: symbolSizes.default,
|
||||
itemStyle: {
|
||||
color: this.primaryColor,
|
||||
},
|
||||
};
|
||||
},
|
||||
yAxisLabel() {
|
||||
return `${this.graphData.y_label}`;
|
||||
},
|
||||
csvText() {
|
||||
const chartData = this.chartData[0].data;
|
||||
const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
|
||||
return chartData.reduce((csv, data) => {
|
||||
const row = data.join(',');
|
||||
return `${csv}${row}\r\n`;
|
||||
}, header);
|
||||
},
|
||||
downloadLink() {
|
||||
const data = new Blob([this.csvText], { type: 'text/plain' });
|
||||
return window.URL.createObjectURL(data);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
containerWidth: 'onResize',
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', debouncedResize);
|
||||
},
|
||||
created() {
|
||||
debouncedResize = debounceByAnimationFrame(this.onResize);
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
this.setSvg('rocket');
|
||||
this.setSvg('scroll-handle');
|
||||
},
|
||||
methods: {
|
||||
formatLegendLabel(query) {
|
||||
return `${query.label}`;
|
||||
},
|
||||
formatTooltipText(params) {
|
||||
this.tooltip.title = dateFormat(params.value, dateFormats.default);
|
||||
this.tooltip.content = [];
|
||||
params.seriesData.forEach(dataPoint => {
|
||||
const [xVal, yVal] = dataPoint.value;
|
||||
this.tooltip.isDeployment = dataPoint.componentSubType === graphTypes.deploymentData;
|
||||
if (this.tooltip.isDeployment) {
|
||||
const [deploy] = this.recentDeployments.filter(
|
||||
deployment => deployment.createdAt === xVal,
|
||||
);
|
||||
this.tooltip.sha = deploy.sha.substring(0, 8);
|
||||
this.tooltip.commitUrl = deploy.commitUrl;
|
||||
} else {
|
||||
const { seriesName, color } = dataPoint;
|
||||
const value = yVal.toFixed(3);
|
||||
this.tooltip.content.push({
|
||||
name: seriesName,
|
||||
value,
|
||||
color,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
setSvg(name) {
|
||||
getSvgIconPathContent(name)
|
||||
.then(path => {
|
||||
if (path) {
|
||||
this.$set(this.svgs, name, `path://${path}`);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
// eslint-disable-next-line no-console, @gitlab/i18n/no-non-i18n-strings
|
||||
console.error('SVG could not be rendered correctly: ', e);
|
||||
});
|
||||
},
|
||||
onChartUpdated(chart) {
|
||||
[this.primaryColor] = chart.getOption().color;
|
||||
},
|
||||
onResize() {
|
||||
if (!this.$refs.chart) return;
|
||||
const { width } = this.$refs.chart.$el.getBoundingClientRect();
|
||||
this.width = width;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
|
||||
<div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
|
||||
<div class="prometheus-graph-header">
|
||||
<h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5>
|
||||
<gl-button
|
||||
v-if="exportMetricsToCsvEnabled"
|
||||
:href="downloadLink"
|
||||
:title="__('Download CSV')"
|
||||
:aria-label="__('Download CSV')"
|
||||
style="margin-left: 200px;"
|
||||
download="chart_metrics.csv"
|
||||
>
|
||||
{{ __('Download CSV') }}
|
||||
</gl-button>
|
||||
<div class="prometheus-graph-widgets js-graph-widgets">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<component
|
||||
:is="glChartComponent"
|
||||
ref="chart"
|
||||
v-bind="$attrs"
|
||||
:data="chartData"
|
||||
:option="chartOptions"
|
||||
:format-tooltip-text="formatTooltipText"
|
||||
:thresholds="thresholds"
|
||||
:width="width"
|
||||
:height="height"
|
||||
@updated="onChartUpdated"
|
||||
>
|
||||
<template v-if="tooltip.isDeployment">
|
||||
<template slot="tooltipTitle">
|
||||
{{ __('Deployed') }}
|
||||
</template>
|
||||
<div slot="tooltipContent" class="d-flex align-items-center">
|
||||
<icon name="commit" class="mr-2" />
|
||||
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template slot="tooltipTitle">
|
||||
<div class="text-nowrap">
|
||||
{{ tooltip.title }}
|
||||
</div>
|
||||
</template>
|
||||
<template slot="tooltipContent">
|
||||
<div
|
||||
v-for="(content, key) in tooltip.content"
|
||||
:key="key"
|
||||
class="d-flex justify-content-between"
|
||||
>
|
||||
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
|
||||
{{ content.name }}
|
||||
</gl-chart-series-label>
|
||||
<div class="prepend-left-32">
|
||||
{{ content.value }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -15,7 +15,7 @@ import Icon from '~/vue_shared/components/icon.vue';
|
|||
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import invalidUrl from '~/lib/utils/invalid_url';
|
||||
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
|
||||
import MonitorAreaChart from './charts/area.vue';
|
||||
import MonitorTimeSeriesChart from './charts/time_series.vue';
|
||||
import MonitorSingleStatChart from './charts/single_stat.vue';
|
||||
import GraphGroup from './graph_group.vue';
|
||||
import EmptyState from './empty_state.vue';
|
||||
|
@ -26,7 +26,7 @@ let sidebarMutationObserver;
|
|||
|
||||
export default {
|
||||
components: {
|
||||
MonitorAreaChart,
|
||||
MonitorTimeSeriesChart,
|
||||
MonitorSingleStatChart,
|
||||
PanelType,
|
||||
GraphGroup,
|
||||
|
@ -465,7 +465,7 @@ export default {
|
|||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<monitor-area-chart
|
||||
<monitor-time-series-chart
|
||||
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
|
||||
:key="graphIndex"
|
||||
:graph-data="graphData"
|
||||
|
@ -473,7 +473,7 @@ export default {
|
|||
:thresholds="getGraphAlertValues(graphData.queries)"
|
||||
:container-width="elWidth"
|
||||
:project-path="projectPath"
|
||||
group-id="monitor-area-chart"
|
||||
group-id="monitor-time-series-chart"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<alert-widget
|
||||
|
@ -515,7 +515,7 @@ export default {
|
|||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
</monitor-area-chart>
|
||||
</monitor-time-series-chart>
|
||||
</template>
|
||||
</graph-group>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,10 @@ export const graphTypes = {
|
|||
deploymentData: 'scatter',
|
||||
};
|
||||
|
||||
export const symbolSizes = {
|
||||
default: 14,
|
||||
};
|
||||
|
||||
export const lineTypes = {
|
||||
default: 'solid',
|
||||
};
|
||||
|
@ -21,6 +25,11 @@ export const timeWindows = {
|
|||
oneWeek: __('1 week'),
|
||||
};
|
||||
|
||||
export const dateFormats = {
|
||||
timeOfDay: 'h:MM TT',
|
||||
default: 'dd mmm yyyy, h:MMTT',
|
||||
};
|
||||
|
||||
export const secondsIn = {
|
||||
thirtyMinutes: 60 * 30,
|
||||
threeHours: 60 * 60 * 3,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Create component to display area and line charts in monitor dashboards
|
||||
merge_request: 31639
|
||||
author:
|
||||
type: added
|
|
@ -166,7 +166,7 @@ panel_groups:
|
|||
label: Total (cores)
|
||||
unit: "cores"
|
||||
- title: "Memory Usage (Pod average)"
|
||||
type: "area-chart"
|
||||
type: "line-chart"
|
||||
y_label: "Memory Used per Pod (MB)"
|
||||
weight: 2
|
||||
metrics:
|
||||
|
@ -175,7 +175,7 @@ panel_groups:
|
|||
label: Pod average (MB)
|
||||
unit: MB
|
||||
- title: "Canary: Memory Usage (Pod Average)"
|
||||
type: "area-chart"
|
||||
type: "line-chart"
|
||||
y_label: "Memory Used per Pod (MB)"
|
||||
weight: 2
|
||||
metrics:
|
||||
|
@ -185,7 +185,7 @@ panel_groups:
|
|||
unit: MB
|
||||
track: canary
|
||||
- title: "Core Usage (Pod Average)"
|
||||
type: "area-chart"
|
||||
type: "line-chart"
|
||||
y_label: "Cores per Pod"
|
||||
weight: 1
|
||||
metrics:
|
||||
|
@ -194,7 +194,7 @@ panel_groups:
|
|||
label: Pod average (cores)
|
||||
unit: "cores"
|
||||
- title: "Canary: Core Usage (Pod Average)"
|
||||
type: "area-chart"
|
||||
type: "line-chart"
|
||||
y_label: "Cores per Pod"
|
||||
weight: 1
|
||||
metrics:
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ImportCommonMetricsLineCharts < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
335
spec/javascripts/monitoring/charts/time_series_spec.js
Normal file
335
spec/javascripts/monitoring/charts/time_series_spec.js
Normal file
|
@ -0,0 +1,335 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from '~/monitoring/stores';
|
||||
import { GlLink } from '@gitlab/ui';
|
||||
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
|
||||
import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
|
||||
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
|
||||
import * as types from '~/monitoring/stores/mutation_types';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import MonitoringMock, { deploymentData, mockProjectPath } from '../mock_data';
|
||||
|
||||
describe('Time series component', () => {
|
||||
const mockSha = 'mockSha';
|
||||
const mockWidgets = 'mockWidgets';
|
||||
const mockSvgPathContent = 'mockSvgPathContent';
|
||||
const projectPath = `${TEST_HOST}${mockProjectPath}`;
|
||||
const commitUrl = `${projectPath}/commit/${mockSha}`;
|
||||
let mockGraphData;
|
||||
let makeTimeSeriesChart;
|
||||
let spriteSpy;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
|
||||
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
|
||||
store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true });
|
||||
[mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
|
||||
|
||||
makeTimeSeriesChart = (graphData, type) =>
|
||||
shallowMount(TimeSeries, {
|
||||
propsData: {
|
||||
graphData: { ...graphData, type },
|
||||
containerWidth: 0,
|
||||
deploymentData: store.state.monitoringDashboard.deploymentData,
|
||||
projectPath,
|
||||
},
|
||||
slots: {
|
||||
default: mockWidgets,
|
||||
},
|
||||
sync: false,
|
||||
store,
|
||||
});
|
||||
|
||||
spriteSpy = spyOnDependency(TimeSeries, 'getSvgIconPathContent').and.callFake(
|
||||
() => new Promise(resolve => resolve(mockSvgPathContent)),
|
||||
);
|
||||
});
|
||||
|
||||
describe('general functions', () => {
|
||||
let timeSeriesChart;
|
||||
|
||||
beforeEach(() => {
|
||||
timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
|
||||
});
|
||||
|
||||
it('renders chart title', () => {
|
||||
expect(timeSeriesChart.find('.js-graph-title').text()).toBe(mockGraphData.title);
|
||||
});
|
||||
|
||||
it('contains graph widgets from slot', () => {
|
||||
expect(timeSeriesChart.find('.js-graph-widgets').text()).toBe(mockWidgets);
|
||||
});
|
||||
|
||||
describe('when exportMetricsToCsvEnabled is disabled', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false });
|
||||
});
|
||||
|
||||
it('does not render the Download CSV button', done => {
|
||||
timeSeriesChart.vm.$nextTick(() => {
|
||||
expect(timeSeriesChart.contains('glbutton-stub')).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('formatTooltipText', () => {
|
||||
const mockDate = deploymentData[0].created_at;
|
||||
const mockCommitUrl = deploymentData[0].commitUrl;
|
||||
const generateSeriesData = type => ({
|
||||
seriesData: [
|
||||
{
|
||||
seriesName: timeSeriesChart.vm.chartData[0].name,
|
||||
componentSubType: type,
|
||||
value: [mockDate, 5.55555],
|
||||
seriesIndex: 0,
|
||||
},
|
||||
],
|
||||
value: mockDate,
|
||||
});
|
||||
|
||||
describe('when series is of line type', () => {
|
||||
beforeEach(done => {
|
||||
timeSeriesChart.vm.formatTooltipText(generateSeriesData('line'));
|
||||
timeSeriesChart.vm.$nextTick(done);
|
||||
});
|
||||
|
||||
it('formats tooltip title', () => {
|
||||
expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
|
||||
});
|
||||
|
||||
it('formats tooltip content', () => {
|
||||
const name = 'Core Usage';
|
||||
const value = '5.556';
|
||||
const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
|
||||
|
||||
expect(seriesLabel.vm.color).toBe('');
|
||||
expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
|
||||
expect(timeSeriesChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]);
|
||||
expect(
|
||||
shallowWrapperContainsSlotText(
|
||||
timeSeriesChart.find(GlAreaChart),
|
||||
'tooltipContent',
|
||||
value,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when series is of scatter type', () => {
|
||||
beforeEach(() => {
|
||||
timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
|
||||
});
|
||||
|
||||
it('formats tooltip title', () => {
|
||||
expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
|
||||
});
|
||||
|
||||
it('formats tooltip sha', () => {
|
||||
expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9');
|
||||
});
|
||||
|
||||
it('formats tooltip commit url', () => {
|
||||
expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSvg', () => {
|
||||
const mockSvgName = 'mockSvgName';
|
||||
|
||||
beforeEach(done => {
|
||||
timeSeriesChart.vm.setSvg(mockSvgName);
|
||||
timeSeriesChart.vm.$nextTick(done);
|
||||
});
|
||||
|
||||
it('gets svg path content', () => {
|
||||
expect(spriteSpy).toHaveBeenCalledWith(mockSvgName);
|
||||
});
|
||||
|
||||
it('sets svg path content', () => {
|
||||
timeSeriesChart.vm.$nextTick(() => {
|
||||
expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onResize', () => {
|
||||
const mockWidth = 233;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({
|
||||
width: mockWidth,
|
||||
}));
|
||||
timeSeriesChart.vm.onResize();
|
||||
});
|
||||
|
||||
it('sets area chart width', () => {
|
||||
expect(timeSeriesChart.vm.width).toBe(mockWidth);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('chartData', () => {
|
||||
let chartData;
|
||||
const seriesData = () => chartData[0];
|
||||
|
||||
beforeEach(() => {
|
||||
({ chartData } = timeSeriesChart.vm);
|
||||
});
|
||||
|
||||
it('utilizes all data points', () => {
|
||||
const { values } = mockGraphData.queries[0].result[0];
|
||||
|
||||
expect(chartData.length).toBe(1);
|
||||
expect(seriesData().data.length).toBe(values.length);
|
||||
});
|
||||
|
||||
it('creates valid data', () => {
|
||||
const { data } = seriesData();
|
||||
|
||||
expect(
|
||||
data.filter(
|
||||
([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
|
||||
).length,
|
||||
).toBe(data.length);
|
||||
});
|
||||
|
||||
it('formats line width correctly', () => {
|
||||
expect(chartData[0].lineStyle.width).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chartOptions', () => {
|
||||
describe('yAxis formatter', () => {
|
||||
let format;
|
||||
|
||||
beforeEach(() => {
|
||||
format = timeSeriesChart.vm.chartOptions.yAxis.axisLabel.formatter;
|
||||
});
|
||||
|
||||
it('rounds to 3 decimal places', () => {
|
||||
expect(format(0.88888)).toBe('0.889');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('scatterSeries', () => {
|
||||
it('utilizes deployment data', () => {
|
||||
expect(timeSeriesChart.vm.scatterSeries.data).toEqual([
|
||||
['2017-05-31T21:23:37.881Z', 0],
|
||||
['2017-05-30T20:08:04.629Z', 0],
|
||||
['2017-05-30T17:42:38.409Z', 0],
|
||||
]);
|
||||
|
||||
expect(timeSeriesChart.vm.scatterSeries.symbolSize).toBe(14);
|
||||
});
|
||||
});
|
||||
|
||||
describe('yAxisLabel', () => {
|
||||
it('constructs a label for the chart y-axis', () => {
|
||||
expect(timeSeriesChart.vm.yAxisLabel).toBe('CPU');
|
||||
});
|
||||
});
|
||||
|
||||
describe('csvText', () => {
|
||||
it('converts data from json to csv', () => {
|
||||
const header = `timestamp,${mockGraphData.y_label}`;
|
||||
const data = mockGraphData.queries[0].result[0].values;
|
||||
const firstRow = `${data[0][0]},${data[0][1]}`;
|
||||
|
||||
expect(timeSeriesChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadLink', () => {
|
||||
it('produces a link to download metrics as csv', () => {
|
||||
const link = timeSeriesChart.vm.downloadLink;
|
||||
|
||||
expect(link).toContain('blob:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
timeSeriesChart.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrapped components', () => {
|
||||
const glChartComponents = [
|
||||
{
|
||||
chartType: 'area-chart',
|
||||
component: GlAreaChart,
|
||||
},
|
||||
{
|
||||
chartType: 'line-chart',
|
||||
component: GlLineChart,
|
||||
},
|
||||
];
|
||||
|
||||
glChartComponents.forEach(dynamicComponent => {
|
||||
describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
|
||||
let timeSeriesAreaChart;
|
||||
let glChart;
|
||||
|
||||
beforeEach(done => {
|
||||
timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
|
||||
glChart = timeSeriesAreaChart.find(dynamicComponent.component);
|
||||
timeSeriesAreaChart.vm.$nextTick(done);
|
||||
});
|
||||
|
||||
it('is a Vue instance', () => {
|
||||
expect(glChart.exists()).toBe(true);
|
||||
expect(glChart.isVueInstance()).toBe(true);
|
||||
});
|
||||
|
||||
it('receives data properties needed for proper chart render', () => {
|
||||
const props = glChart.props();
|
||||
|
||||
expect(props.data).toBe(timeSeriesAreaChart.vm.chartData);
|
||||
expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions);
|
||||
expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText);
|
||||
expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds);
|
||||
});
|
||||
|
||||
it('recieves a tooltip title', done => {
|
||||
const mockTitle = 'mockTitle';
|
||||
timeSeriesAreaChart.vm.tooltip.title = mockTitle;
|
||||
|
||||
timeSeriesAreaChart.vm.$nextTick(() => {
|
||||
expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', mockTitle)).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when tooltip is showing deployment data', () => {
|
||||
beforeEach(done => {
|
||||
timeSeriesAreaChart.vm.tooltip.isDeployment = true;
|
||||
timeSeriesAreaChart.vm.$nextTick(done);
|
||||
});
|
||||
|
||||
it('uses deployment title', () => {
|
||||
expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', 'Deployed')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders clickable commit sha in tooltip content', done => {
|
||||
timeSeriesAreaChart.vm.tooltip.sha = mockSha;
|
||||
timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl;
|
||||
|
||||
timeSeriesAreaChart.vm.$nextTick(() => {
|
||||
const commitLink = timeSeriesAreaChart.find(GlLink);
|
||||
|
||||
expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
|
||||
expect(commitLink.attributes('href')).toEqual(commitUrl);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
|
||||
|
||||
export const mockProjectPath = '/frontend-fixtures/environments-project';
|
||||
|
||||
export const metricsGroupsAPIResponse = {
|
||||
success: true,
|
||||
data: [
|
||||
|
@ -902,7 +904,7 @@ export const metricsDashboardResponse = {
|
|||
},
|
||||
{
|
||||
title: 'Memory Usage (Pod average)',
|
||||
type: 'area-chart',
|
||||
type: 'line-chart',
|
||||
y_label: 'Memory Used per Pod',
|
||||
weight: 2,
|
||||
metrics: [
|
||||
|
|
Loading…
Reference in a new issue