Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/monitoring')
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js4
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap65
-rw-r--r--spec/frontend/monitoring/components/alert_widget_form_spec.js112
-rw-r--r--spec/frontend/monitoring/components/charts/gauge_spec.js215
-rw-r--r--spec/frontend/monitoring/components/charts/heatmap_spec.js18
-rw-r--r--spec/frontend/monitoring/components/charts/options_spec.js244
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js60
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js27
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js440
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js372
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js234
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js111
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js522
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js120
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js4
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js2
-rw-r--r--spec/frontend/monitoring/components/group_empty_state_spec.js2
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js30
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js6
-rw-r--r--spec/frontend/monitoring/csv_export_spec.js126
-rw-r--r--spec/frontend/monitoring/fixture_data.js24
-rw-r--r--spec/frontend/monitoring/graph_data.js92
-rw-r--r--spec/frontend/monitoring/mock_data.js107
-rw-r--r--spec/frontend/monitoring/pages/panel_new_page_spec.js98
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js149
-rw-r--r--spec/frontend/monitoring/router_spec.js66
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js125
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js119
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js149
-rw-r--r--spec/frontend/monitoring/utils_spec.js2
31 files changed, 2539 insertions, 1108 deletions
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
index f0355dfa01b..193dbb3e63f 100644
--- a/spec/frontend/monitoring/alert_widget_spec.js
+++ b/spec/frontend/monitoring/alert_widget_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
-import AlertWidget from '~/monitoring/components/alert_widget.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import AlertWidget from '~/monitoring/components/alert_widget.vue';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
const mockReadAlert = jest.fn();
const mockCreateAlert = jest.fn();
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index e7c51d82cd2..7ef956f8e05 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -20,7 +20,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master"
id="monitor-dashboards-dropdown"
- modalid="duplicateDashboard"
toggle-class="dropdown-menu-toggle"
/>
</div>
@@ -33,26 +32,24 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="mb-2 pr-2 d-flex d-sm-block"
>
- <gl-dropdown-stub
+ <gl-new-dropdown-stub
+ category="tertiary"
class="flex-grow-1"
data-qa-selector="environments_dropdown"
+ headertext=""
id="monitor-environments-dropdown"
menu-class="monitor-environment-dropdown-menu"
+ size="medium"
text="production"
- toggle-class="dropdown-menu-toggle"
+ toggleclass="dropdown-menu-toggle"
+ variant="default"
>
<div
class="d-flex flex-column overflow-hidden"
>
- <gl-dropdown-header-stub
- class="monitor-environment-dropdown-header text-center"
- >
-
- Environment
-
- </gl-dropdown-header-stub>
-
- <gl-dropdown-divider-stub />
+ <gl-new-dropdown-header-stub>
+ Environment
+ </gl-new-dropdown-header-stub>
<gl-search-box-by-type-stub
class="m-2"
@@ -72,7 +69,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
</div>
</div>
- </gl-dropdown-stub>
+ </gl-new-dropdown-stub>
</div>
<div
@@ -100,45 +97,23 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="d-sm-flex"
>
- <div
- class="mb-2 mr-2 d-flex"
- >
- <div
- class="flex-grow-1"
- title="Star dashboard"
- >
- <gl-deprecated-button-stub
- class="w-100"
- size="md"
- variant="default"
- >
- <gl-icon-stub
- name="star-o"
- size="16"
- />
- </gl-deprecated-button-stub>
- </div>
- </div>
-
<!---->
<!---->
- <!---->
-
- <!---->
-
- <!---->
-
- <!---->
+ <div
+ class="gl-mb-3 gl-mr-3 d-flex d-sm-block"
+ >
+ <actions-menu-stub
+ custommetricspath="/monitoring/monitor-project/prometheus/metrics"
+ defaultbranch="master"
+ isootbdashboard="true"
+ validatequerypath="/monitoring/monitor-project/prometheus/metrics/validate_query"
+ />
+ </div>
<!---->
</div>
-
- <duplicate-dashboard-modal-stub
- defaultbranch="master"
- modalid="duplicateDashboard"
- />
</div>
<empty-state-stub
diff --git a/spec/frontend/monitoring/components/alert_widget_form_spec.js b/spec/frontend/monitoring/components/alert_widget_form_spec.js
index a8416216a94..6d71a9b09e5 100644
--- a/spec/frontend/monitoring/components/alert_widget_form_spec.js
+++ b/spec/frontend/monitoring/components/alert_widget_form_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import INVALID_URL from '~/lib/utils/invalid_url';
import AlertWidgetForm from '~/monitoring/components/alert_widget_form.vue';
import ModalStub from '../stubs/modal_stub';
@@ -24,7 +25,13 @@ describe('AlertWidgetForm', () => {
const propsWithAlertData = {
...defaultProps,
alertsToManage: {
- alert: { alert_path: alertPath, operator: '<', threshold: 5, metricId },
+ alert: {
+ alert_path: alertPath,
+ operator: '<',
+ threshold: 5,
+ metricId,
+ runbookUrl: INVALID_URL,
+ },
},
configuredAlert: metricId,
};
@@ -46,15 +53,11 @@ describe('AlertWidgetForm', () => {
const modal = () => wrapper.find(ModalStub);
const modalTitle = () => modal().attributes('title');
const submitButton = () => modal().find(GlLink);
+ const findRunbookField = () => modal().find('[data-testid="alertRunbookField"]');
+ const findThresholdField = () => modal().find('[data-qa-selector="alert_threshold_field"]');
const submitButtonTrackingOpts = () =>
JSON.parse(submitButton().attributes('data-tracking-options'));
- const e = {
- preventDefault: jest.fn(),
- };
-
- beforeEach(() => {
- e.preventDefault.mockReset();
- });
+ const stubEvent = { preventDefault: jest.fn() };
afterEach(() => {
if (wrapper) wrapper.destroy();
@@ -81,35 +84,34 @@ describe('AlertWidgetForm', () => {
expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.create);
});
- it('emits a "create" event when form submitted without existing alert', () => {
- createComponent();
+ it('emits a "create" event when form submitted without existing alert', async () => {
+ createComponent(defaultProps);
- wrapper.vm.selectQuery('9');
- wrapper.setData({
- threshold: 900,
- });
+ modal().vm.$emit('shown');
+
+ findThresholdField().vm.$emit('input', 900);
+ findRunbookField().vm.$emit('input', INVALID_URL);
- wrapper.vm.handleSubmit(e);
+ modal().vm.$emit('ok', stubEvent);
expect(wrapper.emitted().create[0]).toEqual([
{
alert: undefined,
operator: '>',
threshold: 900,
- prometheus_metric_id: '9',
+ prometheus_metric_id: '8',
+ runbookUrl: INVALID_URL,
},
]);
- expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
it('resets form when modal is dismissed (hidden)', () => {
- createComponent();
+ createComponent(defaultProps);
- wrapper.vm.selectQuery('9');
- wrapper.vm.selectQuery('>');
- wrapper.setData({
- threshold: 800,
- });
+ modal().vm.$emit('shown');
+
+ findThresholdField().vm.$emit('input', 800);
+ findRunbookField().vm.$emit('input', INVALID_URL);
modal().vm.$emit('hidden');
@@ -117,6 +119,7 @@ describe('AlertWidgetForm', () => {
expect(wrapper.vm.operator).toBe(null);
expect(wrapper.vm.threshold).toBe(null);
expect(wrapper.vm.prometheusMetricId).toBe(null);
+ expect(wrapper.vm.runbookUrl).toBe(null);
});
it('sets selectedAlert to the provided configuredAlert on modal show', () => {
@@ -163,7 +166,7 @@ describe('AlertWidgetForm', () => {
beforeEach(() => {
createComponent(propsWithAlertData);
- wrapper.vm.selectQuery(metricId);
+ modal().vm.$emit('shown');
});
it('sets tracking options for delete alert', () => {
@@ -176,7 +179,7 @@ describe('AlertWidgetForm', () => {
});
it('emits "delete" event when form values unchanged', () => {
- wrapper.vm.handleSubmit(e);
+ modal().vm.$emit('ok', stubEvent);
expect(wrapper.emitted().delete[0]).toEqual([
{
@@ -184,37 +187,52 @@ describe('AlertWidgetForm', () => {
operator: '<',
threshold: 5,
prometheus_metric_id: '8',
+ runbookUrl: INVALID_URL,
},
]);
- expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
+ });
- it('emits "update" event when form changed', () => {
- wrapper.setData({
- threshold: 11,
- });
+ it('emits "update" event when form changed', () => {
+ const updatedRunbookUrl = `${INVALID_URL}/test`;
- wrapper.vm.handleSubmit(e);
+ createComponent(propsWithAlertData);
- expect(wrapper.emitted().update[0]).toEqual([
- {
- alert: 'alert',
- operator: '<',
- threshold: 11,
- prometheus_metric_id: '8',
- },
- ]);
- expect(e.preventDefault).toHaveBeenCalledTimes(1);
- });
+ modal().vm.$emit('shown');
+
+ findRunbookField().vm.$emit('input', updatedRunbookUrl);
+ findThresholdField().vm.$emit('input', 11);
- it('sets tracking options for update alert', () => {
- wrapper.setData({
+ modal().vm.$emit('ok', stubEvent);
+
+ expect(wrapper.emitted().update[0]).toEqual([
+ {
+ alert: 'alert',
+ operator: '<',
threshold: 11,
- });
+ prometheus_metric_id: '8',
+ runbookUrl: updatedRunbookUrl,
+ },
+ ]);
+ });
+
+ it('sets tracking options for update alert', async () => {
+ createComponent(propsWithAlertData);
+
+ modal().vm.$emit('shown');
+
+ findThresholdField().vm.$emit('input', 11);
+
+ await wrapper.vm.$nextTick();
+
+ expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
+ });
+
+ describe('alert runbooks', () => {
+ it('shows the runbook field', () => {
+ createComponent();
- return wrapper.vm.$nextTick(() => {
- expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
- });
+ expect(findRunbookField().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js
new file mode 100644
index 00000000000..850e2ca87db
--- /dev/null
+++ b/spec/frontend/monitoring/components/charts/gauge_spec.js
@@ -0,0 +1,215 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlGaugeChart } from '@gitlab/ui/dist/charts';
+import GaugeChart from '~/monitoring/components/charts/gauge.vue';
+import { gaugeChartGraphData } from '../../graph_data';
+
+describe('Gauge Chart component', () => {
+ const defaultGraphData = gaugeChartGraphData();
+
+ let wrapper;
+
+ const findGaugeChart = () => wrapper.find(GlGaugeChart);
+
+ const createWrapper = ({ ...graphProps } = {}) => {
+ wrapper = shallowMount(GaugeChart, {
+ propsData: {
+ graphData: {
+ ...defaultGraphData,
+ ...graphProps,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('chart component', () => {
+ it('is rendered when props are passed', () => {
+ createWrapper();
+
+ expect(findGaugeChart().exists()).toBe(true);
+ });
+ });
+
+ describe('min and max', () => {
+ const MIN_DEFAULT = 0;
+ const MAX_DEFAULT = 100;
+
+ it('are passed to chart component', () => {
+ createWrapper();
+
+ expect(findGaugeChart().props('min')).toBe(100);
+ expect(findGaugeChart().props('max')).toBe(1000);
+ });
+
+ const invalidCases = [undefined, NaN, 'a string'];
+
+ it.each(invalidCases)(
+ 'if min has invalid value, defaults are used for both min and max',
+ invalidValue => {
+ createWrapper({ minValue: invalidValue });
+
+ expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT);
+ expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT);
+ },
+ );
+
+ it.each(invalidCases)(
+ 'if max has invalid value, defaults are used for both min and max',
+ invalidValue => {
+ createWrapper({ minValue: invalidValue });
+
+ expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT);
+ expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT);
+ },
+ );
+
+ it('if min is bigger than max, defaults are used for both min and max', () => {
+ createWrapper({ minValue: 100, maxValue: 0 });
+
+ expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT);
+ expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT);
+ });
+ });
+
+ describe('thresholds', () => {
+ it('thresholds are set on chart', () => {
+ createWrapper();
+
+ expect(findGaugeChart().props('thresholds')).toEqual([500, 800]);
+ });
+
+ it('when no thresholds are defined, a default threshold is defined at 95% of max_value', () => {
+ createWrapper({
+ minValue: 0,
+ maxValue: 100,
+ thresholds: {},
+ });
+
+ expect(findGaugeChart().props('thresholds')).toEqual([95]);
+ });
+
+ it('when out of min-max bounds thresholds are defined, a default threshold is defined at 95% of the range between min_value and max_value', () => {
+ createWrapper({
+ thresholds: {
+ values: [-10, 1500],
+ },
+ });
+
+ expect(findGaugeChart().props('thresholds')).toEqual([855]);
+ });
+
+ describe('when mode is absolute', () => {
+ it('only valid threshold values are used', () => {
+ createWrapper({
+ thresholds: {
+ mode: 'absolute',
+ values: [undefined, 10, 110, NaN, 'a string', 400],
+ },
+ });
+
+ expect(findGaugeChart().props('thresholds')).toEqual([110, 400]);
+ });
+
+ it('if all threshold values are invalid, a default threshold is defined at 95% of the range between min_value and max_value', () => {
+ createWrapper({
+ thresholds: {
+ mode: 'absolute',
+ values: [NaN, undefined, 'a string', 1500],
+ },
+ });
+
+ expect(findGaugeChart().props('thresholds')).toEqual([855]);
+ });
+ });
+
+ describe('when mode is percentage', () => {
+ it('when values outside of 0-100 bounds are used, a default threshold is defined at 95% of max_value', () => {
+ createWrapper({
+ thresholds: {
+ mode: 'percentage',
+ values: [110],
+ },
+ });
+
+ expect(findGaugeChart().props('thresholds')).toEqual([855]);
+ });
+
+ it('if all threshold values are invalid, a default threshold is defined at 95% of max_value', () => {
+ createWrapper({
+ thresholds: {
+ mode: 'percentage',
+ values: [NaN, undefined, 'a string', 1500],
+ },
+ });
+
+ expect(findGaugeChart().props('thresholds')).toEqual([855]);
+ });
+ });
+ });
+
+ describe('split (the number of ticks on the chart arc)', () => {
+ const SPLIT_DEFAULT = 10;
+
+ it('is passed to chart as prop', () => {
+ createWrapper();
+
+ expect(findGaugeChart().props('splitNumber')).toBe(20);
+ });
+
+ it('if not explicitly set, passes a default value to chart', () => {
+ createWrapper({ split: '' });
+
+ expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT);
+ });
+
+ it('if set as a number that is not an integer, passes the default value to chart', () => {
+ createWrapper({ split: 10.5 });
+
+ expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT);
+ });
+
+ it('if set as a negative number, passes the default value to chart', () => {
+ createWrapper({ split: -10 });
+
+ expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT);
+ });
+ });
+
+ describe('text (the text displayed on the gauge for the current value)', () => {
+ it('displays the query result value when format is not set', () => {
+ createWrapper({ format: '' });
+
+ expect(findGaugeChart().props('text')).toBe('3');
+ });
+
+ it('displays the query result value when format is set to invalid value', () => {
+ createWrapper({ format: 'invalid' });
+
+ expect(findGaugeChart().props('text')).toBe('3');
+ });
+
+ it('displays a formatted query result value when format is set', () => {
+ createWrapper();
+
+ expect(findGaugeChart().props('text')).toBe('3kB');
+ });
+
+ it('displays a placeholder value when metric is empty', () => {
+ createWrapper({ metrics: [] });
+
+ expect(findGaugeChart().props('text')).toBe('--');
+ });
+ });
+
+ describe('value', () => {
+ it('correct value is passed', () => {
+ createWrapper();
+
+ expect(findGaugeChart().props('value')).toBe(3);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js
index 2a1c78025ae..27a2021e9be 100644
--- a/spec/frontend/monitoring/components/charts/heatmap_spec.js
+++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlHeatmap } from '@gitlab/ui/dist/charts';
import timezoneMock from 'timezone-mock';
import Heatmap from '~/monitoring/components/charts/heatmap.vue';
-import { graphDataPrometheusQueryRangeMultiTrack } from '../../mock_data';
+import { heatmapGraphData } from '../../graph_data';
describe('Heatmap component', () => {
let wrapper;
@@ -10,10 +10,12 @@ describe('Heatmap component', () => {
const findChart = () => wrapper.find(GlHeatmap);
+ const graphData = heatmapGraphData();
+
const createWrapper = (props = {}) => {
wrapper = shallowMount(Heatmap, {
propsData: {
- graphData: graphDataPrometheusQueryRangeMultiTrack,
+ graphData: heatmapGraphData(),
containerWidth: 100,
...props,
},
@@ -38,11 +40,11 @@ describe('Heatmap component', () => {
});
it('should display a label on the x axis', () => {
- expect(wrapper.vm.xAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.x_label);
+ expect(wrapper.vm.xAxisName).toBe(graphData.xLabel);
});
it('should display a label on the y axis', () => {
- expect(wrapper.vm.yAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.y_label);
+ expect(wrapper.vm.yAxisName).toBe(graphData.y_label);
});
// According to the echarts docs https://echarts.apache.org/en/option.html#series-heatmap.data
@@ -54,24 +56,24 @@ describe('Heatmap component', () => {
const row = wrapper.vm.chartData[0];
expect(row.length).toBe(3);
- expect(wrapper.vm.chartData.length).toBe(30);
+ expect(wrapper.vm.chartData.length).toBe(6);
});
it('returns a series of labels for the x axis', () => {
const { xAxisLabels } = wrapper.vm;
- expect(xAxisLabels.length).toBe(5);
+ expect(xAxisLabels.length).toBe(2);
});
describe('y axis labels', () => {
- const gmtLabels = ['3:00 PM', '4:00 PM', '5:00 PM', '6:00 PM', '7:00 PM', '8:00 PM'];
+ const gmtLabels = ['8:10 PM', '8:12 PM', '8:14 PM'];
it('y-axis labels are formatted in AM/PM format', () => {
expect(findChart().props('yAxisLabels')).toEqual(gmtLabels);
});
describe('when in PT timezone', () => {
- const ptLabels = ['8:00 AM', '9:00 AM', '10:00 AM', '11:00 AM', '12:00 PM', '1:00 PM'];
+ const ptLabels = ['1:10 PM', '1:12 PM', '1:14 PM'];
const utcLabels = gmtLabels; // Identical in this case
beforeAll(() => {
diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js
index 1c8fdc01e3e..3372d27e4f9 100644
--- a/spec/frontend/monitoring/components/charts/options_spec.js
+++ b/spec/frontend/monitoring/components/charts/options_spec.js
@@ -1,5 +1,9 @@
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
-import { getYAxisOptions, getTooltipFormatter } from '~/monitoring/components/charts/options';
+import {
+ getYAxisOptions,
+ getTooltipFormatter,
+ getValidThresholds,
+} from '~/monitoring/components/charts/options';
describe('options spec', () => {
describe('getYAxisOptions', () => {
@@ -82,4 +86,242 @@ describe('options spec', () => {
expect(formatter(1)).toBe('1.000B');
});
});
+
+ describe('getValidThresholds', () => {
+ const invalidCases = [null, undefined, NaN, 'a string', true, false];
+
+ let thresholds;
+
+ afterEach(() => {
+ thresholds = null;
+ });
+
+ it('returns same thresholds when passed values within range', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ values: [10, 50],
+ });
+
+ expect(thresholds).toEqual([10, 50]);
+ });
+
+ it('filters out thresholds that are out of range', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ values: [-5, 10, 110],
+ });
+
+ expect(thresholds).toEqual([10]);
+ });
+ it('filters out duplicate thresholds', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ values: [5, 5, 10, 10],
+ });
+
+ expect(thresholds).toEqual([5, 10]);
+ });
+
+ it('sorts passed thresholds and applies only the first two in ascending order', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ values: [10, 1, 35, 20, 5],
+ });
+
+ expect(thresholds).toEqual([1, 5]);
+ });
+
+ it('thresholds equal to min or max are filtered out', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ values: [0, 100],
+ });
+
+ expect(thresholds).toEqual([]);
+ });
+
+ it.each(invalidCases)('invalid values for thresholds are filtered out', invalidValue => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ values: [10, invalidValue],
+ });
+
+ expect(thresholds).toEqual([10]);
+ });
+
+ describe('range', () => {
+ it('when range is not defined, empty result is returned', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ values: [10, 20],
+ });
+
+ expect(thresholds).toEqual([]);
+ });
+
+ it('when min is not defined, empty result is returned', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { max: 100 },
+ values: [10, 20],
+ });
+
+ expect(thresholds).toEqual([]);
+ });
+
+ it('when max is not defined, empty result is returned', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0 },
+ values: [10, 20],
+ });
+
+ expect(thresholds).toEqual([]);
+ });
+
+ it('when min is larger than max, empty result is returned', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 100, max: 0 },
+ values: [10, 20],
+ });
+
+ expect(thresholds).toEqual([]);
+ });
+
+ it.each(invalidCases)(
+ 'when min has invalid value, empty result is returned',
+ invalidValue => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: invalidValue, max: 100 },
+ values: [10, 20],
+ });
+
+ expect(thresholds).toEqual([]);
+ },
+ );
+
+ it.each(invalidCases)(
+ 'when max has invalid value, empty result is returned',
+ invalidValue => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: invalidValue },
+ values: [10, 20],
+ });
+
+ expect(thresholds).toEqual([]);
+ },
+ );
+ });
+
+ describe('values', () => {
+ it('if values parameter is omitted, empty result is returned', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ });
+
+ expect(thresholds).toEqual([]);
+ });
+
+ it('if there are no values passed, empty result is returned', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ values: [],
+ });
+
+ expect(thresholds).toEqual([]);
+ });
+
+ it.each(invalidCases)(
+ 'if invalid values are passed, empty result is returned',
+ invalidValue => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ values: [invalidValue],
+ });
+
+ expect(thresholds).toEqual([]);
+ },
+ );
+ });
+
+ describe('mode', () => {
+ it.each(invalidCases)(
+ 'if invalid values are passed, empty result is returned',
+ invalidValue => {
+ thresholds = getValidThresholds({
+ mode: invalidValue,
+ range: { min: 0, max: 100 },
+ values: [10, 50],
+ });
+
+ expect(thresholds).toEqual([]);
+ },
+ );
+
+ it('if mode is not passed, empty result is returned', () => {
+ thresholds = getValidThresholds({
+ range: { min: 0, max: 100 },
+ values: [10, 50],
+ });
+
+ expect(thresholds).toEqual([]);
+ });
+
+ describe('absolute mode', () => {
+ it('absolute mode behaves correctly', () => {
+ thresholds = getValidThresholds({
+ mode: 'absolute',
+ range: { min: 0, max: 100 },
+ values: [10, 50],
+ });
+
+ expect(thresholds).toEqual([10, 50]);
+ });
+ });
+
+ describe('percentage mode', () => {
+ it('percentage mode behaves correctly', () => {
+ thresholds = getValidThresholds({
+ mode: 'percentage',
+ range: { min: 0, max: 1000 },
+ values: [10, 50],
+ });
+
+ expect(thresholds).toEqual([100, 500]);
+ });
+
+ const outOfPercentBoundsValues = [-1, 0, 100, 101];
+ it.each(outOfPercentBoundsValues)(
+ 'when values out of 0-100 range are passed, empty result is returned',
+ invalidValue => {
+ thresholds = getValidThresholds({
+ mode: 'percentage',
+ range: { min: 0, max: 1000 },
+ values: [invalidValue],
+ });
+
+ expect(thresholds).toEqual([]);
+ },
+ );
+ });
+ });
+
+ it('calling without passing object parameter returns empty array', () => {
+ thresholds = getValidThresholds();
+
+ expect(thresholds).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
index 3783b1eebd2..37712eb3012 100644
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js
@@ -1,71 +1,91 @@
import { shallowMount } from '@vue/test-utils';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
import { singleStatGraphData } from '../../graph_data';
describe('Single Stat Chart component', () => {
- let singleStatChart;
+ let wrapper;
- beforeEach(() => {
- singleStatChart = shallowMount(SingleStatChart, {
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(SingleStatChart, {
propsData: {
graphData: singleStatGraphData({}, { unit: 'MB' }),
+ ...props,
},
});
+ };
+
+ const findChart = () => wrapper.find(GlSingleStat);
+
+ beforeEach(() => {
+ createComponent();
});
afterEach(() => {
- singleStatChart.destroy();
+ wrapper.destroy();
});
describe('computed', () => {
describe('statValue', () => {
it('should interpolate the value and unit props', () => {
- expect(singleStatChart.vm.statValue).toBe('1.00MB');
+ expect(findChart().props('value')).toBe('1.00MB');
});
it('should change the value representation to a percentile one', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ max_value: 120 }, { value: 91 }),
});
- expect(singleStatChart.vm.statValue).toContain('75.83%');
+ expect(findChart().props('value')).toContain('75.83%');
});
it('should display NaN for non numeric maxValue values', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ max_value: 'not a number' }),
});
- expect(singleStatChart.vm.statValue).toContain('NaN');
+ expect(findChart().props('value')).toContain('NaN');
});
it('should display NaN for missing query values', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ max_value: 120 }, { value: 'NaN' }),
});
- expect(singleStatChart.vm.statValue).toContain('NaN');
+ expect(findChart().props('value')).toContain('NaN');
+ });
+
+ it('should not display `unit` when `unit` is undefined', () => {
+ createComponent({
+ graphData: singleStatGraphData({}, { unit: undefined }),
+ });
+
+ expect(findChart().props('value')).not.toContain('undefined');
});
- describe('field attribute', () => {
+ it('should not display `unit` when `unit` is null', () => {
+ createComponent({
+ graphData: singleStatGraphData({}, { unit: null }),
+ });
+
+ expect(findChart().props('value')).not.toContain('null');
+ });
+
+ describe('when a field attribute is set', () => {
it('displays a label value instead of metric value when field attribute is used', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ field: 'job' }, { isVector: true }),
});
- return singleStatChart.vm.$nextTick(() => {
- expect(singleStatChart.vm.statValue).toContain('prometheus');
- });
+ expect(findChart().props('value')).toContain('prometheus');
});
it('displays No data to display if field attribute is not present', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ field: 'this-does-not-exist' }),
});
- return singleStatChart.vm.$nextTick(() => {
- expect(singleStatChart.vm.statValue).toContain('No data to display');
- });
+ expect(findChart().props('value')).toContain('No data to display');
});
});
});
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 97386be9e32..6f9a89feb3e 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -12,7 +12,12 @@ import {
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { panelTypes, chartHeight } from '~/monitoring/constants';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
-import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data';
+import {
+ deploymentData,
+ mockProjectDir,
+ annotationsData,
+ mockFixedTimeRange,
+} from '../../mock_data';
import { timeSeriesGraphData } from '../../graph_data';
@@ -42,6 +47,7 @@ describe('Time series component', () => {
deploymentData,
annotations: annotationsData,
projectPath: `${TEST_HOST}${mockProjectDir}`,
+ timeRange: mockFixedTimeRange,
...props,
},
stubs: {
@@ -382,6 +388,25 @@ describe('Time series component', () => {
});
describe('chartOptions', () => {
+ describe('x-Axis bounds', () => {
+ it('is set to the time range bounds', () => {
+ expect(getChartOptions().xAxis).toMatchObject({
+ min: mockFixedTimeRange.start,
+ max: mockFixedTimeRange.end,
+ });
+ });
+
+ it('is not set if time range is not set or incorrectly set', () => {
+ wrapper.setProps({
+ timeRange: {},
+ });
+ return wrapper.vm.$nextTick(() => {
+ expect(getChartOptions().xAxis).not.toHaveProperty('min');
+ expect(getChartOptions().xAxis).not.toHaveProperty('max');
+ });
+ });
+ });
+
describe('dataZoom', () => {
it('renders with scroll handle icons', () => {
expect(getChartOptions().dataZoom).toHaveLength(1);
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
new file mode 100644
index 00000000000..024b2cbd7f1
--- /dev/null
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -0,0 +1,440 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlNewDropdownItem } from '@gitlab/ui';
+import { createStore } from '~/monitoring/stores';
+import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
+import { setupAllDashboards, setupStoreWithData } from '../store_utils';
+import { redirectTo } from '~/lib/utils/url_utility';
+import Tracking from '~/tracking';
+import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
+import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
+import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data';
+import * as types from '~/monitoring/stores/mutation_types';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+ queryToObject: jest.fn(),
+}));
+
+describe('Actions menu', () => {
+ const ootbDashboards = [dashboardGitResponse[0], dashboardGitResponse[2]];
+ const customDashboard = dashboardGitResponse[1];
+
+ let store;
+ let wrapper;
+
+ const findAddMetricItem = () => wrapper.find('[data-testid="add-metric-item"]');
+ const findAddPanelItemEnabled = () => wrapper.find('[data-testid="add-panel-item-enabled"]');
+ const findAddPanelItemDisabled = () => wrapper.find('[data-testid="add-panel-item-disabled"]');
+ const findAddMetricModal = () => wrapper.find('[data-testid="add-metric-modal"]');
+ const findAddMetricModalSubmitButton = () =>
+ wrapper.find('[data-testid="add-metric-modal-submit-button"]');
+ const findStarDashboardItem = () => wrapper.find('[data-testid="star-dashboard-item"]');
+ const findEditDashboardItemEnabled = () =>
+ wrapper.find('[data-testid="edit-dashboard-item-enabled"]');
+ const findEditDashboardItemDisabled = () =>
+ wrapper.find('[data-testid="edit-dashboard-item-disabled"]');
+ const findDuplicateDashboardItem = () => wrapper.find('[data-testid="duplicate-dashboard-item"]');
+ const findDuplicateDashboardModal = () =>
+ wrapper.find('[data-testid="duplicate-dashboard-modal"]');
+ const findCreateDashboardItem = () => wrapper.find('[data-testid="create-dashboard-item"]');
+ const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
+
+ const createShallowWrapper = (props = {}, options = {}) => {
+ wrapper = shallowMount(ActionsMenu, {
+ propsData: { ...dashboardActionsMenuProps, ...props },
+ store,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('add metric item', () => {
+ it('is rendered when custom metrics are available', () => {
+ createShallowWrapper();
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findAddMetricItem().exists()).toBe(true);
+ });
+ });
+
+ it('is not rendered when custom metrics are not available', () => {
+ createShallowWrapper({
+ addingMetricsAvailable: false,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findAddMetricItem().exists()).toBe(false);
+ });
+ });
+
+ describe('when available', () => {
+ beforeEach(() => {
+ createShallowWrapper();
+ });
+
+ it('modal for custom metrics form is rendered', () => {
+ expect(findAddMetricModal().exists()).toBe(true);
+ expect(findAddMetricModal().attributes().modalid).toBe('addMetric');
+ });
+
+ it('add metric modal submit button exists', () => {
+ expect(findAddMetricModalSubmitButton().exists()).toBe(true);
+ });
+
+ it('renders custom metrics form fields', () => {
+ expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true);
+ });
+ });
+
+ describe('when not available', () => {
+ beforeEach(() => {
+ createShallowWrapper({ addingMetricsAvailable: false });
+ });
+
+ it('modal for custom metrics form is not rendered', () => {
+ expect(findAddMetricModal().exists()).toBe(false);
+ });
+ });
+
+ describe('adding new metric from modal', () => {
+ let origPage;
+
+ beforeEach(done => {
+ jest.spyOn(Tracking, 'event').mockReturnValue();
+ createShallowWrapper();
+
+ setupStoreWithData(store);
+
+ origPage = document.body.dataset.page;
+ document.body.dataset.page = 'projects:environments:metrics';
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ afterEach(() => {
+ document.body.dataset.page = origPage;
+ });
+
+ it('is tracked', done => {
+ const submitButton = findAddMetricModalSubmitButton().vm;
+
+ wrapper.vm.$nextTick(() => {
+ submitButton.$el.click();
+ wrapper.vm.$nextTick(() => {
+ expect(Tracking.event).toHaveBeenCalledWith(
+ document.body.dataset.page,
+ 'click_button',
+ {
+ label: 'add_new_metric',
+ property: 'modal',
+ value: undefined,
+ },
+ );
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('add panel item', () => {
+ const GlNewDropdownItemStub = {
+ extends: GlNewDropdownItem,
+ props: {
+ to: [String, Object],
+ },
+ };
+
+ let $route;
+
+ beforeEach(() => {
+ $route = { name: DASHBOARD_PAGE, params: { dashboard: 'my_dashboard.yml' } };
+
+ createShallowWrapper(
+ {
+ isOotbDashboard: false,
+ },
+ {
+ mocks: { $route },
+ stubs: { GlNewDropdownItem: GlNewDropdownItemStub },
+ },
+ );
+ });
+
+ it('is disabled for ootb dashboards', () => {
+ createShallowWrapper({
+ isOotbDashboard: true,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findAddPanelItemDisabled().exists()).toBe(true);
+ });
+ });
+
+ it('is visible for custom dashboards', () => {
+ expect(findAddPanelItemEnabled().exists()).toBe(true);
+ });
+
+ it('renders a link to the new panel page for custom dashboards', () => {
+ expect(findAddPanelItemEnabled().props('to')).toEqual({
+ name: PANEL_NEW_PAGE,
+ params: {
+ dashboard: 'my_dashboard.yml',
+ },
+ });
+ });
+ });
+
+ describe('edit dashboard yml item', () => {
+ beforeEach(() => {
+ createShallowWrapper();
+ });
+
+ describe('when current dashboard is custom', () => {
+ beforeEach(() => {
+ setupAllDashboards(store, customDashboard.path);
+ });
+
+ it('enabled item is rendered and has falsy disabled attribute', () => {
+ expect(findEditDashboardItemEnabled().exists()).toBe(true);
+ expect(findEditDashboardItemEnabled().attributes('disabled')).toBe(undefined);
+ });
+
+ it('enabled item links to their edit path', () => {
+ expect(findEditDashboardItemEnabled().attributes('href')).toBe(
+ customDashboard.project_blob_path,
+ );
+ });
+
+ it('disabled item is not rendered', () => {
+ expect(findEditDashboardItemDisabled().exists()).toBe(false);
+ });
+ });
+
+ describe.each(ootbDashboards)('when current dashboard is OOTB', dashboard => {
+ beforeEach(() => {
+ setupAllDashboards(store, dashboard.path);
+ });
+
+ it('disabled item is rendered and has disabled attribute set on it', () => {
+ expect(findEditDashboardItemDisabled().exists()).toBe(true);
+ expect(findEditDashboardItemDisabled().attributes('disabled')).toBe('');
+ });
+
+ it('enabled item is not rendered', () => {
+ expect(findEditDashboardItemEnabled().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('duplicate dashboard item', () => {
+ beforeEach(() => {
+ createShallowWrapper();
+ });
+
+ describe.each(ootbDashboards)('when current dashboard is OOTB', dashboard => {
+ beforeEach(() => {
+ setupAllDashboards(store, dashboard.path);
+ });
+
+ it('is rendered', () => {
+ expect(findDuplicateDashboardItem().exists()).toBe(true);
+ });
+
+ it('duplicate dashboard modal is rendered', () => {
+ expect(findDuplicateDashboardModal().exists()).toBe(true);
+ });
+
+ it('clicking on item opens up the duplicate dashboard modal', () => {
+ const modalId = 'duplicateDashboard';
+ const modalTrigger = findDuplicateDashboardItem();
+ const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+
+ modalTrigger.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
+ });
+ });
+ });
+
+ describe('when current dashboard is custom', () => {
+ beforeEach(() => {
+ setupAllDashboards(store, customDashboard.path);
+ });
+
+ it('is not rendered', () => {
+ expect(findDuplicateDashboardItem().exists()).toBe(false);
+ });
+
+ it('duplicate dashboard modal is not rendered', () => {
+ expect(findDuplicateDashboardModal().exists()).toBe(false);
+ });
+ });
+
+ describe('when no dashboard is set', () => {
+ it('is not rendered', () => {
+ expect(findDuplicateDashboardItem().exists()).toBe(false);
+ });
+
+ it('duplicate dashboard modal is not rendered', () => {
+ expect(findDuplicateDashboardModal().exists()).toBe(false);
+ });
+ });
+
+ describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
+ beforeEach(() => {
+ store.state.monitoringDashboard.projectPath = 'root/sandbox';
+
+ setupAllDashboards(store, dashboardGitResponse[0].path);
+ });
+
+ it('redirects to the newly created dashboard', () => {
+ delete window.location;
+ window.location = new URL('https://localhost');
+
+ const newDashboard = dashboardGitResponse[1];
+
+ const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
+ findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(redirectTo).toHaveBeenCalled();
+ expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
+ });
+ });
+ });
+ });
+
+ describe('star dashboard item', () => {
+ beforeEach(() => {
+ createShallowWrapper();
+ setupAllDashboards(store);
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ });
+
+ it('is shown', () => {
+ expect(findStarDashboardItem().exists()).toBe(true);
+ });
+
+ it('is not disabled', () => {
+ expect(findStarDashboardItem().attributes('disabled')).toBeFalsy();
+ });
+
+ it('is disabled when starring is taking place', () => {
+ store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findStarDashboardItem().exists()).toBe(true);
+ expect(findStarDashboardItem().attributes('disabled')).toBe('true');
+ });
+ });
+
+ it('on click it dispatches a toggle star action', () => {
+ findStarDashboardItem().vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/toggleStarredValue',
+ undefined,
+ );
+ });
+ });
+
+ describe('when dashboard is not starred', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[0].path,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('item text shows "Star dashboard"', () => {
+ expect(findStarDashboardItem().html()).toMatch(/Star dashboard/);
+ });
+ });
+
+ describe('when dashboard is starred', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[1].path,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('item text shows "Unstar dashboard"', () => {
+ expect(findStarDashboardItem().html()).toMatch(/Unstar dashboard/);
+ });
+ });
+ });
+
+ describe('create dashboard item', () => {
+ beforeEach(() => {
+ createShallowWrapper();
+ });
+
+ it('is rendered by default but it is disabled', () => {
+ expect(findCreateDashboardItem().attributes('disabled')).toBe('true');
+ });
+
+ describe('when project path is set', () => {
+ const mockProjectPath = 'root/sandbox';
+ const mockAddDashboardDocPath = '/doc/add-dashboard';
+
+ beforeEach(() => {
+ store.state.monitoringDashboard.projectPath = mockProjectPath;
+ store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath;
+ });
+
+ it('is not disabled', () => {
+ expect(findCreateDashboardItem().attributes('disabled')).toBe(undefined);
+ });
+
+ it('renders a modal for creating a dashboard', () => {
+ expect(findCreateDashboardModal().exists()).toBe(true);
+ });
+
+ it('clicking opens up the modal', () => {
+ const modalId = 'createDashboard';
+ const modalTrigger = findCreateDashboardItem();
+ const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+
+ modalTrigger.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
+ });
+ });
+
+ it('modal gets passed correct props', () => {
+ expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
+ expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe(
+ mockAddDashboardDocPath,
+ );
+ });
+ });
+
+ describe('when project path is not set', () => {
+ beforeEach(() => {
+ store.state.monitoringDashboard.projectPath = null;
+ });
+
+ it('is disabled', () => {
+ expect(findCreateDashboardItem().attributes('disabled')).toBe('true');
+ });
+
+ it('does not render a modal for creating a dashboard', () => {
+ expect(findCreateDashboardModal().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 5a1a615c703..5cf24706ebd 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -1,16 +1,23 @@
import { shallowMount } from '@vue/test-utils';
+import { GlNewDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
import { createStore } from '~/monitoring/stores';
+import * as types from '~/monitoring/stores/mutation_types';
+import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
+import RefreshButton from '~/monitoring/components/refresh_button.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
-import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
-import { setupAllDashboards } from '../store_utils';
+import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
+import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
+import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
import {
+ environmentData,
dashboardGitResponse,
selfMonitoringDashboardGitResponse,
dashboardHeaderProps,
} from '../mock_data';
import { redirectTo } from '~/lib/utils/url_utility';
+const mockProjectPath = 'https://path/to/project';
+
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
queryToObject: jest.fn(),
@@ -21,13 +28,22 @@ describe('Dashboard header', () => {
let store;
let wrapper;
- const findActionsMenu = () => wrapper.find('[data-testid="actions-menu"]');
- const findCreateDashboardMenuItem = () =>
- findActionsMenu().find('[data-testid="action-create-dashboard"]');
- const findCreateDashboardDuplicateItem = () =>
- findActionsMenu().find('[data-testid="action-duplicate-dashboard"]');
- const findDuplicateDashboardModal = () => wrapper.find(DuplicateDashboardModal);
- const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
+ const findDashboardDropdown = () => wrapper.find(DashboardsDropdown);
+
+ const findEnvsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
+ const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlNewDropdownItem);
+ const findEnvsDropdownSearch = () => findEnvsDropdown().find(GlSearchBoxByType);
+ const findEnvsDropdownSearchMsg = () => wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' });
+ const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().find(GlLoadingIcon);
+
+ const findDateTimePicker = () => wrapper.find(DateTimePicker);
+ const findRefreshButton = () => wrapper.find(RefreshButton);
+
+ const findActionsMenu = () => wrapper.find(ActionsMenu);
+
+ const setSearchTerm = searchTerm => {
+ store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
+ };
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(DashboardHeader, {
@@ -45,139 +61,315 @@ describe('Dashboard header', () => {
wrapper.destroy();
});
- describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
+ describe('dashboards dropdown', () => {
beforeEach(() => {
- store.state.monitoringDashboard.projectPath = 'root/sandbox';
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ projectPath: mockProjectPath,
+ });
+
+ createShallowWrapper();
});
- /**
- * The duplicate dashboard modal gets called both by a menu item from the
- * dashboards dropdown and by an item from the actions menu.
- *
- * This spec is context agnostic, so it addresses all cases where the
- * duplicate dashboard modal gets called.
- */
- it('redirects to the newly created dashboard', () => {
- delete window.location;
- window.location = new URL('https://localhost');
- const newDashboard = dashboardGitResponse[1];
+ it('shows the dashboard dropdown', () => {
+ expect(findDashboardDropdown().exists()).toBe(true);
+ });
- createShallowWrapper();
+ it('when an out of the box dashboard is selected, encodes dashboard path', () => {
+ findDashboardDropdown().vm.$emit('selectDashboard', {
+ path: '.gitlab/dashboards/dashboard&copy.yml',
+ out_of_the_box_dashboard: true,
+ display_name: 'A display name',
+ });
- const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
- findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
+ expect(redirectTo).toHaveBeenCalledWith(
+ `${mockProjectPath}/-/metrics/.gitlab%2Fdashboards%2Fdashboard%26copy.yml`,
+ );
+ });
- return wrapper.vm.$nextTick().then(() => {
- expect(redirectTo).toHaveBeenCalled();
- expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
+ it('when a custom dashboard is selected, encodes dashboard display name', () => {
+ findDashboardDropdown().vm.$emit('selectDashboard', {
+ path: '.gitlab/dashboards/file&path.yml',
+ display_name: 'dashboard&copy.yml',
});
+
+ expect(redirectTo).toHaveBeenCalledWith(`${mockProjectPath}/-/metrics/dashboard%26copy.yml`);
});
});
- describe('actions menu', () => {
+ describe('environments dropdown', () => {
beforeEach(() => {
- store.state.monitoringDashboard.projectPath = '';
createShallowWrapper();
});
- it('is rendered if projectPath is set in store', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+ it('shows the environments dropdown', () => {
+ expect(findEnvsDropdown().exists()).toBe(true);
+ });
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().exists()).toBe(true);
+ it('renders a search input', () => {
+ expect(findEnvsDropdownSearch().exists()).toBe(true);
+ });
+
+ describe('when environments data is not loaded', () => {
+ beforeEach(() => {
+ setupStoreWithDashboard(store);
+ return wrapper.vm.$nextTick();
+ });
+
+ it('there are no environments listed', () => {
+ expect(findEnvsDropdownItems()).toHaveLength(0);
+ });
+ });
+
+ describe('when environments data is loaded', () => {
+ const currentDashboard = dashboardGitResponse[0].path;
+ const currentEnvironmentName = environmentData[0].name;
+
+ beforeEach(() => {
+ setupStoreWithData(store);
+ store.state.monitoringDashboard.projectPath = mockProjectPath;
+ store.state.monitoringDashboard.currentDashboard = currentDashboard;
+ store.state.monitoringDashboard.currentEnvironmentName = currentEnvironmentName;
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders dropdown items with the environment name', () => {
+ const path = `${mockProjectPath}/-/metrics/${encodeURIComponent(currentDashboard)}`;
+
+ findEnvsDropdownItems().wrappers.forEach((itemWrapper, index) => {
+ const { name, id } = environmentData[index];
+ const idParam = encodeURIComponent(id);
+
+ expect(itemWrapper.text()).toBe(name);
+ expect(itemWrapper.attributes('href')).toBe(`${path}?environment=${idParam}`);
+ });
+ });
+
+ it('environments dropdown items can be checked', () => {
+ const items = findEnvsDropdownItems();
+ const checkItems = findEnvsDropdownItems().filter(item => item.props('isCheckItem'));
+
+ expect(items).toHaveLength(checkItems.length);
+ });
+
+ it('checks the currently selected environment', () => {
+ const selectedItems = findEnvsDropdownItems().filter(item => item.props('isChecked'));
+
+ expect(selectedItems).toHaveLength(1);
+ expect(selectedItems.at(0).text()).toBe(currentEnvironmentName);
+ });
+
+ it('filters rendered dropdown items', () => {
+ const searchTerm = 'production';
+ const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
+ setSearchTerm(searchTerm);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findEnvsDropdownItems()).toHaveLength(resultEnvs.length);
+ });
+ });
+
+ it('does not filter dropdown items if search term is empty string', () => {
+ const searchTerm = '';
+ setSearchTerm(searchTerm);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findEnvsDropdownItems()).toHaveLength(environmentData.length);
+ });
+ });
+
+ it("shows error message if search term doesn't match", () => {
+ const searchTerm = 'does-not-exist';
+ setSearchTerm(searchTerm);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findEnvsDropdownSearchMsg().isVisible()).toBe(true);
+ });
+ });
+
+ it('shows loading element when environments fetch is still loading', () => {
+ store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(findEnvsDropdownLoadingIcon().exists()).toBe(true);
+ })
+ .then(() => {
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
+ environmentData,
+ );
+ })
+ .then(() => {
+ expect(findEnvsDropdownLoadingIcon().exists()).toBe(false);
+ });
});
});
+ });
- it('is not rendered if projectPath is not set in store', () => {
- expect(findActionsMenu().exists()).toBe(false);
+ describe('date time picker', () => {
+ beforeEach(() => {
+ createShallowWrapper();
});
- it('contains a modal', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+ it('is rendered', () => {
+ expect(findDateTimePicker().exists()).toBe(true);
+ });
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().contains(CreateDashboardModal)).toBe(true);
+ describe('timezone setting', () => {
+ const setupWithTimezone = value => {
+ store = createStore({ dashboardTimezone: value });
+ createShallowWrapper();
+ };
+
+ describe('local timezone is enabled by default', () => {
+ it('shows the data time picker in local timezone', () => {
+ expect(findDateTimePicker().props('utc')).toBe(false);
+ });
+ });
+
+ describe('when LOCAL timezone is enabled', () => {
+ beforeEach(() => {
+ setupWithTimezone('LOCAL');
+ });
+
+ it('shows the data time picker in local timezone', () => {
+ expect(findDateTimePicker().props('utc')).toBe(false);
+ });
+ });
+
+ describe('when UTC timezone is enabled', () => {
+ beforeEach(() => {
+ setupWithTimezone('UTC');
+ });
+
+ it('shows the data time picker in UTC format', () => {
+ expect(findDateTimePicker().props('utc')).toBe(true);
+ });
});
});
+ });
+
+ describe('refresh button', () => {
+ beforeEach(() => {
+ createShallowWrapper();
+ });
+
+ it('is rendered', () => {
+ expect(findRefreshButton().exists()).toBe(true);
+ });
+ });
+
+ describe('external dashboard link', () => {
+ beforeEach(() => {
+ store.state.monitoringDashboard.externalDashboardUrl = '/mockUrl';
+ createShallowWrapper();
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows the link', () => {
+ const externalDashboardButton = wrapper.find('.js-external-dashboard-link');
+
+ expect(externalDashboardButton.exists()).toBe(true);
+ expect(externalDashboardButton.is(GlButton)).toBe(true);
+ expect(externalDashboardButton.text()).toContain('View full dashboard');
+ });
+ });
- const duplicableCases = [
- null, // When no path is specified, it uses the default dashboard path.
+ describe('actions menu', () => {
+ const ootbDashboards = [
dashboardGitResponse[0].path,
- dashboardGitResponse[2].path,
selfMonitoringDashboardGitResponse[0].path,
];
+ const customDashboards = [
+ dashboardGitResponse[1].path,
+ selfMonitoringDashboardGitResponse[1].path,
+ ];
- describe.each(duplicableCases)(
- 'when the selected dashboard can be duplicated',
- dashboardPath => {
- it('contains a "Create New" menu item and a "Duplicate Dashboard" menu item', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
- setupAllDashboards(store, dashboardPath);
+ it('is rendered', () => {
+ createShallowWrapper();
- return wrapper.vm.$nextTick().then(() => {
- expect(findCreateDashboardMenuItem().exists()).toBe(true);
- expect(findCreateDashboardDuplicateItem().exists()).toBe(true);
- });
+ expect(findActionsMenu().exists()).toBe(true);
+ });
+
+ describe('adding metrics prop', () => {
+ it.each(ootbDashboards)('gets passed true if current dashboard is OOTB', dashboardPath => {
+ createShallowWrapper({ customMetricsAvailable: true });
+
+ store.state.monitoringDashboard.emptyState = false;
+ setupAllDashboards(store, dashboardPath);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(true);
});
- },
- );
+ });
- const nonDuplicableCases = [
- dashboardGitResponse[1].path,
- selfMonitoringDashboardGitResponse[1].path,
- ];
+ it.each(customDashboards)(
+ 'gets passed false if current dashboard is custom',
+ dashboardPath => {
+ createShallowWrapper({ customMetricsAvailable: true });
- describe.each(nonDuplicableCases)(
- 'when the selected dashboard cannot be duplicated',
- dashboardPath => {
- it('contains a "Create New" menu item and no "Duplicate Dashboard" menu item', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+ store.state.monitoringDashboard.emptyState = false;
setupAllDashboards(store, dashboardPath);
return wrapper.vm.$nextTick().then(() => {
- expect(findCreateDashboardMenuItem().exists()).toBe(true);
- expect(findCreateDashboardDuplicateItem().exists()).toBe(false);
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
});
+ },
+ );
+
+ it('gets passed false if empty state is shown', () => {
+ createShallowWrapper({ customMetricsAvailable: true });
+
+ store.state.monitoringDashboard.emptyState = true;
+ setupAllDashboards(store, ootbDashboards[0]);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
});
- },
- );
- });
+ });
- describe('actions menu modals', () => {
- const url = 'https://path/to/project';
+ it('gets passed false if custom metrics are not available', () => {
+ createShallowWrapper({ customMetricsAvailable: false });
- beforeEach(() => {
- store.state.monitoringDashboard.projectPath = url;
- setupAllDashboards(store);
+ store.state.monitoringDashboard.emptyState = false;
+ setupAllDashboards(store, ootbDashboards[0]);
- createShallowWrapper();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
+ });
+ });
});
- it('Clicking on "Create New" opens up a modal', () => {
- const modalId = 'createDashboard';
- const modalTrigger = findCreateDashboardMenuItem();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+ it('custom metrics path gets passed', () => {
+ const path = 'https://path/to/customMetrics';
- modalTrigger.trigger('click');
+ createShallowWrapper({ customMetricsPath: path });
return wrapper.vm.$nextTick().then(() => {
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
+ expect(findActionsMenu().props('customMetricsPath')).toBe(path);
});
});
- it('"Create new dashboard" modal contains correct buttons', () => {
- expect(findCreateDashboardModal().props('projectPath')).toBe(url);
+ it('validate query path gets passed', () => {
+ const path = 'https://path/to/validateQuery';
+
+ createShallowWrapper({ validateQueryPath: path });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().props('validateQueryPath')).toBe(path);
+ });
});
- it('"Duplicate Dashboard" opens up a modal', () => {
- const modalId = 'duplicateDashboard';
- const modalTrigger = findCreateDashboardDuplicateItem();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+ it('default branch gets passed', () => {
+ const branch = 'branchName';
- modalTrigger.trigger('click');
+ createShallowWrapper({ defaultBranch: branch });
return wrapper.vm.$nextTick().then(() => {
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
+ expect(findActionsMenu().props('defaultBranch')).toBe(branch);
});
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
new file mode 100644
index 00000000000..587ddd23d3f
--- /dev/null
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -0,0 +1,234 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlCard, GlForm, GlFormTextarea, GlAlert } from '@gitlab/ui';
+import { createStore } from '~/monitoring/stores';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
+import * as types from '~/monitoring/stores/mutation_types';
+import { metricsDashboardResponse } from '../fixture_data';
+import { mockTimeRange } from '../mock_data';
+
+import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
+import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
+
+const mockPanel = metricsDashboardResponse.dashboard.panel_groups[0].panels[0];
+
+describe('dashboard invalid url parameters', () => {
+ let store;
+ let wrapper;
+ let mockShowToast;
+
+ const createComponent = (props = {}, options = {}) => {
+ wrapper = shallowMount(DashboardPanelBuilder, {
+ propsData: { ...props },
+ store,
+ stubs: {
+ GlCard,
+ },
+ mocks: {
+ $toast: {
+ show: mockShowToast,
+ },
+ },
+ options,
+ });
+ };
+
+ const findForm = () => wrapper.find(GlForm);
+ const findTxtArea = () => findForm().find(GlFormTextarea);
+ const findSubmitBtn = () => findForm().find('[type="submit"]');
+ const findClipboardCopyBtn = () => wrapper.find({ ref: 'clipboardCopyBtn' });
+ const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' });
+ const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' });
+ const findPanel = () => wrapper.find(DashboardPanel);
+ const findTimeRangePicker = () => wrapper.find(DateTimePicker);
+ const findRefreshButton = () => wrapper.find('[data-testid="previewRefreshButton"]');
+
+ beforeEach(() => {
+ mockShowToast = jest.fn();
+ store = createStore();
+ createComponent();
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ });
+
+ afterEach(() => {});
+
+ it('is mounted', () => {
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('displays an empty dashboard panel', () => {
+ expect(findPanel().exists()).toBe(true);
+ expect(findPanel().props('graphData')).toBe(null);
+ });
+
+ it('does not fetch initial data by default', () => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
+ describe('yml form', () => {
+ it('form exists and can be submitted', () => {
+ expect(findForm().exists()).toBe(true);
+ expect(findSubmitBtn().exists()).toBe(true);
+ expect(findSubmitBtn().is('[disabled]')).toBe(false);
+ });
+
+ it('form has a text area with a default value', () => {
+ expect(findTxtArea().exists()).toBe(true);
+
+ const value = findTxtArea().attributes('value');
+
+ // Panel definition should contain a title and a type
+ expect(value).toContain('title:');
+ expect(value).toContain('type:');
+ });
+
+ it('"copy to clipboard" button works', () => {
+ findClipboardCopyBtn().vm.$emit('click');
+ const clipboardText = findClipboardCopyBtn().attributes('data-clipboard-text');
+
+ expect(clipboardText).toContain('title:');
+ expect(clipboardText).toContain('type:');
+
+ expect(mockShowToast).toHaveBeenCalledTimes(1);
+ });
+
+ it('on submit fetches a panel preview', () => {
+ findForm().vm.$emit('submit', new Event('submit'));
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/fetchPanelPreview',
+ expect.stringContaining('title:'),
+ );
+ });
+ });
+
+ describe('when form is submitted', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.REQUEST_PANEL_PREVIEW}`, 'mock yml content');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('submit button is disabled', () => {
+ expect(findSubmitBtn().is('[disabled]')).toBe(true);
+ });
+ });
+ });
+
+ describe('time range picker', () => {
+ it('is visible by default', () => {
+ expect(findTimeRangePicker().exists()).toBe(true);
+ });
+
+ it('when changed does not trigger data fetch unless preview panel button is clicked', () => {
+ // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
+ store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ it('when changed triggers data fetch if preview panel button is clicked', () => {
+ findForm().vm.$emit('submit', new Event('submit'));
+
+ store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(store.dispatch).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('refresh', () => {
+ it('is visible by default', () => {
+ expect(findRefreshButton().exists()).toBe(true);
+ });
+
+ it('when clicked does not trigger data fetch unless preview panel button is clicked', () => {
+ // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
+ store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ it('when clicked triggers data fetch if preview panel button is clicked', () => {
+ // mimic state where preview is visible. SET_PANEL_PREVIEW_IS_SHOWN is set to true
+ store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, true);
+
+ findRefreshButton().vm.$emit('click');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/fetchPanelPreviewMetrics',
+ undefined,
+ );
+ });
+ });
+ });
+
+ describe('instructions card', () => {
+ const mockDocsPath = '/docs-path';
+ const mockProjectPath = '/project-path';
+
+ beforeEach(() => {
+ store.state.monitoringDashboard.addDashboardDocumentationPath = mockDocsPath;
+ store.state.monitoringDashboard.projectPath = mockProjectPath;
+
+ createComponent();
+ });
+
+ it('displays next actions for the user', () => {
+ expect(findViewDocumentationBtn().exists()).toBe(true);
+ expect(findViewDocumentationBtn().attributes('href')).toBe(mockDocsPath);
+
+ expect(findOpenRepositoryBtn().exists()).toBe(true);
+ expect(findOpenRepositoryBtn().attributes('href')).toBe(mockProjectPath);
+ });
+ });
+
+ describe('when there is an error', () => {
+ const mockError = 'an error ocurred!';
+
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_FAILURE}`, mockError);
+ return wrapper.vm.$nextTick();
+ });
+
+ it('displays an alert', () => {
+ expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.find(GlAlert).text()).toBe(mockError);
+ });
+
+ it('displays an empty dashboard panel', () => {
+ expect(findPanel().props('graphData')).toBe(null);
+ });
+
+ it('changing time range should not refetch data', () => {
+ store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when panel data is available', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_SUCCESS}`, mockPanel);
+ return wrapper.vm.$nextTick();
+ });
+
+ it('displays no alert', () => {
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
+ });
+
+ it('displays panel with data', () => {
+ const { title, type } = wrapper.find(DashboardPanel).props('graphData');
+
+ expect(title).toBe(mockPanel.title);
+ expect(type).toBe(mockPanel.type);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index 693818aa55a..fb96bcc042f 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -2,23 +2,23 @@ import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { setTestTimeout } from 'helpers/timeout';
+import { GlNewDropdownItem as GlDropdownItem } from '@gitlab/ui';
import invalidUrl from '~/lib/utils/invalid_url';
import axios from '~/lib/utils/axios_utils';
-import { GlNewDropdownItem as GlDropdownItem } from '@gitlab/ui';
import AlertWidget from '~/monitoring/components/alert_widget.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import {
+ mockAlert,
mockLogsHref,
mockLogsPath,
mockNamespace,
mockNamespacedData,
mockTimeRange,
- graphDataPrometheusQueryRangeMultiTrack,
barMockData,
} from '../mock_data';
import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
-import { anomalyGraphData, singleStatGraphData } from '../graph_data';
+import { anomalyGraphData, singleStatGraphData, heatmapGraphData } from '../graph_data';
import { panelTypes } from '~/monitoring/constants';
@@ -56,9 +56,10 @@ describe('Dashboard Panel', () => {
const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' });
const findMenuItems = () => wrapper.findAll(GlDropdownItem);
const findMenuItemByText = text => findMenuItems().filter(i => i.text() === text);
+ const findAlertsWidget = () => wrapper.find(AlertWidget);
- const createWrapper = (props, options) => {
- wrapper = shallowMount(DashboardPanel, {
+ const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
+ wrapper = mountFn(DashboardPanel, {
propsData: {
graphData,
settingsPath: dashboardProps.settingsPath,
@@ -79,6 +80,9 @@ describe('Dashboard Panel', () => {
});
};
+ const setMetricsSavedToDb = val =>
+ monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
+
beforeEach(() => {
setTestTimeout(1000);
@@ -235,7 +239,7 @@ describe('Dashboard Panel', () => {
${anomalyGraphData()} | ${MonitorAnomalyChart} | ${false}
${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart} | ${false}
${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart} | ${false}
- ${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart} | ${false}
+ ${heatmapGraphData()} | ${MonitorHeatmapChart} | ${false}
${barMockData} | ${MonitorBarChart} | ${false}
`('when $data.type data is provided', ({ data, component, hasCtxMenu }) => {
const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
@@ -255,6 +259,35 @@ describe('Dashboard Panel', () => {
});
});
});
+
+ describe('computed', () => {
+ describe('fixedCurrentTimeRange', () => {
+ it('returns fixed time for valid time range', () => {
+ state.timeRange = mockTimeRange;
+ return wrapper.vm.$nextTick(() => {
+ expect(findTimeChart().props('timeRange')).toEqual(
+ expect.objectContaining({
+ start: expect.any(String),
+ end: expect.any(String),
+ }),
+ );
+ });
+ });
+
+ it.each`
+ input | output
+ ${''} | ${{}}
+ ${undefined} | ${{}}
+ ${null} | ${{}}
+ ${'2020-12-03'} | ${{}}
+ `('returns $output for invalid input like $input', ({ input, output }) => {
+ state.timeRange = input;
+ return wrapper.vm.$nextTick(() => {
+ expect(findTimeChart().props('timeRange')).toEqual(output);
+ });
+ });
+ });
+ });
});
describe('Edit custom metric dropdown item', () => {
@@ -444,7 +477,7 @@ describe('Dashboard Panel', () => {
describe('csvText', () => {
it('converts metrics data from json to csv', () => {
- const header = `timestamp,${graphData.y_label}`;
+ const header = `timestamp,"${graphData.y_label} > ${graphData.metrics[0].label}"`;
const data = graphData.metrics[0].result[0].values;
const firstRow = `${data[0][0]},${data[0][1]}`;
const secondRow = `${data[1][0]},${data[1][1]}`;
@@ -523,7 +556,7 @@ describe('Dashboard Panel', () => {
});
it('displays a heatmap in local timezone', () => {
- createWrapper({ graphData: graphDataPrometheusQueryRangeMultiTrack });
+ createWrapper({ graphData: heatmapGraphData() });
expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('LOCAL');
});
@@ -538,7 +571,7 @@ describe('Dashboard Panel', () => {
});
it('displays a heatmap with UTC', () => {
- createWrapper({ graphData: graphDataPrometheusQueryRangeMultiTrack });
+ createWrapper({ graphData: heatmapGraphData() });
expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('UTC');
});
});
@@ -573,10 +606,6 @@ describe('Dashboard Panel', () => {
});
describe('panel alerts', () => {
- const setMetricsSavedToDb = val =>
- monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
- const findAlertsWidget = () => wrapper.find(AlertWidget);
-
beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []);
@@ -702,4 +731,60 @@ describe('Dashboard Panel', () => {
expect(findManageLinksItem().exists()).toBe(false);
});
});
+
+ describe('Runbook url', () => {
+ const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]');
+ const { metricId } = graphData.metrics[0];
+ const { alert_path: alertPath } = mockAlert;
+
+ const mockRunbookAlert = {
+ ...mockAlert,
+ metricId,
+ };
+
+ beforeEach(() => {
+ mockGetterReturnValue('metricsSavedToDb', []);
+ });
+
+ it('does not show a runbook link when alerts are not present', () => {
+ createWrapper();
+
+ expect(findRunbookLinks().length).toBe(0);
+ });
+
+ describe('when alerts are present', () => {
+ beforeEach(() => {
+ setMetricsSavedToDb([metricId]);
+
+ createWrapper({
+ alertsEndpoint: '/endpoint',
+ prometheusAlertsAvailable: true,
+ });
+ });
+
+ it('does not show a runbook link when a runbook is not set', async () => {
+ findAlertsWidget().vm.$emit('setAlerts', alertPath, {
+ ...mockRunbookAlert,
+ runbookUrl: '',
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(findRunbookLinks().length).toBe(0);
+ });
+
+ it('shows a runbook link when a runbook is set', async () => {
+ findAlertsWidget().vm.$emit('setAlerts', alertPath, mockRunbookAlert);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findRunbookLinks().length).toBe(1);
+ expect(
+ findRunbookLinks()
+ .at(0)
+ .attributes('href'),
+ ).toBe(invalidUrl);
+ });
+ });
+ });
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 4b7f7a9ddb3..f37d95317ab 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -1,19 +1,14 @@
import { shallowMount, mount } from '@vue/test-utils';
-import Tracking from '~/tracking';
-import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
-import { GlModal, GlDropdownItem, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
-import { objectToQuery } from '~/lib/utils/url_utility';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import { ESC_KEY } from '~/lib/utils/keys';
+import { objectToQuery } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import RefreshButton from '~/monitoring/components/refresh_button.vue';
-import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
-import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
import EmptyState from '~/monitoring/components/empty_state.vue';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
@@ -29,14 +24,13 @@ import {
setupStoreWithDataForPanelCount,
setupStoreWithLinks,
} from '../store_utils';
-import { environmentData, dashboardGitResponse, storeVariables } from '../mock_data';
+import { dashboardGitResponse, storeVariables } from '../mock_data';
import {
metricsDashboardViewModel,
metricsDashboardPanelCount,
dashboardProps,
} from '../fixture_data';
-import createFlash from '~/flash';
-import { TEST_HOST } from 'helpers/test_constants';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
@@ -45,14 +39,6 @@ describe('Dashboard', () => {
let wrapper;
let mock;
- const findDashboardHeader = () => wrapper.find(DashboardHeader);
- const findEnvironmentsDropdown = () =>
- findDashboardHeader().find({ ref: 'monitorEnvironmentsDropdown' });
- const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
- const setSearchTerm = searchTerm => {
- store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
- };
-
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(Dashboard, {
propsData: { ...dashboardProps, ...props },
@@ -90,28 +76,6 @@ describe('Dashboard', () => {
}
});
- describe('no metrics are available yet', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('shows the environment selector', () => {
- expect(findEnvironmentsDropdown().exists()).toBe(true);
- });
- });
-
- describe('no data found', () => {
- beforeEach(() => {
- createShallowWrapper();
-
- return wrapper.vm.$nextTick();
- });
-
- it('shows the environment selector dropdown', () => {
- expect(findEnvironmentsDropdown().exists()).toBe(true);
- });
- });
-
describe('request information to the server', () => {
it('calls to set time range and fetch data', () => {
createShallowWrapper({ hasMetrics: true });
@@ -149,17 +113,14 @@ describe('Dashboard', () => {
});
it('fetches the metrics data with proper time window', () => {
- jest.spyOn(store, 'dispatch');
-
createMountedWrapper({ hasMetrics: true });
- store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
-
return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalled();
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/setTimeRange',
+ expect.objectContaining({ duration: { seconds: 28800 } }),
+ );
});
});
});
@@ -427,37 +388,6 @@ describe('Dashboard', () => {
);
});
});
-
- describe('when custom dashboard is selected', () => {
- const windowLocation = window.location;
- const findDashboardDropdown = () => wrapper.find(DashboardHeader).find(DashboardsDropdown);
-
- beforeEach(() => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- projectPath: TEST_HOST,
- });
-
- delete window.location;
- window.location = { ...windowLocation, assign: jest.fn() };
- createMountedWrapper();
-
- return wrapper.vm.$nextTick();
- });
-
- afterEach(() => {
- window.location = windowLocation;
- });
-
- it('encodes dashboard param', () => {
- findDashboardDropdown().vm.$emit('selectDashboard', {
- path: '.gitlab/dashboards/dashboard&copy.yml',
- display_name: 'dashboard&copy.yml',
- });
- expect(window.location.assign).toHaveBeenCalledWith(
- `${TEST_HOST}/-/metrics/dashboard%26copy.yml`,
- );
- });
- });
});
describe('when all panels in the first group are loading', () => {
@@ -500,21 +430,6 @@ describe('Dashboard', () => {
return wrapper.vm.$nextTick();
});
- it('renders the environments dropdown with a number of environments', () => {
- expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
-
- findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
- const anchorEl = itemWrapper.find('a');
- if (anchorEl.exists()) {
- const href = anchorEl.attributes('href');
- const currentDashboard = encodeURIComponent(dashboardGitResponse[0].path);
- const environmentId = encodeURIComponent(environmentData[index].id);
- const url = `${TEST_HOST}/-/metrics/${currentDashboard}?environment=${environmentId}`;
- expect(href).toBe(url);
- }
- });
- });
-
it('it does not show loading icons in any group', () => {
setupStoreWithData(store);
@@ -524,127 +439,6 @@ describe('Dashboard', () => {
});
});
});
-
- // Note: This test is not working, .active does not show the active environment
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('renders the environments dropdown with a single active element', () => {
- const activeItem = findAllEnvironmentsDropdownItems().wrappers.filter(itemWrapper =>
- itemWrapper.find('.active').exists(),
- );
-
- expect(activeItem.length).toBe(1);
- });
- });
-
- describe('star dashboards', () => {
- const findToggleStar = () => wrapper.find(DashboardHeader).find({ ref: 'toggleStarBtn' });
- const findToggleStarIcon = () => findToggleStar().find(GlIcon);
-
- beforeEach(() => {
- createShallowWrapper();
- setupAllDashboards(store);
- });
-
- it('toggle star button is shown', () => {
- expect(findToggleStar().exists()).toBe(true);
- expect(findToggleStar().props('disabled')).toBe(false);
- });
-
- it('toggle star button is disabled when starring is taking place', () => {
- store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
-
- return wrapper.vm.$nextTick(() => {
- expect(findToggleStar().exists()).toBe(true);
- expect(findToggleStar().props('disabled')).toBe(true);
- });
- });
-
- describe('when the dashboard list is loaded', () => {
- // Tooltip element should wrap directly
- const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title');
-
- beforeEach(() => {
- setupAllDashboards(store);
- jest.spyOn(store, 'dispatch');
- });
-
- it('dispatches a toggle star action', () => {
- findToggleStar().vm.$emit('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/toggleStarredValue',
- undefined,
- );
- });
- });
-
- describe('when dashboard is not starred', () => {
- beforeEach(() => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboardGitResponse[0].path,
- });
- return wrapper.vm.$nextTick();
- });
-
- it('toggle star button shows "Star dashboard"', () => {
- expect(getToggleTooltip()).toBe('Star dashboard');
- });
-
- it('toggle star button shows an unstarred state', () => {
- expect(findToggleStarIcon().attributes('name')).toBe('star-o');
- });
- });
-
- describe('when dashboard is starred', () => {
- beforeEach(() => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboardGitResponse[1].path,
- });
- return wrapper.vm.$nextTick();
- });
-
- it('toggle star button shows "Star dashboard"', () => {
- expect(getToggleTooltip()).toBe('Unstar dashboard');
- });
-
- it('toggle star button shows a starred state', () => {
- expect(findToggleStarIcon().attributes('name')).toBe('star');
- });
- });
- });
- });
-
- it('hides the environments dropdown list when there is no environments', () => {
- createMountedWrapper({ hasMetrics: true });
-
- setupStoreWithDashboard(store);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findAllEnvironmentsDropdownItems()).toHaveLength(0);
- });
- });
-
- it('renders the datetimepicker dropdown', () => {
- createMountedWrapper({ hasMetrics: true });
-
- setupStoreWithData(store);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DateTimePicker).exists()).toBe(true);
- });
- });
-
- it('renders the refresh dashboard button', () => {
- createMountedWrapper({ hasMetrics: true });
-
- setupStoreWithData(store);
-
- return wrapper.vm.$nextTick().then(() => {
- const refreshBtn = wrapper.find(DashboardHeader).find(RefreshButton);
-
- expect(refreshBtn.exists()).toBe(true);
- });
});
describe('variables section', () => {
@@ -772,15 +566,6 @@ describe('Dashboard', () => {
undefined,
);
});
-
- it('restores dashboard from full screen by typing the Escape key on IE11', () => {
- mockKeyup(ESC_KEY_IE11);
-
- expect(store.dispatch).toHaveBeenCalledWith(
- `monitoringDashboard/clearExpandedPanel`,
- undefined,
- );
- });
});
});
@@ -811,100 +596,6 @@ describe('Dashboard', () => {
});
});
- describe('searchable environments dropdown', () => {
- beforeEach(() => {
- createMountedWrapper({ hasMetrics: true }, { attachToDocument: true });
-
- setupStoreWithData(store);
-
- return wrapper.vm.$nextTick();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders a search input', () => {
- expect(
- wrapper
- .find(DashboardHeader)
- .find({ ref: 'monitorEnvironmentsDropdownSearch' })
- .exists(),
- ).toBe(true);
- });
-
- it('renders dropdown items', () => {
- findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
- const anchorEl = itemWrapper.find('a');
- if (anchorEl.exists()) {
- expect(anchorEl.text()).toBe(environmentData[index].name);
- }
- });
- });
-
- it('filters rendered dropdown items', () => {
- const searchTerm = 'production';
- const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
- setSearchTerm(searchTerm);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findAllEnvironmentsDropdownItems().length).toEqual(resultEnvs.length);
- });
- });
-
- it('does not filter dropdown items if search term is empty string', () => {
- const searchTerm = '';
- setSearchTerm(searchTerm);
-
- return wrapper.vm.$nextTick(() => {
- expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
- });
- });
-
- it("shows error message if search term doesn't match", () => {
- const searchTerm = 'does-not-exist';
- setSearchTerm(searchTerm);
-
- return wrapper.vm.$nextTick(() => {
- expect(
- wrapper
- .find(DashboardHeader)
- .find({ ref: 'monitorEnvironmentsDropdownMsg' })
- .isVisible(),
- ).toBe(true);
- });
- });
-
- it('shows loading element when environments fetch is still loading', () => {
- store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
-
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(
- wrapper
- .find(DashboardHeader)
- .find({ ref: 'monitorEnvironmentsDropdownLoading' })
- .exists(),
- ).toBe(true);
- })
- .then(() => {
- store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
- })
- .then(() => {
- expect(
- wrapper
- .find(DashboardHeader)
- .find({ ref: 'monitorEnvironmentsDropdownLoading' })
- .exists(),
- ).toBe(false);
- });
- });
- });
-
describe('drag and drop function', () => {
const findDraggables = () => wrapper.findAll(VueDraggable);
const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled'));
@@ -998,57 +689,6 @@ describe('Dashboard', () => {
});
});
- describe('dashboard timezone', () => {
- const setupWithTimezone = value => {
- store = createStore({ dashboardTimezone: value });
- setupStoreWithData(store);
- createShallowWrapper({ hasMetrics: true });
- return wrapper.vm.$nextTick;
- };
-
- describe('local timezone is enabled by default', () => {
- beforeEach(() => {
- return setupWithTimezone();
- });
-
- it('shows the data time picker in local timezone', () => {
- expect(
- findDashboardHeader()
- .find(DateTimePicker)
- .props('utc'),
- ).toBe(false);
- });
- });
-
- describe('when LOCAL timezone is enabled', () => {
- beforeEach(() => {
- return setupWithTimezone('LOCAL');
- });
-
- it('shows the data time picker in local timezone', () => {
- expect(
- findDashboardHeader()
- .find(DateTimePicker)
- .props('utc'),
- ).toBe(false);
- });
- });
-
- describe('when UTC timezone is enabled', () => {
- beforeEach(() => {
- return setupWithTimezone('UTC');
- });
-
- it('shows the data time picker in UTC format', () => {
- expect(
- findDashboardHeader()
- .find(DateTimePicker)
- .props('utc'),
- ).toBe(true);
- });
- });
- });
-
describe('cluster health', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true, showHeader: false });
@@ -1068,36 +708,9 @@ describe('Dashboard', () => {
});
});
- describe('dashboard edit link', () => {
- const findEditLink = () => wrapper.find('.js-edit-link');
-
- beforeEach(() => {
- createShallowWrapper({ hasMetrics: true });
-
- setupAllDashboards(store);
- return wrapper.vm.$nextTick();
- });
-
- it('is not present for the default dashboard', () => {
- expect(findEditLink().exists()).toBe(false);
- });
-
- it('is present for a custom dashboard, and links to its edit_path', () => {
- const dashboard = dashboardGitResponse[1];
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboard.path,
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findEditLink().exists()).toBe(true);
- expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path);
- });
- });
- });
-
describe('document title', () => {
const originalTitle = 'Original Title';
- const defaultDashboardName = dashboardGitResponse[0].display_name;
+ const overviewDashboardName = dashboardGitResponse[0].display_name;
beforeEach(() => {
document.title = originalTitle;
@@ -1108,11 +721,11 @@ describe('Dashboard', () => {
document.title = '';
});
- it('is prepended with default dashboard name by default', () => {
+ it('is prepended with the overview dashboard name by default', () => {
setupAllDashboards(store);
return wrapper.vm.$nextTick().then(() => {
- expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true);
+ expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
});
@@ -1127,11 +740,11 @@ describe('Dashboard', () => {
});
});
- it('is prepended with default dashboard name is path is not known', () => {
+ it('is prepended with the overview dashboard name if path is not known', () => {
setupAllDashboards(store, 'unknown/path');
return wrapper.vm.$nextTick().then(() => {
- expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true);
+ expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
});
@@ -1151,41 +764,6 @@ describe('Dashboard', () => {
});
});
- describe('Dashboard dropdown', () => {
- beforeEach(() => {
- createMountedWrapper({ hasMetrics: true });
- setupAllDashboards(store);
- return wrapper.vm.$nextTick();
- });
-
- it('shows the dashboard dropdown', () => {
- const dashboardDropdown = wrapper.find(DashboardsDropdown);
-
- expect(dashboardDropdown.exists()).toBe(true);
- });
- });
-
- describe('external dashboard link', () => {
- beforeEach(() => {
- createMountedWrapper({
- hasMetrics: true,
- showPanels: false,
- showTimeWindowDropdown: false,
- externalDashboardUrl: '/mockUrl',
- });
-
- return wrapper.vm.$nextTick();
- });
-
- it('shows the link', () => {
- const externalDashboardButton = wrapper.find('.js-external-dashboard-link');
-
- expect(externalDashboardButton.exists()).toBe(true);
- expect(externalDashboardButton.is(GlDeprecatedButton)).toBe(true);
- expect(externalDashboardButton.text()).toContain('View full dashboard');
- });
- });
-
describe('Clipboard text in panels', () => {
const currentDashboard = dashboardGitResponse[1].path;
const panelIndex = 1; // skip expanded panel
@@ -1243,74 +821,4 @@ describe('Dashboard', () => {
expect(dashboardPanel.exists()).toBe(true);
});
});
-
- describe('add custom metrics', () => {
- const findAddMetricButton = () => wrapper.find(DashboardHeader).find({ ref: 'addMetricBtn' });
-
- describe('when not available', () => {
- beforeEach(() => {
- createShallowWrapper({
- hasMetrics: true,
- customMetricsPath: '/endpoint',
- });
- });
- it('does not render add button on the dashboard', () => {
- expect(findAddMetricButton().exists()).toBe(false);
- });
- });
-
- describe('when available', () => {
- let origPage;
- beforeEach(done => {
- jest.spyOn(Tracking, 'event').mockReturnValue();
- createShallowWrapper({
- hasMetrics: true,
- customMetricsPath: '/endpoint',
- customMetricsAvailable: true,
- });
- setupStoreWithData(store);
-
- origPage = document.body.dataset.page;
- document.body.dataset.page = 'projects:environments:metrics';
-
- wrapper.vm.$nextTick(done);
- });
- afterEach(() => {
- document.body.dataset.page = origPage;
- });
-
- it('renders add button on the dashboard', () => {
- expect(findAddMetricButton()).toBeDefined();
- });
-
- it('uses modal for custom metrics form', () => {
- expect(wrapper.find(GlModal).exists()).toBe(true);
- expect(wrapper.find(GlModal).attributes().modalid).toBe('addMetric');
- });
- it('adding new metric is tracked', done => {
- const submitButton = wrapper
- .find(DashboardHeader)
- .find({ ref: 'submitCustomMetricsFormBtn' }).vm;
- wrapper.vm.$nextTick(() => {
- submitButton.$el.click();
- wrapper.vm.$nextTick(() => {
- expect(Tracking.event).toHaveBeenCalledWith(
- document.body.dataset.page,
- 'click_button',
- {
- label: 'add_new_metric',
- property: 'modal',
- value: undefined,
- },
- );
- done();
- });
- });
- });
-
- it('renders custom metrics form fields', () => {
- expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true);
- });
- });
- });
});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index 276e20bae6a..c4630bde32f 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
queryToObject,
redirectTo,
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index d09fcc92ee7..89adbad386f 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -1,12 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { GlNewDropdownItem, GlIcon } from '@gitlab/ui';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
-import { dashboardGitResponse, selfMonitoringDashboardGitResponse } from '../mock_data';
+import { dashboardGitResponse } from '../mock_data';
const defaultBranch = 'master';
-const modalId = 'duplicateDashboardModalId';
const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred);
const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred);
@@ -17,20 +16,16 @@ describe('DashboardsDropdown', () => {
function createComponent(props, opts = {}) {
const storeOpts = {
- methods: {
- duplicateSystemDashboard: jest.fn(),
- },
computed: {
allDashboards: () => mockDashboards,
selectedDashboard: () => mockSelectedDashboard,
},
};
- return shallowMount(DashboardsDropdown, {
+ wrapper = shallowMount(DashboardsDropdown, {
propsData: {
...props,
defaultBranch,
- modalId,
},
sync: false,
...storeOpts,
@@ -38,8 +33,8 @@ describe('DashboardsDropdown', () => {
});
}
- const findItems = () => wrapper.findAll(GlDropdownItem);
- const findItemAt = i => wrapper.findAll(GlDropdownItem).at(i);
+ const findItems = () => wrapper.findAll(GlNewDropdownItem);
+ const findItemAt = i => wrapper.findAll(GlNewDropdownItem).at(i);
const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' });
const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' });
const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' });
@@ -52,7 +47,7 @@ describe('DashboardsDropdown', () => {
describe('when it receives dashboards data', () => {
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
});
it('displays an item for each dashboard', () => {
@@ -78,7 +73,7 @@ describe('DashboardsDropdown', () => {
});
it('filters dropdown items when searched for item exists in the list', () => {
- const searchTerm = 'Default';
+ const searchTerm = 'Overview';
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick().then(() => {
@@ -96,10 +91,22 @@ describe('DashboardsDropdown', () => {
});
});
+ describe('when a dashboard is selected', () => {
+ beforeEach(() => {
+ [mockSelectedDashboard] = starredDashboards;
+ createComponent();
+ });
+
+ it('dashboard item is selected', () => {
+ expect(findItemAt(0).props('isChecked')).toBe(true);
+ expect(findItemAt(1).props('isChecked')).toBe(false);
+ });
+ });
+
describe('when the dashboard is missing a display name', () => {
beforeEach(() => {
mockDashboards = dashboardGitResponse.map(d => ({ ...d, display_name: undefined }));
- wrapper = createComponent();
+ createComponent();
});
it('displays items with the dashboard path, with starred dashboards first', () => {
@@ -112,7 +119,7 @@ describe('DashboardsDropdown', () => {
describe('when it receives starred dashboards', () => {
beforeEach(() => {
mockDashboards = starredDashboards;
- wrapper = createComponent();
+ createComponent();
});
it('displays an item for each dashboard', () => {
@@ -133,7 +140,7 @@ describe('DashboardsDropdown', () => {
describe('when it receives only not-starred dashboards', () => {
beforeEach(() => {
mockDashboards = notStarredDashboards;
- wrapper = createComponent();
+ createComponent();
});
it('displays an item for each dashboard', () => {
@@ -150,90 +157,9 @@ describe('DashboardsDropdown', () => {
});
});
- const duplicableCases = [
- dashboardGitResponse[0],
- dashboardGitResponse[2],
- selfMonitoringDashboardGitResponse[0],
- ];
-
- describe.each(duplicableCases)('when the selected dashboard can be duplicated', dashboard => {
- let duplicateDashboardAction;
- let modalDirective;
-
- beforeEach(() => {
- mockSelectedDashboard = dashboard;
- modalDirective = jest.fn();
- duplicateDashboardAction = jest.fn().mockResolvedValue();
-
- wrapper = createComponent(
- {},
- {
- directives: {
- GlModal: modalDirective,
- },
- methods: {
- // Mock vuex actions
- duplicateSystemDashboard: duplicateDashboardAction,
- },
- },
- );
- });
-
- it('displays a dropdown item for each dashboard', () => {
- expect(findItems().length).toEqual(dashboardGitResponse.length + 1);
- });
-
- it('displays one "duplicate dashboard" dropdown item with a directive attached', () => {
- const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]');
-
- expect(item.length).toBe(1);
- });
-
- it('"duplicate dashboard" dropdown item directive works', () => {
- const item = wrapper.find('[data-testid="duplicateDashboardItem"]');
-
- item.trigger('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(modalDirective).toHaveBeenCalled();
- });
- });
-
- it('id is correct, as the value of modal directive binding matches modal id', () => {
- expect(modalDirective).toHaveBeenCalledTimes(1);
-
- // Binding's second argument contains the modal id
- expect(modalDirective.mock.calls[0][1]).toEqual(
- expect.objectContaining({
- value: modalId,
- }),
- );
- });
- });
-
- const nonDuplicableCases = [dashboardGitResponse[1], selfMonitoringDashboardGitResponse[1]];
-
- describe.each(nonDuplicableCases)(
- 'when the selected dashboard can not be duplicated',
- dashboard => {
- beforeEach(() => {
- mockSelectedDashboard = dashboard;
-
- wrapper = createComponent();
- });
-
- it('displays a dropdown list item for each dashboard, but no list item for "duplicate dashboard"', () => {
- const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]');
-
- expect(findItems()).toHaveLength(dashboardGitResponse.length);
- expect(item.length).toBe(0);
- });
- },
- );
-
describe('when a dashboard gets selected by the user', () => {
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
findItemAt(1).vm.$emit('click');
});
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
index 4e7fee81d66..74f265930b1 100644
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
@@ -1,10 +1,10 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { TEST_HOST } from 'helpers/test_constants';
+import { setHTMLFixture } from 'helpers/fixtures';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
import { groups, initialState, metricsData, metricsWithData } from './mock_data';
-import { setHTMLFixture } from 'helpers/fixtures';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index 81f5d90c310..86e2523f708 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import GraphGroup from '~/monitoring/components/graph_group.vue';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import GraphGroup from '~/monitoring/components/graph_group.vue';
describe('Graph group component', () => {
let wrapper;
diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js
index e8ef8192067..90bd6f67196 100644
--- a/spec/frontend/monitoring/components/group_empty_state_spec.js
+++ b/spec/frontend/monitoring/components/group_empty_state_spec.js
@@ -24,7 +24,7 @@ describe('GroupEmptyState', () => {
'FOO STATE', // does not fail with unknown states
];
- test.each(supportedStates)('Renders an empty state for %s', selectedState => {
+ it.each(supportedStates)('Renders an empty state for %s', selectedState => {
const wrapper = createComponent({ selectedState });
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index 29615638453..a9b8295f38e 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { createStore } from '~/monitoring/stores';
+import Visibility from 'visibilityjs';
import { GlNewDropdown, GlNewDropdownItem, GlButton } from '@gitlab/ui';
-
+import { createStore } from '~/monitoring/stores';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
describe('RefreshButton', () => {
@@ -10,8 +10,8 @@ describe('RefreshButton', () => {
let dispatch;
let documentHidden;
- const createWrapper = () => {
- wrapper = shallowMount(RefreshButton, { store });
+ const createWrapper = (options = {}) => {
+ wrapper = shallowMount(RefreshButton, { store, ...options });
};
const findRefreshBtn = () => wrapper.find(GlButton);
@@ -31,14 +31,8 @@ describe('RefreshButton', () => {
jest.spyOn(store, 'dispatch').mockResolvedValue();
dispatch = store.dispatch;
- // Document can be mock hidden by overriding the `hidden` property
documentHidden = false;
- Object.defineProperty(document, 'hidden', {
- configurable: true,
- get() {
- return documentHidden;
- },
- });
+ jest.spyOn(Visibility, 'hidden').mockImplementation(() => documentHidden);
createWrapper();
});
@@ -57,6 +51,20 @@ describe('RefreshButton', () => {
expect(findDropdown().props('text')).toBe('Off');
});
+ describe('when feature flag disable_metric_dashboard_refresh_rate is on', () => {
+ beforeEach(() => {
+ createWrapper({
+ provide: {
+ glFeatures: { disableMetricDashboardRefreshRate: true },
+ },
+ });
+ });
+
+ it('refresh rate is not available', () => {
+ expect(findDropdown().exists()).toBe(false);
+ });
+ });
+
describe('refresh rate options', () => {
it('presents multiple options', () => {
expect(findOptions().length).toBeGreaterThan(1);
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index cc384aef231..788f3abf617 100644
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
describe('Custom variable component', () => {
@@ -23,8 +23,8 @@ describe('Custom variable component', () => {
});
};
- const findDropdown = () => wrapper.find(GlDropdown);
- const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findDropdown = () => wrapper.find(GlDeprecatedDropdown);
+ const findDropdownItems = () => wrapper.findAll(GlDeprecatedDropdownItem);
it('renders dropdown element when all necessary props are passed', () => {
createShallowWrapper();
diff --git a/spec/frontend/monitoring/csv_export_spec.js b/spec/frontend/monitoring/csv_export_spec.js
new file mode 100644
index 00000000000..eb2a6e40243
--- /dev/null
+++ b/spec/frontend/monitoring/csv_export_spec.js
@@ -0,0 +1,126 @@
+import { timeSeriesGraphData } from './graph_data';
+import { graphDataToCsv } from '~/monitoring/csv_export';
+
+describe('monitoring export_csv', () => {
+ describe('graphDataToCsv', () => {
+ const expectCsvToMatchLines = (csv, lines) => expect(`${lines.join('\r\n')}\r\n`).toEqual(csv);
+
+ it('should return a csv with 0 metrics', () => {
+ const data = timeSeriesGraphData({}, { metricCount: 0 });
+
+ expect(graphDataToCsv(data)).toEqual('');
+ });
+
+ it('should return a csv with 1 metric with no data', () => {
+ const data = timeSeriesGraphData({}, { metricCount: 1 });
+
+ // When state is NO_DATA, result is null
+ data.metrics[0].result = null;
+
+ expect(graphDataToCsv(data)).toEqual('');
+ });
+
+ it('should return a csv with 1 metric', () => {
+ const data = timeSeriesGraphData({}, { metricCount: 1 });
+
+ expectCsvToMatchLines(graphDataToCsv(data), [
+ `timestamp,"Y Axis > Metric 1"`,
+ '2015-07-01T20:10:50.000Z,1',
+ '2015-07-01T20:12:50.000Z,2',
+ '2015-07-01T20:14:50.000Z,3',
+ ]);
+ });
+
+ it('should return a csv with multiple metrics and one with no data', () => {
+ const data = timeSeriesGraphData({}, { metricCount: 2 });
+
+ // When state is NO_DATA, result is null
+ data.metrics[0].result = null;
+
+ expectCsvToMatchLines(graphDataToCsv(data), [
+ `timestamp,"Y Axis > Metric 2"`,
+ '2015-07-01T20:10:50.000Z,1',
+ '2015-07-01T20:12:50.000Z,2',
+ '2015-07-01T20:14:50.000Z,3',
+ ]);
+ });
+
+ it('should return a csv when not all metrics have the same timestamps', () => {
+ const data = timeSeriesGraphData({}, { metricCount: 3 });
+
+ // Add an "odd" timestamp that is not in the dataset
+ Object.assign(data.metrics[2].result[0], {
+ value: ['2016-01-01T00:00:00.000Z', 9],
+ values: [['2016-01-01T00:00:00.000Z', 9]],
+ });
+
+ expectCsvToMatchLines(graphDataToCsv(data), [
+ `timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
+ '2015-07-01T20:10:50.000Z,1,1,',
+ '2015-07-01T20:12:50.000Z,2,2,',
+ '2015-07-01T20:14:50.000Z,3,3,',
+ '2016-01-01T00:00:00.000Z,,,9',
+ ]);
+ });
+
+ it('should escape double quotes in metric labels with two double quotes ("")', () => {
+ const data = timeSeriesGraphData({}, { metricCount: 1 });
+
+ data.metrics[0].label = 'My "quoted" metric';
+
+ expectCsvToMatchLines(graphDataToCsv(data), [
+ `timestamp,"Y Axis > My ""quoted"" metric"`,
+ '2015-07-01T20:10:50.000Z,1',
+ '2015-07-01T20:12:50.000Z,2',
+ '2015-07-01T20:14:50.000Z,3',
+ ]);
+ });
+
+ it('should return a csv with multiple metrics', () => {
+ const data = timeSeriesGraphData({}, { metricCount: 3 });
+
+ expectCsvToMatchLines(graphDataToCsv(data), [
+ `timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
+ '2015-07-01T20:10:50.000Z,1,1,1',
+ '2015-07-01T20:12:50.000Z,2,2,2',
+ '2015-07-01T20:14:50.000Z,3,3,3',
+ ]);
+ });
+
+ it('should return a csv with 1 metric and multiple series with labels', () => {
+ const data = timeSeriesGraphData({}, { isMultiSeries: true });
+
+ expectCsvToMatchLines(graphDataToCsv(data), [
+ `timestamp,"Y Axis > Metric 1","Y Axis > Metric 1"`,
+ '2015-07-01T20:10:50.000Z,1,4',
+ '2015-07-01T20:12:50.000Z,2,5',
+ '2015-07-01T20:14:50.000Z,3,6',
+ ]);
+ });
+
+ it('should return a csv with 1 metric and multiple series', () => {
+ const data = timeSeriesGraphData({}, { isMultiSeries: true, withLabels: false });
+
+ expectCsvToMatchLines(graphDataToCsv(data), [
+ `timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
+ '2015-07-01T20:10:50.000Z,1,4',
+ '2015-07-01T20:12:50.000Z,2,5',
+ '2015-07-01T20:14:50.000Z,3,6',
+ ]);
+ });
+
+ it('should return a csv with multiple metrics and multiple series', () => {
+ const data = timeSeriesGraphData(
+ {},
+ { metricCount: 3, isMultiSeries: true, withLabels: false },
+ );
+
+ expectCsvToMatchLines(graphDataToCsv(data), [
+ `timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
+ '2015-07-01T20:10:50.000Z,1,4,1,4,1,4',
+ '2015-07-01T20:12:50.000Z,2,5,2,5,2,5',
+ '2015-07-01T20:14:50.000Z,3,6,3,6,3,6',
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js
index 97edf7bda74..30040d3f89f 100644
--- a/spec/frontend/monitoring/fixture_data.js
+++ b/spec/frontend/monitoring/fixture_data.js
@@ -29,36 +29,12 @@ const datasetState = stateAndPropsFromDataset(
// https://gitlab.com/gitlab-org/gitlab/-/issues/229256
export const dashboardProps = {
...datasetState.dataProps,
- addDashboardDocumentationPath: 'https://path/to/docs',
alertsEndpoint: null,
};
export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload);
export const metricsDashboardPanelCount = 22;
-export const metricResultStatus = {
- // First metric in fixture `metrics_dashboard/environment_metrics_dashboard.json`
- metricId: 'NO_DB_response_metrics_nginx_ingress_throughput_status_code',
- data: {
- resultType: 'matrix',
- result: metricsResult,
- },
-};
-export const metricResultPods = {
- // Second metric in fixture `metrics_dashboard/environment_metrics_dashboard.json`
- metricId: 'NO_DB_response_metrics_nginx_ingress_latency_pod_average',
- data: {
- resultType: 'matrix',
- result: metricsResult,
- },
-};
-export const metricResultEmpty = {
- metricId: 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code',
- data: {
- resultType: 'matrix',
- result: [],
- },
-};
// Graph data
diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js
index e1b95723f3d..f85351e55d7 100644
--- a/spec/frontend/monitoring/graph_data.js
+++ b/spec/frontend/monitoring/graph_data.js
@@ -1,10 +1,38 @@
import { mapPanelToViewModel, normalizeQueryResponseData } from '~/monitoring/stores/utils';
import { panelTypes, metricStates } from '~/monitoring/constants';
-const initTime = 1435781451.781;
+const initTime = 1435781450; // "Wed, 01 Jul 2015 20:10:50 GMT"
+const intervalSeconds = 120;
const makeValue = val => [initTime, val];
-const makeValues = vals => vals.map((val, i) => [initTime + 15 * i, val]);
+const makeValues = vals => vals.map((val, i) => [initTime + intervalSeconds * i, val]);
+
+// Raw Promethues Responses
+
+export const prometheusMatrixMultiResult = ({
+ values1 = ['1', '2', '3'],
+ values2 = ['4', '5', '6'],
+} = {}) => ({
+ resultType: 'matrix',
+ result: [
+ {
+ metric: {
+ __name__: 'up',
+ job: 'prometheus',
+ instance: 'localhost:9090',
+ },
+ values: makeValues(values1),
+ },
+ {
+ metric: {
+ __name__: 'up',
+ job: 'node',
+ instance: 'localhost:9091',
+ },
+ values: makeValues(values2),
+ },
+ ],
+});
// Normalized Prometheus Responses
@@ -82,7 +110,7 @@ const matrixMultiResult = ({ values1 = ['1', '2', '3'], values2 = ['4', '5', '6'
* @param {Object} dataOptions.isMultiSeries
*/
export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { metricCount = 1, isMultiSeries = false } = dataOptions;
+ const { metricCount = 1, isMultiSeries = false, withLabels = true } = dataOptions;
return mapPanelToViewModel({
title: 'Time Series Panel',
@@ -90,7 +118,7 @@ export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
x_label: 'X Axis',
y_label: 'Y Axis',
metrics: Array.from(Array(metricCount), (_, i) => ({
- label: `Metric ${i + 1}`,
+ label: withLabels ? `Metric ${i + 1}` : undefined,
state: metricStates.OK,
result: isMultiSeries ? matrixMultiResult() : matrixSingleResult(),
})),
@@ -162,3 +190,59 @@ export const anomalyGraphData = (panelOptions = {}, dataOptions = {}) => {
...panelOptions,
});
};
+
+/**
+ * Generate mock graph data for heatmaps according to options
+ */
+export const heatmapGraphData = (panelOptions = {}, dataOptions = {}) => {
+ const { metricCount = 1 } = dataOptions;
+
+ return mapPanelToViewModel({
+ title: 'Heatmap Panel',
+ type: panelTypes.HEATMAP,
+ x_label: 'X Axis',
+ y_label: 'Y Axis',
+ metrics: Array.from(Array(metricCount), (_, i) => ({
+ label: `Metric ${i + 1}`,
+ state: metricStates.OK,
+ result: matrixMultiResult(),
+ })),
+ ...panelOptions,
+ });
+};
+
+/**
+ * Generate gauge chart mock graph data according to options
+ *
+ * @param {Object} panelOptions - Panel options as in YML.
+ *
+ */
+export const gaugeChartGraphData = (panelOptions = {}) => {
+ const {
+ minValue = 100,
+ maxValue = 1000,
+ split = 20,
+ thresholds = {
+ mode: 'absolute',
+ values: [500, 800],
+ },
+ format = 'kilobytes',
+ } = panelOptions;
+
+ return mapPanelToViewModel({
+ title: 'Gauge Chart Panel',
+ type: panelTypes.GAUGE_CHART,
+ min_value: minValue,
+ max_value: maxValue,
+ split,
+ thresholds,
+ format,
+ metrics: [
+ {
+ label: `Metric`,
+ state: metricStates.OK,
+ result: matrixSingleResult(),
+ },
+ ],
+ });
+};
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 49ad33402c6..28a7dd1af4f 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -1,3 +1,4 @@
+import invalidUrl from '~/lib/utils/invalid_url';
// This import path needs to be relative for now because this mock data is used in
// Karma specs too, where the helpers/test_constants alias can not be resolved
import { TEST_HOST } from '../helpers/test_constants';
@@ -170,7 +171,7 @@ export const environmentData = [
export const dashboardGitResponse = [
{
default: true,
- display_name: 'Default',
+ display_name: 'Overview',
can_edit: false,
system_dashboard: true,
out_of_the_box_dashboard: true,
@@ -209,7 +210,7 @@ export const selfMonitoringDashboardGitResponse = [
default: true,
display_name: 'Default',
can_edit: false,
- system_dashboard: false,
+ system_dashboard: true,
out_of_the_box_dashboard: true,
project_blob_path: null,
path: 'config/prometheus/self_monitoring_default.yml',
@@ -244,83 +245,6 @@ export const metricsResult = [
},
];
-export const graphDataPrometheusQueryRangeMultiTrack = {
- title: 'Super Chart A3',
- type: 'heatmap',
- weight: 3,
- x_label: 'Status Code',
- y_label: 'Time',
- metrics: [
- {
- metricId: '1_metric_b',
- id: 'response_metrics_nginx_ingress_throughput_status_code',
- query_range:
- 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)',
- unit: 'req / sec',
- label: 'Status Code',
- prometheus_endpoint_path:
- '/root/rails_nodb/environments/3/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
- result: [
- {
- metric: { status_code: '1xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 0],
- ['2019-08-30T16:00:00.000Z', 2],
- ['2019-08-30T17:00:00.000Z', 0],
- ['2019-08-30T18:00:00.000Z', 0],
- ['2019-08-30T19:00:00.000Z', 0],
- ['2019-08-30T20:00:00.000Z', 3],
- ],
- },
- {
- metric: { status_code: '2xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 1],
- ['2019-08-30T16:00:00.000Z', 3],
- ['2019-08-30T17:00:00.000Z', 6],
- ['2019-08-30T18:00:00.000Z', 10],
- ['2019-08-30T19:00:00.000Z', 8],
- ['2019-08-30T20:00:00.000Z', 6],
- ],
- },
- {
- metric: { status_code: '3xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 1],
- ['2019-08-30T16:00:00.000Z', 2],
- ['2019-08-30T17:00:00.000Z', 3],
- ['2019-08-30T18:00:00.000Z', 3],
- ['2019-08-30T19:00:00.000Z', 2],
- ['2019-08-30T20:00:00.000Z', 1],
- ],
- },
- {
- metric: { status_code: '4xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 2],
- ['2019-08-30T16:00:00.000Z', 0],
- ['2019-08-30T17:00:00.000Z', 0],
- ['2019-08-30T18:00:00.000Z', 2],
- ['2019-08-30T19:00:00.000Z', 0],
- ['2019-08-30T20:00:00.000Z', 2],
- ],
- },
- {
- metric: { status_code: '5xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 0],
- ['2019-08-30T16:00:00.000Z', 1],
- ['2019-08-30T17:00:00.000Z', 0],
- ['2019-08-30T18:00:00.000Z', 0],
- ['2019-08-30T19:00:00.000Z', 0],
- ['2019-08-30T20:00:00.000Z', 2],
- ],
- },
- ],
- },
- ],
-};
-
export const stackedColumnMockedData = {
title: 'memories',
type: 'stacked-column',
@@ -420,6 +344,11 @@ export const mockNamespaces = [`${baseNamespace}/1`, `${baseNamespace}/2`];
export const mockTimeRange = { duration: { seconds: 120 } };
+export const mockFixedTimeRange = {
+ start: '2020-06-17T19:59:08.659Z',
+ end: '2020-07-17T19:59:08.659Z',
+};
+
export const mockNamespacedData = {
mockDeploymentData: ['mockDeploymentData'],
mockProjectPath: '/mockProjectPath',
@@ -688,10 +617,28 @@ export const storeVariables = [
export const dashboardHeaderProps = {
defaultBranch: 'master',
- addDashboardDocumentationPath: 'https://path/to/docs',
isRearrangingPanels: false,
selectedTimeRange: {
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-01T01:00:00.000Z',
},
};
+
+export const dashboardActionsMenuProps = {
+ defaultBranch: 'master',
+ addingMetricsAvailable: true,
+ customMetricsPath: 'https://path/to/customMetrics',
+ validateQueryPath: 'https://path/to/validateQuery',
+ isOotbDashboard: true,
+};
+
+export const mockAlert = {
+ alert_path: 'alert_path',
+ id: 8,
+ metricId: 'mock_metric_id',
+ operator: '>',
+ query: 'testQuery',
+ runbookUrl: invalidUrl,
+ threshold: 5,
+ title: 'alert title',
+};
diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js
new file mode 100644
index 00000000000..83365b754d9
--- /dev/null
+++ b/spec/frontend/monitoring/pages/panel_new_page_spec.js
@@ -0,0 +1,98 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
+import { createStore } from '~/monitoring/stores';
+import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
+
+import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
+
+const dashboard = 'dashboard.yml';
+
+// Button stub that can accept `to` as router links do
+// https://bootstrap-vue.org/docs/components/button#comp-ref-b-button-props
+const GlButtonStub = {
+ extends: GlButton,
+ props: {
+ to: [String, Object],
+ },
+};
+
+describe('monitoring/pages/panel_new_page', () => {
+ let store;
+ let wrapper;
+ let $route;
+ let $router;
+
+ const mountComponent = (propsData = {}, route) => {
+ $route = route ?? { name: PANEL_NEW_PAGE, params: { dashboard } };
+ $router = {
+ push: jest.fn(),
+ };
+
+ wrapper = shallowMount(PanelNewPage, {
+ propsData,
+ store,
+ stubs: {
+ GlButton: GlButtonStub,
+ },
+ mocks: {
+ $router,
+ $route,
+ },
+ });
+ };
+
+ const findBackButton = () => wrapper.find(GlButtonStub);
+ const findPanelBuilder = () => wrapper.find(DashboardPanelBuilder);
+
+ beforeEach(() => {
+ store = createStore();
+ mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('back to dashboard button', () => {
+ it('is rendered', () => {
+ expect(findBackButton().exists()).toBe(true);
+ expect(findBackButton().props('icon')).toBe('go-back');
+ });
+
+ it('links back to the dashboard', () => {
+ expect(findBackButton().props('to')).toEqual({
+ name: DASHBOARD_PAGE,
+ params: { dashboard },
+ });
+ });
+
+ it('links back to the dashboard while preserving query params', () => {
+ $route = {
+ name: PANEL_NEW_PAGE,
+ params: { dashboard },
+ query: { another: 'param' },
+ };
+
+ mountComponent({}, $route);
+
+ expect(findBackButton().props('to')).toEqual({
+ name: DASHBOARD_PAGE,
+ params: { dashboard },
+ query: { another: 'param' },
+ });
+ });
+ });
+
+ describe('dashboard panel builder', () => {
+ it('is rendered', () => {
+ expect(findPanelBuilder().exists()).toBe(true);
+ });
+ });
+
+ describe('page routing', () => {
+ it('route is not updated by default', () => {
+ expect($router.push).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
new file mode 100644
index 00000000000..a91c209875a
--- /dev/null
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -0,0 +1,149 @@
+import MockAdapter from 'axios-mock-adapter';
+import { backoffMockImplementation } from 'jest/helpers/backoff_helper';
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import * as commonUtils from '~/lib/utils/common_utils';
+import { metricsDashboardResponse } from '../fixture_data';
+import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
+
+describe('monitoring metrics_requests', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
+ });
+
+ afterEach(() => {
+ mock.reset();
+
+ commonUtils.backOff.mockReset();
+ });
+
+ describe('getDashboard', () => {
+ const response = metricsDashboardResponse;
+ const dashboardEndpoint = '/dashboard';
+ const params = {
+ start_time: 'start_time',
+ end_time: 'end_time',
+ };
+
+ it('returns a dashboard response', () => {
+ mock.onGet(dashboardEndpoint).reply(statusCodes.OK, response);
+
+ return getDashboard(dashboardEndpoint, params).then(data => {
+ expect(data).toEqual(metricsDashboardResponse);
+ });
+ });
+
+ it('returns a dashboard response after retrying twice', () => {
+ mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(dashboardEndpoint).reply(statusCodes.OK, response);
+
+ return getDashboard(dashboardEndpoint, params).then(data => {
+ expect(data).toEqual(metricsDashboardResponse);
+ expect(mock.history.get).toHaveLength(3);
+ });
+ });
+
+ it('rejects after getting an error', () => {
+ mock.onGet(dashboardEndpoint).reply(500);
+
+ return getDashboard(dashboardEndpoint, params).catch(error => {
+ expect(error).toEqual(expect.any(Error));
+ expect(mock.history.get).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('getPrometheusQueryData', () => {
+ const response = {
+ status: 'success',
+ data: {
+ resultType: 'matrix',
+ result: [],
+ },
+ };
+ const prometheusEndpoint = '/query_range';
+ const params = {
+ start_time: 'start_time',
+ end_time: 'end_time',
+ };
+
+ it('returns a dashboard response', () => {
+ mock.onGet(prometheusEndpoint).reply(statusCodes.OK, response);
+
+ return getPrometheusQueryData(prometheusEndpoint, params).then(data => {
+ expect(data).toEqual(response.data);
+ });
+ });
+
+ it('returns a dashboard response after retrying twice', () => {
+ // Mock multiple attempts while the cache is filling up
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).reply(statusCodes.OK, response); // 3rd attempt
+
+ return getPrometheusQueryData(prometheusEndpoint, params).then(data => {
+ expect(data).toEqual(response.data);
+ expect(mock.history.get).toHaveLength(3);
+ });
+ });
+
+ it('rejects after getting an HTTP 500 error', () => {
+ mock.onGet(prometheusEndpoint).reply(500, {
+ status: 'error',
+ error: 'An error ocurred',
+ });
+
+ return getPrometheusQueryData(prometheusEndpoint, params).catch(error => {
+ expect(error).toEqual(new Error('Request failed with status code 500'));
+ });
+ });
+
+ it('rejects after retrying twice and getting an HTTP 401 error', () => {
+ // Mock multiple attempts while the cache is filling up and fails
+ mock.onGet(prometheusEndpoint).reply(statusCodes.UNAUTHORIZED, {
+ status: 'error',
+ error: 'An error ocurred',
+ });
+
+ return getPrometheusQueryData(prometheusEndpoint, params).catch(error => {
+ expect(error).toEqual(new Error('Request failed with status code 401'));
+ });
+ });
+
+ it('rejects after retrying twice and getting an HTTP 500 error', () => {
+ // Mock multiple attempts while the cache is filling up and fails
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).reply(500, {
+ status: 'error',
+ error: 'An error ocurred',
+ }); // 3rd attempt
+
+ return getPrometheusQueryData(prometheusEndpoint, params).catch(error => {
+ expect(error).toEqual(new Error('Request failed with status code 500'));
+ expect(mock.history.get).toHaveLength(3);
+ });
+ });
+
+ test.each`
+ code | reason
+ ${statusCodes.BAD_REQUEST} | ${'Parameters are missing or incorrect'}
+ ${statusCodes.UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
+ ${statusCodes.SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
+ `('rejects with details: "$reason" after getting an HTTP $code error', ({ code, reason }) => {
+ mock.onGet(prometheusEndpoint).reply(code, {
+ status: 'error',
+ error: reason,
+ });
+
+ return getPrometheusQueryData(prometheusEndpoint, params).catch(error => {
+ expect(error).toEqual(new Error(reason));
+ expect(mock.history.get).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
index 5b8f4b3c83e..8b97c8ed125 100644
--- a/spec/frontend/monitoring/router_spec.js
+++ b/spec/frontend/monitoring/router_spec.js
@@ -1,18 +1,28 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueRouter from 'vue-router';
import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
+import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import createRouter from '~/monitoring/router';
import { dashboardProps } from './fixture_data';
import { dashboardHeaderProps } from './mock_data';
+const LEGACY_BASE_PATH = '/project/my-group/test-project/-/environments/71146/metrics';
+const BASE_PATH = '/project/my-group/test-project/-/metrics';
+
+const MockApp = {
+ data() {
+ return {
+ dashboardProps: { ...dashboardProps, ...dashboardHeaderProps },
+ };
+ },
+ template: `<router-view :dashboard-props="dashboardProps"/>`,
+};
+
describe('Monitoring router', () => {
let router;
let store;
- const propsData = { dashboardProps: { ...dashboardProps, ...dashboardHeaderProps } };
- const NEW_BASE_PATH = '/project/my-group/test-project/-/metrics';
- const OLD_BASE_PATH = '/project/my-group/test-project/-/environments/71146/metrics';
const createWrapper = (basePath, routeArg) => {
const localVue = createLocalVue();
@@ -23,11 +33,10 @@ describe('Monitoring router', () => {
router.push(routeArg);
}
- return mount(DashboardPage, {
+ return mount(MockApp, {
localVue,
store,
router,
- propsData,
});
};
@@ -40,26 +49,32 @@ describe('Monitoring router', () => {
window.location.hash = '';
});
- describe('support old URL with full dashboard path', () => {
+ describe('support legacy URLs with full dashboard path to visit dashboard page', () => {
it.each`
- route | currentDashboard
+ path | currentDashboard
${'/dashboard.yml'} | ${'dashboard.yml'}
${'/folder1/dashboard.yml'} | ${'folder1/dashboard.yml'}
${'/?dashboard=dashboard.yml'} | ${'dashboard.yml'}
- `('sets component as $componentName for path "$route"', ({ route, currentDashboard }) => {
- const wrapper = createWrapper(OLD_BASE_PATH, route);
+ `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
+ const wrapper = createWrapper(LEGACY_BASE_PATH, path);
expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
currentDashboard,
});
- expect(wrapper.find(Dashboard)).toExist();
+ expect(wrapper.find(DashboardPage).exists()).toBe(true);
+ expect(
+ wrapper
+ .find(DashboardPage)
+ .find(Dashboard)
+ .exists(),
+ ).toBe(true);
});
});
- describe('supports new URL with short dashboard path', () => {
+ describe('supports URLs to visit dashboard page', () => {
it.each`
- route | currentDashboard
+ path | currentDashboard
${'/'} | ${null}
${'/dashboard.yml'} | ${'dashboard.yml'}
${'/folder1/dashboard.yml'} | ${'folder1/dashboard.yml'}
@@ -68,14 +83,35 @@ describe('Monitoring router', () => {
${'/config/prometheus/common_metrics.yml'} | ${'config/prometheus/common_metrics.yml'}
${'/config/prometheus/pod_metrics.yml'} | ${'config/prometheus/pod_metrics.yml'}
${'/config%2Fprometheus%2Fpod_metrics.yml'} | ${'config/prometheus/pod_metrics.yml'}
- `('sets component as $componentName for path "$route"', ({ route, currentDashboard }) => {
- const wrapper = createWrapper(NEW_BASE_PATH, route);
+ `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
+ const wrapper = createWrapper(BASE_PATH, path);
expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
currentDashboard,
});
- expect(wrapper.find(Dashboard)).toExist();
+ expect(wrapper.find(DashboardPage).exists()).toBe(true);
+ expect(
+ wrapper
+ .find(DashboardPage)
+ .find(Dashboard)
+ .exists(),
+ ).toBe(true);
+ });
+ });
+
+ describe('supports URLs to visit new panel page', () => {
+ it.each`
+ path | currentDashboard
+ ${'/panel/new'} | ${undefined}
+ ${'/dashboard.yml/panel/new'} | ${'dashboard.yml'}
+ ${'/config/prometheus/common_metrics.yml/panel/new'} | ${'config/prometheus/common_metrics.yml'}
+ ${'/config%2Fprometheus%2Fcommon_metrics.yml/panel/new'} | ${'config/prometheus/common_metrics.yml'}
+ `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
+ const wrapper = createWrapper(BASE_PATH, path);
+
+ expect(wrapper.vm.$route.params.dashboard).toBe(currentDashboard);
+ expect(wrapper.find(PanelNewPage).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 22f2b2e3c77..5c7ab4e6a1f 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -1,10 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import { backoffMockImplementation } from 'jest/helpers/backoff_helper';
import Tracking from '~/tracking';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import * as commonUtils from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { defaultTimeRange } from '~/vue_shared/constants';
import * as getters from '~/monitoring/stores/getters';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
@@ -30,6 +31,7 @@ import {
duplicateSystemDashboard,
updateVariablesAndFetchData,
fetchVariableMetricLabelValues,
+ fetchPanelPreview,
} from '~/monitoring/stores/actions';
import {
gqClient,
@@ -73,19 +75,7 @@ describe('Monitoring store actions', () => {
commit = jest.fn();
dispatch = jest.fn();
- jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
- const q = new Promise((resolve, reject) => {
- const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
- const next = () => callback(next, stop);
- // Define a timeout based on a mock timer
- setTimeout(() => {
- callback(next, stop);
- });
- });
- // Run all resolved promises in chain
- jest.runOnlyPendingTimers();
- return q;
- });
+ jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
});
afterEach(() => {
@@ -483,7 +473,6 @@ describe('Monitoring store actions', () => {
],
[],
() => {
- expect(mock.history.get).toHaveLength(1);
done();
},
).catch(done.fail);
@@ -569,46 +558,8 @@ describe('Monitoring store actions', () => {
});
});
- it('commits result, when waiting for results', done => {
- // Mock multiple attempts while the cache is filling up
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).reply(200, { data }); // 4th attempt
-
- testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_SUCCESS,
- payload: {
- metricId: metric.metricId,
- data,
- },
- },
- ],
- [],
- () => {
- expect(mock.history.get).toHaveLength(4);
- done();
- },
- ).catch(done.fail);
- });
-
it('commits failure, when waiting for results and getting a server error', done => {
- // Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).reply(500); // 4th attempt
+ mock.onGet(prometheusEndpointPath).reply(500);
const error = new Error('Request failed with status code 500');
@@ -633,7 +584,6 @@ describe('Monitoring store actions', () => {
],
[],
).catch(e => {
- expect(mock.history.get).toHaveLength(4);
expect(e).toEqual(error);
done();
});
@@ -1205,4 +1155,69 @@ describe('Monitoring store actions', () => {
);
});
});
+
+ describe('fetchPanelPreview', () => {
+ const panelPreviewEndpoint = '/builder.json';
+ const mockYmlContent = 'mock yml content';
+
+ beforeEach(() => {
+ state.panelPreviewEndpoint = panelPreviewEndpoint;
+ });
+
+ it('should not commit or dispatch if payload is empty', () => {
+ testAction(fetchPanelPreview, '', state, [], []);
+ });
+
+ it('should store the panel and fetch metric results', () => {
+ const mockPanel = {
+ title: 'Go heap size',
+ type: 'area-chart',
+ };
+
+ mock
+ .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
+ .reply(statusCodes.OK, mockPanel);
+
+ testAction(
+ fetchPanelPreview,
+ mockYmlContent,
+ state,
+ [
+ { type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
+ { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
+ { type: types.RECEIVE_PANEL_PREVIEW_SUCCESS, payload: mockPanel },
+ ],
+ [{ type: 'fetchPanelPreviewMetrics' }],
+ );
+ });
+
+ it('should display a validation error when the backend cannot process the yml', () => {
+ const mockErrorMsg = 'Each "metric" must define one of :query or :query_range';
+
+ mock
+ .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
+ .reply(statusCodes.UNPROCESSABLE_ENTITY, {
+ message: mockErrorMsg,
+ });
+
+ testAction(fetchPanelPreview, mockYmlContent, state, [
+ { type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
+ { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
+ { type: types.RECEIVE_PANEL_PREVIEW_FAILURE, payload: mockErrorMsg },
+ ]);
+ });
+
+ it('should display a generic error when the backend fails', () => {
+ mock.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent }).reply(500);
+
+ testAction(fetchPanelPreview, mockYmlContent, state, [
+ { type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
+ { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
+ {
+ type: types.RECEIVE_PANEL_PREVIEW_FAILURE,
+ payload: 'Request failed with status code 500',
+ },
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index a69f5265ea7..509de8a4596 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -11,37 +11,36 @@ import {
storeVariables,
mockLinks,
} from '../mock_data';
-import {
- metricsDashboardPayload,
- metricResultStatus,
- metricResultPods,
- metricResultEmpty,
-} from '../fixture_data';
+import { metricsDashboardPayload } from '../fixture_data';
describe('Monitoring store Getters', () => {
+ let state;
+
+ const getMetric = ({ group = 0, panel = 0, metric = 0 } = {}) =>
+ state.dashboard.panelGroups[group].panels[panel].metrics[metric];
+
+ const setMetricSuccess = ({ group, panel, metric, result = metricsResult } = {}) => {
+ const { metricId } = getMetric({ group, panel, metric });
+ mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, {
+ metricId,
+ data: {
+ resultType: 'matrix',
+ result,
+ },
+ });
+ };
+
+ const setMetricFailure = ({ group, panel, metric } = {}) => {
+ const { metricId } = getMetric({ group, panel, metric });
+ mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
+ metricId,
+ });
+ };
+
describe('getMetricStates', () => {
let setupState;
- let state;
let getMetricStates;
- const setMetricSuccess = ({ result = metricsResult, group = 0, panel = 0, metric = 0 }) => {
- const { metricId } = state.dashboard.panelGroups[group].panels[panel].metrics[metric];
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, {
- metricId,
- data: {
- resultType: 'matrix',
- result,
- },
- });
- };
-
- const setMetricFailure = ({ group = 0, panel = 0, metric = 0 }) => {
- const { metricId } = state.dashboard.panelGroups[group].panels[panel].metrics[metric];
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
- metricId,
- });
- };
-
beforeEach(() => {
setupState = (initState = {}) => {
state = initState;
@@ -81,7 +80,7 @@ describe('Monitoring store Getters', () => {
it('on an empty metric with no result, returns NO_DATA', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ result: [], group: 2 });
+ setMetricSuccess({ group: 2, result: [] });
expect(getMetricStates()).toEqual([metricStates.NO_DATA]);
});
@@ -147,7 +146,6 @@ describe('Monitoring store Getters', () => {
describe('metricsWithData', () => {
let metricsWithData;
let setupState;
- let state;
beforeEach(() => {
setupState = (initState = {}) => {
@@ -191,35 +189,39 @@ describe('Monitoring store Getters', () => {
it('an empty metric, returns empty', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultEmpty);
+ setMetricSuccess({ result: [] });
expect(metricsWithData()).toEqual([]);
});
it('a metric with results, it returns a metric', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus);
+ setMetricSuccess();
- expect(metricsWithData()).toEqual([metricResultStatus.metricId]);
+ expect(metricsWithData()).toEqual([getMetric().metricId]);
});
it('multiple metrics with results, it return multiple metrics', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultPods);
+ setMetricSuccess({ panel: 0 });
+ setMetricSuccess({ panel: 1 });
- expect(metricsWithData()).toEqual([metricResultStatus.metricId, metricResultPods.metricId]);
+ expect(metricsWithData()).toEqual([
+ getMetric({ panel: 0 }).metricId,
+ getMetric({ panel: 1 }).metricId,
+ ]);
});
it('multiple metrics with results, it returns metrics filtered by group', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultPods);
+
+ setMetricSuccess({ group: 1 });
+ setMetricSuccess({ group: 1, panel: 1 });
// First group has metrics
expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([
- metricResultStatus.metricId,
- metricResultPods.metricId,
+ getMetric({ group: 1 }).metricId,
+ getMetric({ group: 1, panel: 1 }).metricId,
]);
// Second group has no metrics
@@ -229,7 +231,6 @@ describe('Monitoring store Getters', () => {
});
describe('filteredEnvironments', () => {
- let state;
const setupState = (initState = {}) => {
state = {
...state,
@@ -284,7 +285,6 @@ describe('Monitoring store Getters', () => {
describe('metricsSavedToDb', () => {
let metricsSavedToDb;
- let state;
let mockData;
beforeEach(() => {
@@ -335,8 +335,6 @@ describe('Monitoring store Getters', () => {
});
describe('getCustomVariablesParams', () => {
- let state;
-
beforeEach(() => {
state = {
variables: {},
@@ -367,58 +365,65 @@ describe('Monitoring store Getters', () => {
describe('selectedDashboard', () => {
const { selectedDashboard } = getters;
- const localGetters = state => ({
- fullDashboardPath: getters.fullDashboardPath(state),
+ const localGetters = localState => ({
+ fullDashboardPath: getters.fullDashboardPath(localState),
});
it('returns a dashboard', () => {
- const state = {
+ const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[0].path,
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(
+ dashboardGitResponse[0],
+ );
});
- it('returns a non-default dashboard', () => {
- const state = {
+ it('returns a dashboard different from the overview dashboard', () => {
+ const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[1].path,
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[1]);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(
+ dashboardGitResponse[1],
+ );
});
- it('returns a default dashboard when no dashboard is selected', () => {
- const state = {
+ it('returns the overview dashboard when no dashboard is selected', () => {
+ const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: null,
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(
+ dashboardGitResponse[0],
+ );
});
- it('returns a default dashboard when dashboard cannot be found', () => {
- const state = {
+ it('returns the overview dashboard when dashboard cannot be found', () => {
+ const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: 'wrong_path',
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(
+ dashboardGitResponse[0],
+ );
});
it('returns null when no dashboards are present', () => {
- const state = {
+ const localState = {
allDashboards: [],
currentDashboard: dashboardGitResponse[0].path,
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(null);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(null);
});
});
describe('linksWithMetadata', () => {
- let state;
const setupState = (initState = {}) => {
state = {
...state,
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 14b38d79aa2..8d1351fc909 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -4,8 +4,8 @@ import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state';
import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
-
import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data';
+import { prometheusMatrixMultiResult } from '../graph_data';
import { metricsDashboardPayload } from '../fixture_data';
describe('Monitoring mutations', () => {
@@ -259,27 +259,6 @@ describe('Monitoring mutations', () => {
describe('Individual panel/metric results', () => {
const metricId = 'NO_DB_response_metrics_nginx_ingress_throughput_status_code';
- const data = {
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: [[1435781430.781, '1'], [1435781445.781, '1'], [1435781460.781, '1']],
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9091',
- },
- values: [[1435781430.781, '0'], [1435781445.781, '0'], [1435781460.781, '1']],
- },
- ],
- };
const dashboard = metricsDashboardPayload;
const getMetric = () => stateCopy.dashboard.panelGroups[1].panels[0].metrics[0];
@@ -307,6 +286,8 @@ describe('Monitoring mutations', () => {
});
it('adds results to the store', () => {
+ const data = prometheusMatrixMultiResult();
+
expect(getMetric().result).toBe(null);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
@@ -488,4 +469,128 @@ describe('Monitoring mutations', () => {
});
});
});
+
+ describe('REQUEST_PANEL_PREVIEW', () => {
+ it('saves yml content and resets other preview data', () => {
+ const mockYmlContent = 'mock yml content';
+ mutations[types.REQUEST_PANEL_PREVIEW](stateCopy, mockYmlContent);
+
+ expect(stateCopy.panelPreviewIsLoading).toBe(true);
+ expect(stateCopy.panelPreviewYml).toBe(mockYmlContent);
+ expect(stateCopy.panelPreviewGraphData).toBe(null);
+ expect(stateCopy.panelPreviewError).toBe(null);
+ });
+ });
+
+ describe('RECEIVE_PANEL_PREVIEW_SUCCESS', () => {
+ it('saves graph data', () => {
+ mutations[types.RECEIVE_PANEL_PREVIEW_SUCCESS](stateCopy, {
+ title: 'My Title',
+ type: 'area-chart',
+ });
+
+ expect(stateCopy.panelPreviewIsLoading).toBe(false);
+ expect(stateCopy.panelPreviewGraphData).toMatchObject({
+ title: 'My Title',
+ type: 'area-chart',
+ });
+ expect(stateCopy.panelPreviewError).toBe(null);
+ });
+ });
+
+ describe('RECEIVE_PANEL_PREVIEW_FAILURE', () => {
+ it('saves graph data', () => {
+ mutations[types.RECEIVE_PANEL_PREVIEW_FAILURE](stateCopy, 'Error!');
+
+ expect(stateCopy.panelPreviewIsLoading).toBe(false);
+ expect(stateCopy.panelPreviewGraphData).toBe(null);
+ expect(stateCopy.panelPreviewError).toBe('Error!');
+ });
+ });
+
+ describe('panel preview metric', () => {
+ const getPreviewMetricAt = i => stateCopy.panelPreviewGraphData.metrics[i];
+
+ beforeEach(() => {
+ stateCopy.panelPreviewGraphData = {
+ title: 'Preview panel title',
+ metrics: [
+ {
+ query: 'query',
+ },
+ ],
+ };
+ });
+
+ describe('REQUEST_PANEL_PREVIEW_METRIC_RESULT', () => {
+ it('sets the metric to loading for the first time', () => {
+ mutations[types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](stateCopy, { index: 0 });
+
+ expect(getPreviewMetricAt(0).loading).toBe(true);
+ expect(getPreviewMetricAt(0).state).toBe(metricStates.LOADING);
+ });
+
+ it('sets the metric to loading and keeps the result', () => {
+ getPreviewMetricAt(0).result = [[0, 1]];
+ getPreviewMetricAt(0).state = metricStates.OK;
+
+ mutations[types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](stateCopy, { index: 0 });
+
+ expect(getPreviewMetricAt(0)).toMatchObject({
+ loading: true,
+ result: [[0, 1]],
+ state: metricStates.OK,
+ });
+ });
+ });
+
+ describe('RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS', () => {
+ it('saves the result in the metric', () => {
+ const data = prometheusMatrixMultiResult();
+
+ mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS](stateCopy, {
+ index: 0,
+ data,
+ });
+
+ expect(getPreviewMetricAt(0)).toMatchObject({
+ loading: false,
+ state: metricStates.OK,
+ result: expect.any(Array),
+ });
+ expect(getPreviewMetricAt(0).result).toHaveLength(data.result.length);
+ });
+ });
+
+ describe('RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE', () => {
+ it('stores an error in the metric', () => {
+ mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](stateCopy, {
+ index: 0,
+ });
+
+ expect(getPreviewMetricAt(0).loading).toBe(false);
+ expect(getPreviewMetricAt(0).state).toBe(metricStates.UNKNOWN_ERROR);
+ expect(getPreviewMetricAt(0).result).toBe(null);
+
+ expect(getPreviewMetricAt(0)).toMatchObject({
+ loading: false,
+ result: null,
+ state: metricStates.UNKNOWN_ERROR,
+ });
+ });
+
+ it('stores a timeout error in a metric', () => {
+ mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](stateCopy, {
+ index: 0,
+ error: { message: 'BACKOFF_TIMEOUT' },
+ });
+
+ expect(getPreviewMetricAt(0)).toMatchObject({
+ loading: false,
+ result: null,
+ state: metricStates.TIMEOUT,
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index 35ca6ba9b52..fd7d09f7f72 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -1,6 +1,6 @@
+import { TEST_HOST } from 'jest/helpers/test_constants';
import * as monitoringUtils from '~/monitoring/utils';
import * as urlUtils from '~/lib/utils/url_utility';
-import { TEST_HOST } from 'jest/helpers/test_constants';
import { mockProjectDir, barMockData } from './mock_data';
import { singleStatGraphData, anomalyGraphData } from './graph_data';
import { metricsDashboardViewModel, graphData } from './fixture_data';