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/components/__snapshots__/dashboard_template_spec.js.snap55
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap68
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js133
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js10
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js54
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js95
-rw-r--r--spec/frontend/monitoring/components/create_dashboard_modal_spec.js48
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js232
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js59
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js237
-rw-r--r--spec/frontend/monitoring/components/dashboard_template_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js5
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js184
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js111
-rw-r--r--spec/frontend/monitoring/components/empty_state_spec.js23
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js126
-rw-r--r--spec/frontend/monitoring/components/links_section_spec.js2
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js143
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js (renamed from spec/frontend/monitoring/components/variables/custom_variable_spec.js)33
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js (renamed from spec/frontend/monitoring/components/variables/text_variable_spec.js)8
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js63
-rw-r--r--spec/frontend/monitoring/fixture_data.js40
-rw-r--r--spec/frontend/monitoring/graph_data.js164
-rw-r--r--spec/frontend/monitoring/mock_data.js572
-rw-r--r--spec/frontend/monitoring/pages/dashboard_page_spec.js36
-rw-r--r--spec/frontend/monitoring/router_spec.js81
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js980
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js40
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js154
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js297
-rw-r--r--spec/frontend/monitoring/store/variable_mapping_spec.js263
-rw-r--r--spec/frontend/monitoring/store_utils.js32
-rw-r--r--spec/frontend/monitoring/utils_spec.js55
33 files changed, 2896 insertions, 1511 deletions
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 4b08163f30a..e7c51d82cd2 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -4,22 +4,32 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="prometheus-graphs"
data-qa-selector="prometheus_graphs"
+ environmentstate="available"
+ metricsdashboardbasepath="/monitoring/monitor-project/-/environments/1/metrics"
+ metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
+ prometheusstatus=""
>
<div
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
>
<div
- class="mb-2 pr-2 d-flex d-sm-block"
+ class="mb-2 mr-2 d-flex d-sm-block"
>
<dashboards-dropdown-stub
class="flex-grow-1"
data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master"
id="monitor-dashboards-dropdown"
+ modalid="duplicateDashboard"
toggle-class="dropdown-menu-toggle"
/>
</div>
+ <span
+ aria-hidden="true"
+ class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"
+ />
+
<div
class="mb-2 pr-2 d-flex d-sm-block"
>
@@ -80,17 +90,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="mb-2 pr-2 d-flex d-sm-block"
>
- <gl-deprecated-button-stub
- class="flex-grow-1"
- size="md"
- title="Refresh dashboard"
- variant="default"
- >
- <icon-stub
- name="retry"
- size="16"
- />
- </gl-deprecated-button-stub>
+ <refresh-button-stub />
</div>
<div
@@ -127,23 +127,30 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<!---->
<!---->
+
+ <!---->
+
+ <!---->
+
+ <!---->
</div>
+
+ <duplicate-dashboard-modal-stub
+ defaultbranch="master"
+ modalid="duplicateDashboard"
+ />
</div>
- <!---->
-
- <!---->
-
<empty-state-stub
- clusterspath="/path/to/clusters"
- documentationpath="/path/to/docs"
- emptygettingstartedsvgpath="/path/to/getting-started.svg"
- emptyloadingsvgpath="/path/to/loading.svg"
- emptynodatasmallsvgpath="/path/to/no-data-small.svg"
- emptynodatasvgpath="/path/to/no-data.svg"
- emptyunabletoconnectsvgpath="/path/to/unable-to-connect.svg"
+ clusterspath="/monitoring/monitor-project/-/clusters"
+ documentationpath="/help/administration/monitoring/prometheus/index.md"
+ emptygettingstartedsvgpath="/images/illustrations/monitoring/getting_started.svg"
+ emptyloadingsvgpath="/images/illustrations/monitoring/loading.svg"
+ emptynodatasmallsvgpath="/images/illustrations/chart-empty-state-small.svg"
+ emptynodatasvgpath="/images/illustrations/monitoring/no_data.svg"
+ emptyunabletoconnectsvgpath="/images/illustrations/monitoring/unable_to_connect.svg"
selectedstate="gettingStarted"
- settingspath="/path/to/settings"
+ settingspath="/monitoring/monitor-project/-/services/prometheus/edit"
/>
</div>
`;
diff --git a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
index 31b3ad1bd76..4f8a82692b8 100644
--- a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
@@ -1,37 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyState shows gettingStarted state 1`] = `
-<gl-empty-state-stub
- description="Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
- primarybuttonlink="/clustersPath"
- primarybuttontext="Install on clusters"
- secondarybuttonlink="/settingsPath"
- secondarybuttontext="Configure existing installation"
- svgpath="/path/to/getting-started.svg"
- title="Get started with performance monitoring"
-/>
+<div>
+ <!---->
+
+ <gl-empty-state-stub
+ description="Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
+ primarybuttonlink="/clustersPath"
+ primarybuttontext="Install on clusters"
+ secondarybuttonlink="/settingsPath"
+ secondarybuttontext="Configure existing installation"
+ svgpath="/path/to/getting-started.svg"
+ title="Get started with performance monitoring"
+ />
+</div>
`;
-exports[`EmptyState shows loading state 1`] = `
-<gl-empty-state-stub
- description="Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available."
- primarybuttonlink="/documentationPath"
- primarybuttontext="View documentation"
- secondarybuttonlink=""
- secondarybuttontext=""
- svgpath="/path/to/loading.svg"
- title="Waiting for performance data"
-/>
+exports[`EmptyState shows noData state 1`] = `
+<div>
+ <!---->
+
+ <gl-empty-state-stub
+ description="You are connected to the Prometheus server, but there is currently no data to display."
+ primarybuttonlink="/settingsPath"
+ primarybuttontext="Configure Prometheus"
+ secondarybuttonlink=""
+ secondarybuttontext=""
+ svgpath="/path/to/no-data.svg"
+ title="No data found"
+ />
+</div>
`;
exports[`EmptyState shows unableToConnect state 1`] = `
-<gl-empty-state-stub
- description="Ensure connectivity is available from the GitLab server to the Prometheus server"
- primarybuttonlink="/documentationPath"
- primarybuttontext="View documentation"
- secondarybuttonlink="/settingsPath"
- secondarybuttontext="Configure Prometheus"
- svgpath="/path/to/unable-to-connect.svg"
- title="Unable to connect to Prometheus server"
-/>
+<div>
+ <!---->
+
+ <gl-empty-state-stub
+ description="Ensure connectivity is available from the GitLab server to the Prometheus server"
+ primarybuttonlink="/documentationPath"
+ primarybuttontext="View documentation"
+ secondarybuttonlink="/settingsPath"
+ secondarybuttontext="Configure Prometheus"
+ svgpath="/path/to/unable-to-connect.svg"
+ title="Unable to connect to Prometheus server"
+ />
+</div>
`;
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
index 4178d3f0d2d..15a52d03bcd 100644
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -3,28 +3,14 @@ import { TEST_HOST } from 'helpers/test_constants';
import Anomaly from '~/monitoring/components/charts/anomaly.vue';
import { colorValues } from '~/monitoring/constants';
-import {
- anomalyDeploymentData,
- mockProjectDir,
- anomalyMockGraphData,
- anomalyMockResultValues,
-} from '../../mock_data';
+import { anomalyDeploymentData, mockProjectDir } from '../../mock_data';
+import { anomalyGraphData } from '../../graph_data';
import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
-const makeAnomalyGraphData = (datasetName, template = anomalyMockGraphData) => {
- const metrics = anomalyMockResultValues[datasetName].map((values, index) => ({
- ...template.metrics[index],
- result: [
- {
- metrics: {},
- values,
- },
- ],
- }));
- return { ...template, metrics };
-};
+const TEST_UPPER = 11;
+const TEST_LOWER = 9;
describe('Anomaly chart component', () => {
let wrapper;
@@ -38,13 +24,22 @@ describe('Anomaly chart component', () => {
const getTimeSeriesProps = () => findTimeSeries().props();
describe('wrapped monitor-time-series-chart component', () => {
- const dataSetName = 'noAnomaly';
- const dataSet = anomalyMockResultValues[dataSetName];
+ const mockValues = ['10', '10', '10'];
+
+ const mockGraphData = anomalyGraphData(
+ {},
+ {
+ upper: mockValues.map(() => String(TEST_UPPER)),
+ values: mockValues,
+ lower: mockValues.map(() => String(TEST_LOWER)),
+ },
+ );
+
const inputThresholds = ['some threshold'];
beforeEach(() => {
setupAnomalyChart({
- graphData: makeAnomalyGraphData(dataSetName),
+ graphData: mockGraphData,
deploymentData: anomalyDeploymentData,
thresholds: inputThresholds,
projectPath: mockProjectPath,
@@ -65,21 +60,21 @@ describe('Anomaly chart component', () => {
it('receives "metric" with all data', () => {
const { graphData } = getTimeSeriesProps();
- const query = graphData.metrics[0];
- const expectedQuery = makeAnomalyGraphData(dataSetName).metrics[0];
- expect(query).toEqual(expectedQuery);
+ const metric = graphData.metrics[0];
+ const expectedMetric = mockGraphData.metrics[0];
+ expect(metric).toEqual(expectedMetric);
});
it('receives the "metric" results', () => {
const { graphData } = getTimeSeriesProps();
const { result } = graphData.metrics[0];
const { values } = result[0];
- const [metricDataset] = dataSet;
- expect(values).toEqual(expect.any(Array));
- values.forEach(([, y], index) => {
- expect(y).toBeCloseTo(metricDataset[index][1]);
- });
+ expect(values).toEqual([
+ [expect.any(String), 10],
+ [expect.any(String), 10],
+ [expect.any(String), 10],
+ ]);
});
});
@@ -108,14 +103,13 @@ describe('Anomaly chart component', () => {
it('upper boundary values are stacked on top of lower boundary', () => {
const [lowerSeries, upperSeries] = series;
- const [, upperDataset, lowerDataset] = dataSet;
- lowerSeries.data.forEach(([, y], i) => {
- expect(y).toBeCloseTo(lowerDataset[i][1]);
+ lowerSeries.data.forEach(([, y]) => {
+ expect(y).toBeCloseTo(TEST_LOWER);
});
- upperSeries.data.forEach(([, y], i) => {
- expect(y).toBeCloseTo(upperDataset[i][1] - lowerDataset[i][1]);
+ upperSeries.data.forEach(([, y]) => {
+ expect(y).toBeCloseTo(TEST_UPPER - TEST_LOWER);
});
});
});
@@ -140,11 +134,10 @@ describe('Anomaly chart component', () => {
}),
);
});
+
it('does not display anomalies', () => {
const { symbolSize, itemStyle } = seriesConfig;
- const [metricDataset] = dataSet;
-
- metricDataset.forEach((v, dataIndex) => {
+ mockValues.forEach((v, dataIndex) => {
const size = symbolSize(null, { dataIndex });
const color = itemStyle.color({ dataIndex });
@@ -155,9 +148,10 @@ describe('Anomaly chart component', () => {
});
it('can format y values (to use in tooltips)', () => {
- expect(parseFloat(wrapper.vm.yValueFormatted(0, 0))).toEqual(dataSet[0][0][1]);
- expect(parseFloat(wrapper.vm.yValueFormatted(1, 0))).toEqual(dataSet[1][0][1]);
- expect(parseFloat(wrapper.vm.yValueFormatted(2, 0))).toEqual(dataSet[2][0][1]);
+ mockValues.forEach((v, dataIndex) => {
+ const formatted = wrapper.vm.yValueFormatted(0, dataIndex);
+ expect(parseFloat(formatted)).toEqual(parseFloat(v));
+ });
});
});
@@ -179,12 +173,18 @@ describe('Anomaly chart component', () => {
});
describe('with no boundary data', () => {
- const dataSetName = 'noBoundary';
- const dataSet = anomalyMockResultValues[dataSetName];
+ const noBoundaryData = anomalyGraphData(
+ {},
+ {
+ upper: [],
+ values: ['10', '10', '10'],
+ lower: [],
+ },
+ );
beforeEach(() => {
setupAnomalyChart({
- graphData: makeAnomalyGraphData(dataSetName),
+ graphData: noBoundaryData,
deploymentData: anomalyDeploymentData,
});
});
@@ -204,7 +204,7 @@ describe('Anomaly chart component', () => {
});
it('can format y values (to use in tooltips)', () => {
- expect(parseFloat(wrapper.vm.yValueFormatted(0, 0))).toEqual(dataSet[0][0][1]);
+ expect(parseFloat(wrapper.vm.yValueFormatted(0, 0))).toEqual(10);
expect(wrapper.vm.yValueFormatted(1, 0)).toBe(''); // missing boundary
expect(wrapper.vm.yValueFormatted(2, 0)).toBe(''); // missing boundary
});
@@ -212,12 +212,20 @@ describe('Anomaly chart component', () => {
});
describe('with one anomaly', () => {
- const dataSetName = 'oneAnomaly';
- const dataSet = anomalyMockResultValues[dataSetName];
+ const mockValues = ['10', '20', '10'];
+
+ const oneAnomalyData = anomalyGraphData(
+ {},
+ {
+ upper: mockValues.map(() => TEST_UPPER),
+ values: mockValues,
+ lower: mockValues.map(() => TEST_LOWER),
+ },
+ );
beforeEach(() => {
setupAnomalyChart({
- graphData: makeAnomalyGraphData(dataSetName),
+ graphData: oneAnomalyData,
deploymentData: anomalyDeploymentData,
});
});
@@ -226,13 +234,12 @@ describe('Anomaly chart component', () => {
it('displays one anomaly', () => {
const { seriesConfig } = getTimeSeriesProps();
const { symbolSize, itemStyle } = seriesConfig;
- const [metricDataset] = dataSet;
- const bigDots = metricDataset.filter((v, dataIndex) => {
+ const bigDots = mockValues.filter((v, dataIndex) => {
const size = symbolSize(null, { dataIndex });
return size > 0.1;
});
- const redDots = metricDataset.filter((v, dataIndex) => {
+ const redDots = mockValues.filter((v, dataIndex) => {
const color = itemStyle.color({ dataIndex });
return color === colorValues.anomalySymbol;
});
@@ -244,13 +251,21 @@ describe('Anomaly chart component', () => {
});
describe('with offset', () => {
- const dataSetName = 'negativeBoundary';
- const dataSet = anomalyMockResultValues[dataSetName];
- const expectedOffset = 4; // Lowst point in mock data is -3.70, it gets rounded
+ const mockValues = ['10', '11', '12'];
+ const mockUpper = ['20', '20', '20'];
+ const mockLower = ['-1', '-2', '-3.70'];
+ const expectedOffset = 4; // Lowest point in mock data is -3.70, it gets rounded
beforeEach(() => {
setupAnomalyChart({
- graphData: makeAnomalyGraphData(dataSetName),
+ graphData: anomalyGraphData(
+ {},
+ {
+ upper: mockUpper,
+ values: mockValues,
+ lower: mockLower,
+ },
+ ),
deploymentData: anomalyDeploymentData,
});
});
@@ -266,11 +281,11 @@ describe('Anomaly chart component', () => {
const { graphData } = getTimeSeriesProps();
const { result } = graphData.metrics[0];
const { values } = result[0];
- const [metricDataset] = dataSet;
+
expect(values).toEqual(expect.any(Array));
values.forEach(([, y], index) => {
- expect(y).toBeCloseTo(metricDataset[index][1] + expectedOffset);
+ expect(y).toBeCloseTo(parseFloat(mockValues[index]) + expectedOffset);
});
});
});
@@ -281,14 +296,12 @@ describe('Anomaly chart component', () => {
const { option } = getTimeSeriesProps();
const { series } = option;
const [lowerSeries, upperSeries] = series;
- const [, upperDataset, lowerDataset] = dataSet;
-
lowerSeries.data.forEach(([, y], i) => {
- expect(y).toBeCloseTo(lowerDataset[i][1] + expectedOffset);
+ expect(y).toBeCloseTo(parseFloat(mockLower[i]) + expectedOffset);
});
upperSeries.data.forEach(([, y], i) => {
- expect(y).toBeCloseTo(upperDataset[i][1] - lowerDataset[i][1]);
+ expect(y).toBeCloseTo(parseFloat(mockUpper[i] - mockLower[i]));
});
});
});
diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js
index 89739a7485d..a2056d96dcf 100644
--- a/spec/frontend/monitoring/components/charts/column_spec.js
+++ b/spec/frontend/monitoring/components/charts/column_spec.js
@@ -63,8 +63,8 @@ describe('Column component', () => {
return formatter(date);
};
- it('x-axis is formatted correctly in AM/PM format', () => {
- expect(useXAxisFormatter(mockDate)).toEqual('8:00 PM');
+ it('x-axis is formatted correctly in m/d h:MM TT format', () => {
+ expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
});
describe('when in PT timezone', () => {
@@ -78,17 +78,17 @@ describe('Column component', () => {
it('by default, values are formatted in PT', () => {
createWrapper();
- expect(useXAxisFormatter(mockDate)).toEqual('1:00 PM');
+ expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
});
it('when the chart uses local timezone, y-axis is formatted in PT', () => {
createWrapper({ timezone: 'LOCAL' });
- expect(useXAxisFormatter(mockDate)).toEqual('1:00 PM');
+ expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
});
it('when the chart uses UTC, y-axis is formatted in UTC', () => {
createWrapper({ timezone: 'UTC' });
- expect(useXAxisFormatter(mockDate)).toEqual('8:00 PM');
+ expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
});
});
});
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
index 9cc5970da82..3783b1eebd2 100644
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
-import { singleStatMetricsResult } from '../../mock_data';
+import { singleStatGraphData } from '../../graph_data';
describe('Single Stat Chart component', () => {
let singleStatChart;
@@ -8,7 +8,7 @@ describe('Single Stat Chart component', () => {
beforeEach(() => {
singleStatChart = shallowMount(SingleStatChart, {
propsData: {
- graphData: singleStatMetricsResult,
+ graphData: singleStatGraphData({}, { unit: 'MB' }),
},
});
});
@@ -20,15 +20,12 @@ describe('Single Stat Chart component', () => {
describe('computed', () => {
describe('statValue', () => {
it('should interpolate the value and unit props', () => {
- expect(singleStatChart.vm.statValue).toBe('91.00MB');
+ expect(singleStatChart.vm.statValue).toBe('1.00MB');
});
it('should change the value representation to a percentile one', () => {
singleStatChart.setProps({
- graphData: {
- ...singleStatMetricsResult,
- maxValue: 120,
- },
+ graphData: singleStatGraphData({ max_value: 120 }, { value: 91 }),
});
expect(singleStatChart.vm.statValue).toContain('75.83%');
@@ -36,10 +33,7 @@ describe('Single Stat Chart component', () => {
it('should display NaN for non numeric maxValue values', () => {
singleStatChart.setProps({
- graphData: {
- ...singleStatMetricsResult,
- maxValue: 'not a number',
- },
+ graphData: singleStatGraphData({ max_value: 'not a number' }),
});
expect(singleStatChart.vm.statValue).toContain('NaN');
@@ -47,25 +41,33 @@ describe('Single Stat Chart component', () => {
it('should display NaN for missing query values', () => {
singleStatChart.setProps({
- graphData: {
- ...singleStatMetricsResult,
- metrics: [
- {
- ...singleStatMetricsResult.metrics[0],
- result: [
- {
- ...singleStatMetricsResult.metrics[0].result[0],
- value: [''],
- },
- ],
- },
- ],
- maxValue: 120,
- },
+ graphData: singleStatGraphData({ max_value: 120 }, { value: 'NaN' }),
});
expect(singleStatChart.vm.statValue).toContain('NaN');
});
+
+ describe('field attribute', () => {
+ it('displays a label value instead of metric value when field attribute is used', () => {
+ singleStatChart.setProps({
+ graphData: singleStatGraphData({ field: 'job' }, { isVector: true }),
+ });
+
+ return singleStatChart.vm.$nextTick(() => {
+ expect(singleStatChart.vm.statValue).toContain('prometheus');
+ });
+ });
+
+ it('displays No data to display if field attribute is not present', () => {
+ singleStatChart.setProps({
+ graphData: singleStatGraphData({ field: 'this-does-not-exist' }),
+ });
+
+ return singleStatChart.vm.$nextTick(() => {
+ expect(singleStatChart.vm.statValue).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 50d2c9c80b2..97386be9e32 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -9,18 +9,12 @@ import {
GlChartSeriesLabel,
GlChartLegend,
} from '@gitlab/ui/dist/charts';
-import { cloneDeep } from 'lodash';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
-import { createStore } from '~/monitoring/stores';
import { panelTypes, chartHeight } from '~/monitoring/constants';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
-import * as types from '~/monitoring/stores/mutation_types';
import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data';
-import {
- metricsDashboardPayload,
- metricsDashboardViewModel,
- metricResultStatus,
-} from '../../fixture_data';
+
+import { timeSeriesGraphData } from '../../graph_data';
jest.mock('lodash/throttle', () =>
// this throttle mock executes immediately
@@ -35,23 +29,21 @@ jest.mock('~/lib/utils/icon_utils', () => ({
}));
describe('Time series component', () => {
- let mockGraphData;
- let store;
+ const defaultGraphData = timeSeriesGraphData();
let wrapper;
const createWrapper = (
- { graphData = mockGraphData, ...props } = {},
+ { graphData = defaultGraphData, ...props } = {},
mountingMethod = shallowMount,
) => {
wrapper = mountingMethod(TimeSeries, {
propsData: {
graphData,
- deploymentData: store.state.monitoringDashboard.deploymentData,
- annotations: store.state.monitoringDashboard.annotations,
+ deploymentData,
+ annotations: annotationsData,
projectPath: `${TEST_HOST}${mockProjectDir}`,
...props,
},
- store,
stubs: {
GlPopover: true,
},
@@ -59,27 +51,15 @@ describe('Time series component', () => {
});
};
- describe('With a single time series', () => {
- beforeEach(() => {
- setTestTimeout(1000);
-
- store = createStore();
-
- store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
- metricsDashboardPayload,
- );
-
- store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
+ beforeEach(() => {
+ setTestTimeout(1000);
+ });
- store.commit(
- `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
- metricResultStatus,
- );
- // dashboard is a dynamically generated fixture and stored at environment_metrics_dashboard.json
- [mockGraphData] = store.state.monitoringDashboard.dashboard.panelGroups[1].panels;
- });
+ afterEach(() => {
+ wrapper.destroy();
+ });
+ describe('With a single time series', () => {
describe('general functions', () => {
const findChart = () => wrapper.find({ ref: 'chart' });
@@ -88,10 +68,6 @@ describe('Time series component', () => {
return wrapper.vm.$nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('allows user to override legend label texts using props', () => {
const legendRelatedProps = {
legendMinText: 'legendMinText',
@@ -231,19 +207,20 @@ describe('Time series component', () => {
});
it('formats tooltip content', () => {
- const name = 'Status Code';
+ const name = 'Metric 1';
const value = '5.556';
const dataIndex = 0;
const seriesLabel = wrapper.find(GlChartSeriesLabel);
expect(seriesLabel.vm.color).toBe('');
+
expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
expect(wrapper.vm.tooltip.content).toEqual([
{ name, value, dataIndex, color: undefined },
]);
expect(
- shallowWrapperContainsSlotText(wrapper.find(GlAreaChart), 'tooltipContent', value),
+ shallowWrapperContainsSlotText(wrapper.find(GlLineChart), 'tooltipContent', value),
).toBe(true);
});
@@ -385,10 +362,8 @@ describe('Time series component', () => {
});
it('utilizes all data points', () => {
- const { values } = mockGraphData.metrics[0].result[0];
-
expect(chartData.length).toBe(1);
- expect(seriesData().data.length).toBe(values.length);
+ expect(seriesData().data.length).toBe(3);
});
it('creates valid data', () => {
@@ -552,8 +527,8 @@ describe('Time series component', () => {
return formatter(date);
};
- it('x-axis is formatted correctly in AM/PM format', () => {
- expect(useXAxisFormatter(mockDate)).toEqual('8:00 PM');
+ it('x-axis is formatted correctly in m/d h:MM TT format', () => {
+ expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
});
describe('when in PT timezone', () => {
@@ -567,17 +542,17 @@ describe('Time series component', () => {
it('by default, values are formatted in PT', () => {
createWrapper();
- expect(useXAxisFormatter(mockDate)).toEqual('1:00 PM');
+ expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
});
it('when the chart uses local timezone, y-axis is formatted in PT', () => {
createWrapper({ timezone: 'LOCAL' });
- expect(useXAxisFormatter(mockDate)).toEqual('1:00 PM');
+ expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
});
it('when the chart uses UTC, y-axis is formatted in UTC', () => {
createWrapper({ timezone: 'UTC' });
- expect(useXAxisFormatter(mockDate)).toEqual('8:00 PM');
+ expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
});
});
});
@@ -602,14 +577,10 @@ describe('Time series component', () => {
it('constructs a label for the chart y-axis', () => {
const { yAxis } = getChartOptions();
- expect(yAxis[0].name).toBe('Requests / Sec');
+ expect(yAxis[0].name).toBe('Y Axis');
});
});
});
-
- afterEach(() => {
- wrapper.destroy();
- });
});
describe('wrapped components', () => {
@@ -630,7 +601,7 @@ describe('Time series component', () => {
beforeEach(() => {
createWrapper(
- { graphData: { ...mockGraphData, type: dynamicComponent.chartType } },
+ { graphData: timeSeriesGraphData({ type: dynamicComponent.chartType }) },
mount,
);
return wrapper.vm.$nextTick();
@@ -700,20 +671,12 @@ describe('Time series component', () => {
describe('with multiple time series', () => {
describe('General functions', () => {
beforeEach(() => {
- store = createStore();
- const graphData = cloneDeep(metricsDashboardViewModel.panelGroups[0].panels[3]);
- graphData.metrics.forEach(metric =>
- Object.assign(metric, { result: metricResultStatus.result }),
- );
+ const graphData = timeSeriesGraphData({ type: panelTypes.AREA_CHART, multiMetric: true });
- createWrapper({ graphData: { ...graphData, type: 'area-chart' } }, mount);
+ createWrapper({ graphData }, mount);
return wrapper.vm.$nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Color match', () => {
let lineColors;
@@ -754,14 +717,10 @@ describe('Time series component', () => {
const findLegend = () => wrapper.find(GlChartLegend);
beforeEach(() => {
- createWrapper(mockGraphData, mount);
+ createWrapper({}, mount);
return wrapper.vm.$nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a tabular legend layout by default', () => {
expect(findLegend().props('layout')).toBe('table');
});
diff --git a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
new file mode 100644
index 00000000000..d1028445638
--- /dev/null
+++ b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
@@ -0,0 +1,48 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
+
+describe('Create dashboard modal', () => {
+ let wrapper;
+
+ const defaultProps = {
+ modalId: 'id',
+ projectPath: 'https://localhost/',
+ addDashboardDocumentationPath: 'https://link/to/docs',
+ };
+
+ const findDocsButton = () => wrapper.find('[data-testid="create-dashboard-modal-docs-button"]');
+ const findRepoButton = () => wrapper.find('[data-testid="create-dashboard-modal-repo-button"]');
+
+ const createWrapper = (props = {}, options = {}) => {
+ wrapper = shallowMount(CreateDashboardModal, {
+ propsData: { ...defaultProps, ...props },
+ stubs: {
+ GlModal,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has button that links to the project url', () => {
+ findRepoButton().trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findRepoButton().exists()).toBe(true);
+ expect(findRepoButton().attributes('href')).toBe(defaultProps.projectPath);
+ });
+ });
+
+ it('has button that links to the docs', () => {
+ expect(findDocsButton().exists()).toBe(true);
+ expect(findDocsButton().attributes('href')).toBe(defaultProps.addDashboardDocumentationPath);
+ });
+});
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
new file mode 100644
index 00000000000..5a1a615c703
--- /dev/null
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -0,0 +1,232 @@
+import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/monitoring/stores';
+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 {
+ dashboardGitResponse,
+ selfMonitoringDashboardGitResponse,
+ dashboardHeaderProps,
+} from '../mock_data';
+import { redirectTo } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+ queryToObject: jest.fn(),
+ mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
+}));
+
+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 createShallowWrapper = (props = {}, options = {}) => {
+ wrapper = shallowMount(DashboardHeader, {
+ propsData: { ...dashboardHeaderProps, ...props },
+ store,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
+ beforeEach(() => {
+ store.state.monitoringDashboard.projectPath = 'root/sandbox';
+ });
+ /**
+ * 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];
+
+ createShallowWrapper();
+
+ 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('actions menu', () => {
+ beforeEach(() => {
+ store.state.monitoringDashboard.projectPath = '';
+ createShallowWrapper();
+ });
+
+ it('is rendered if projectPath is set in store', () => {
+ store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().exists()).toBe(true);
+ });
+ });
+
+ it('is not rendered if projectPath is not set in store', () => {
+ expect(findActionsMenu().exists()).toBe(false);
+ });
+
+ it('contains a modal', () => {
+ store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().contains(CreateDashboardModal)).toBe(true);
+ });
+ });
+
+ const duplicableCases = [
+ null, // When no path is specified, it uses the default dashboard path.
+ dashboardGitResponse[0].path,
+ dashboardGitResponse[2].path,
+ selfMonitoringDashboardGitResponse[0].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);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCreateDashboardMenuItem().exists()).toBe(true);
+ expect(findCreateDashboardDuplicateItem().exists()).toBe(true);
+ });
+ });
+ },
+ );
+
+ const nonDuplicableCases = [
+ dashboardGitResponse[1].path,
+ selfMonitoringDashboardGitResponse[1].path,
+ ];
+
+ 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';
+ setupAllDashboards(store, dashboardPath);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCreateDashboardMenuItem().exists()).toBe(true);
+ expect(findCreateDashboardDuplicateItem().exists()).toBe(false);
+ });
+ });
+ },
+ );
+ });
+
+ describe('actions menu modals', () => {
+ const url = 'https://path/to/project';
+
+ beforeEach(() => {
+ store.state.monitoringDashboard.projectPath = url;
+ setupAllDashboards(store);
+
+ createShallowWrapper();
+ });
+
+ it('Clicking on "Create New" opens up a modal', () => {
+ const modalId = 'createDashboard';
+ const modalTrigger = findCreateDashboardMenuItem();
+ const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+
+ modalTrigger.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
+ });
+ });
+
+ it('"Create new dashboard" modal contains correct buttons', () => {
+ expect(findCreateDashboardModal().props('projectPath')).toBe(url);
+ });
+
+ it('"Duplicate Dashboard" opens up a modal', () => {
+ const modalId = 'duplicateDashboard';
+ const modalTrigger = findCreateDashboardDuplicateItem();
+ const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+
+ modalTrigger.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
+ });
+ });
+ });
+
+ describe('metrics settings button', () => {
+ const findSettingsButton = () => wrapper.find('[data-testid="metrics-settings-button"]');
+ const url = 'https://path/to/project/settings';
+
+ beforeEach(() => {
+ createShallowWrapper();
+
+ store.state.monitoringDashboard.canAccessOperationsSettings = false;
+ store.state.monitoringDashboard.operationsSettingsPath = '';
+ });
+
+ it('is rendered when the user can access the project settings and path to settings is available', () => {
+ store.state.monitoringDashboard.canAccessOperationsSettings = true;
+ store.state.monitoringDashboard.operationsSettingsPath = url;
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findSettingsButton().exists()).toBe(true);
+ });
+ });
+
+ it('is not rendered when the user can not access the project settings', () => {
+ store.state.monitoringDashboard.canAccessOperationsSettings = false;
+ store.state.monitoringDashboard.operationsSettingsPath = url;
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findSettingsButton().exists()).toBe(false);
+ });
+ });
+
+ it('is not rendered when the path to settings is unavailable', () => {
+ store.state.monitoringDashboard.canAccessOperationsSettings = false;
+ store.state.monitoringDashboard.operationsSettingsPath = '';
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findSettingsButton().exists()).toBe(false);
+ });
+ });
+
+ it('leads to the project settings page', () => {
+ store.state.monitoringDashboard.canAccessOperationsSettings = true;
+ store.state.monitoringDashboard.operationsSettingsPath = url;
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findSettingsButton().attributes('href')).toBe(url);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index 0ad6e04588f..693818aa55a 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -9,17 +9,16 @@ import AlertWidget from '~/monitoring/components/alert_widget.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import {
- anomalyMockGraphData,
mockLogsHref,
mockLogsPath,
mockNamespace,
mockNamespacedData,
mockTimeRange,
- singleStatMetricsResult,
graphDataPrometheusQueryRangeMultiTrack,
barMockData,
- propsData,
} from '../mock_data';
+import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
+import { anomalyGraphData, singleStatGraphData } from '../graph_data';
import { panelTypes } from '~/monitoring/constants';
@@ -32,7 +31,6 @@ import MonitorColumnChart from '~/monitoring/components/charts/column.vue';
import MonitorBarChart from '~/monitoring/components/charts/bar.vue';
import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
-import { graphData, graphDataEmpty } from '../fixture_data';
import { createStore, monitoringDashboard } from '~/monitoring/stores';
import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
@@ -63,7 +61,7 @@ describe('Dashboard Panel', () => {
wrapper = shallowMount(DashboardPanel, {
propsData: {
graphData,
- settingsPath: propsData.settingsPath,
+ settingsPath: dashboardProps.settingsPath,
...props,
},
store,
@@ -137,10 +135,6 @@ describe('Dashboard Panel', () => {
expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
});
-
- it('does not contain a tabindex attribute', () => {
- expect(wrapper.find(MonitorEmptyChart).contains('[tabindex]')).toBe(false);
- });
});
describe('When graphData is null', () => {
@@ -233,23 +227,32 @@ describe('Dashboard Panel', () => {
expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
});
- it.each`
- data | component
- ${dataWithType(panelTypes.AREA_CHART)} | ${MonitorTimeSeriesChart}
- ${dataWithType(panelTypes.LINE_CHART)} | ${MonitorTimeSeriesChart}
- ${anomalyMockGraphData} | ${MonitorAnomalyChart}
- ${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart}
- ${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart}
- ${singleStatMetricsResult} | ${MonitorSingleStatChart}
- ${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart}
- ${barMockData} | ${MonitorBarChart}
- `('wrapps a $data.type component binding attributes', ({ data, component }) => {
+ describe.each`
+ data | component | hasCtxMenu
+ ${dataWithType(panelTypes.AREA_CHART)} | ${MonitorTimeSeriesChart} | ${true}
+ ${dataWithType(panelTypes.LINE_CHART)} | ${MonitorTimeSeriesChart} | ${true}
+ ${singleStatGraphData()} | ${MonitorSingleStatChart} | ${true}
+ ${anomalyGraphData()} | ${MonitorAnomalyChart} | ${false}
+ ${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart} | ${false}
+ ${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart} | ${false}
+ ${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart} | ${false}
+ ${barMockData} | ${MonitorBarChart} | ${false}
+ `('when $data.type data is provided', ({ data, component, hasCtxMenu }) => {
const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
- createWrapper({ graphData: data }, { attrs });
- expect(wrapper.find(component).exists()).toBe(true);
- expect(wrapper.find(component).isVueInstance()).toBe(true);
- expect(wrapper.find(component).attributes()).toMatchObject(attrs);
+ beforeEach(() => {
+ createWrapper({ graphData: data }, { attrs });
+ });
+
+ it(`renders the chart component and binds attributes`, () => {
+ expect(wrapper.find(component).exists()).toBe(true);
+ expect(wrapper.find(component).isVueInstance()).toBe(true);
+ expect(wrapper.find(component).attributes()).toMatchObject(attrs);
+ });
+
+ it(`contextual menu is ${hasCtxMenu ? '' : 'not '}shown`, () => {
+ expect(findCtxMenu().exists()).toBe(hasCtxMenu);
+ });
});
});
});
@@ -307,7 +310,7 @@ describe('Dashboard Panel', () => {
return wrapper.vm.$nextTick(() => {
expect(findEditCustomMetricLink().text()).toBe('Edit metrics');
- expect(findEditCustomMetricLink().attributes('href')).toBe(propsData.settingsPath);
+ expect(findEditCustomMetricLink().attributes('href')).toBe(dashboardProps.settingsPath);
});
});
});
@@ -361,7 +364,7 @@ describe('Dashboard Panel', () => {
});
});
- it('it is overriden when a datazoom event is received', () => {
+ it('it is overridden when a datazoom event is received', () => {
state.logsPath = mockLogsPath;
state.timeRange = mockTimeRange;
@@ -424,7 +427,7 @@ describe('Dashboard Panel', () => {
wrapper = shallowMount(DashboardPanel, {
propsData: {
clipboardText: exampleText,
- settingsPath: propsData.settingsPath,
+ settingsPath: dashboardProps.settingsPath,
graphData: {
y_label: 'metric',
...graphData,
@@ -474,7 +477,7 @@ describe('Dashboard Panel', () => {
wrapper = shallowMount(DashboardPanel, {
propsData: {
graphData,
- settingsPath: propsData.settingsPath,
+ settingsPath: dashboardProps.settingsPath,
namespace: mockNamespace,
},
store,
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 7bb4c68b4cd..4b7f7a9ddb3 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -6,16 +6,18 @@ import { objectToQuery } from '~/lib/utils/url_utility';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import { metricStates } from '~/monitoring/constants';
+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';
+import GraphGroup from '~/monitoring/components/graph_group.vue';
import LinksSection from '~/monitoring/components/links_section.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
@@ -24,12 +26,17 @@ import {
setupStoreWithDashboard,
setMetricResult,
setupStoreWithData,
- setupStoreWithVariable,
+ setupStoreWithDataForPanelCount,
setupStoreWithLinks,
} from '../store_utils';
-import { environmentData, dashboardGitResponse, propsData } from '../mock_data';
-import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data';
+import { environmentData, dashboardGitResponse, storeVariables } from '../mock_data';
+import {
+ metricsDashboardViewModel,
+ metricsDashboardPanelCount,
+ dashboardProps,
+} from '../fixture_data';
import createFlash from '~/flash';
+import { TEST_HOST } from 'helpers/test_constants';
jest.mock('~/flash');
@@ -48,7 +55,7 @@ describe('Dashboard', () => {
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(Dashboard, {
- propsData: { ...propsData, ...props },
+ propsData: { ...dashboardProps, ...props },
store,
stubs: {
DashboardHeader,
@@ -59,7 +66,7 @@ describe('Dashboard', () => {
const createMountedWrapper = (props = {}, options = {}) => {
wrapper = mount(Dashboard, {
- propsData: { ...propsData, ...props },
+ propsData: { ...dashboardProps, ...props },
store,
stubs: {
'graph-group': true,
@@ -120,13 +127,13 @@ describe('Dashboard', () => {
});
it('shows up a loading state', () => {
- store.state.monitoringDashboard.emptyState = 'loading';
+ store.state.monitoringDashboard.emptyState = dashboardEmptyStates.LOADING;
createShallowWrapper({ hasMetrics: true });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(EmptyState).exists()).toBe(true);
- expect(wrapper.find(EmptyState).props('selectedState')).toBe('loading');
+ expect(wrapper.find(EmptyState).props('selectedState')).toBe(dashboardEmptyStates.LOADING);
});
});
@@ -136,7 +143,7 @@ describe('Dashboard', () => {
setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.showEmptyState).toEqual(false);
+ expect(wrapper.vm.emptyState).toBeNull();
expect(wrapper.findAll('.prometheus-panel')).toHaveLength(0);
});
});
@@ -157,6 +164,103 @@ describe('Dashboard', () => {
});
});
+ describe('panel containers layout', () => {
+ const findPanelLayoutWrapperAt = index => {
+ return wrapper
+ .find(GraphGroup)
+ .findAll('[data-testid="dashboard-panel-layout-wrapper"]')
+ .at(index);
+ };
+
+ beforeEach(() => {
+ createMountedWrapper({ hasMetrics: true });
+
+ return wrapper.vm.$nextTick();
+ });
+
+ describe('when the graph group has an even number of panels', () => {
+ it('2 panels - all panel wrappers take half width of their parent', () => {
+ setupStoreWithDataForPanelCount(store, 2);
+
+ wrapper.vm.$nextTick(() => {
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
+ });
+ });
+
+ it('4 panels - all panel wrappers take half width of their parent', () => {
+ setupStoreWithDataForPanelCount(store, 4);
+
+ wrapper.vm.$nextTick(() => {
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
+ });
+ });
+ });
+
+ describe('when the graph group has an odd number of panels', () => {
+ it('1 panel - panel wrapper does not take half width of its parent', () => {
+ setupStoreWithDataForPanelCount(store, 1);
+
+ wrapper.vm.$nextTick(() => {
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(false);
+ });
+ });
+
+ it('3 panels - all panels but last take half width of their parents', () => {
+ setupStoreWithDataForPanelCount(store, 3);
+
+ wrapper.vm.$nextTick(() => {
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(false);
+ });
+ });
+
+ it('5 panels - all panels but last take half width of their parents', () => {
+ setupStoreWithDataForPanelCount(store, 5);
+
+ wrapper.vm.$nextTick(() => {
+ expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
+ expect(findPanelLayoutWrapperAt(4).classes('col-lg-6')).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('dashboard validation warning', () => {
+ it('displays a warning if there are validation warnings', () => {
+ createMountedWrapper({ hasMetrics: true });
+
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS}`,
+ true,
+ );
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ it('does not display a warning if there are no validation warnings', () => {
+ createMountedWrapper({ hasMetrics: true });
+
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS}`,
+ false,
+ );
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+ });
+ });
+
describe('when the URL contains a reference to a panel', () => {
let location;
@@ -323,12 +427,72 @@ 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', () => {
+ const findGroupAt = i => wrapper.findAll(GraphGroup).at(i);
+
+ beforeEach(() => {
+ setupStoreWithDashboard(store);
+
+ const { panels } = store.state.monitoringDashboard.dashboard.panelGroups[0];
+ panels.forEach(({ metrics }) => {
+ store.commit(`monitoringDashboard/${types.REQUEST_METRIC_RESULT}`, {
+ metricId: metrics[0].metricId,
+ });
+ });
+
+ createShallowWrapper();
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('a loading icon appears in the first group', () => {
+ expect(findGroupAt(0).props('isLoading')).toBe(true);
+ });
+
+ it('a loading icon does not appear in the second group', () => {
+ expect(findGroupAt(1).props('isLoading')).toBe(false);
+ });
});
describe('when all requests have been commited by the store', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentEnvironmentName: 'production',
+ currentDashboard: dashboardGitResponse[0].path,
+ projectPath: TEST_HOST,
});
createMountedWrapper({ hasMetrics: true });
setupStoreWithData(store);
@@ -341,13 +505,26 @@ describe('Dashboard', () => {
findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
const anchorEl = itemWrapper.find('a');
- if (anchorEl.exists() && environmentData[index].metrics_path) {
+ if (anchorEl.exists()) {
const href = anchorEl.attributes('href');
- expect(href).toBe(environmentData[index].metrics_path);
+ 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);
+
+ wrapper.vm.$nextTick(() => {
+ wrapper.findAll(GraphGroup).wrappers.forEach(groupWrapper => {
+ expect(groupWrapper.props('isLoading')).toBe(false);
+ });
+ });
+ });
+
// 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', () => {
@@ -464,10 +641,9 @@ describe('Dashboard', () => {
setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
- const refreshBtn = wrapper.find(DashboardHeader).findAll({ ref: 'refreshDashboardBtn' });
+ const refreshBtn = wrapper.find(DashboardHeader).find(RefreshButton);
- expect(refreshBtn).toHaveLength(1);
- expect(refreshBtn.is(GlDeprecatedButton)).toBe(true);
+ expect(refreshBtn.exists()).toBe(true);
});
});
@@ -475,8 +651,7 @@ describe('Dashboard', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
setupStoreWithData(store);
- setupStoreWithVariable(store);
-
+ store.state.monitoringDashboard.variables = storeVariables;
return wrapper.vm.$nextTick();
});
@@ -1041,6 +1216,34 @@ describe('Dashboard', () => {
});
});
+ describe('keyboard shortcuts', () => {
+ const currentDashboard = dashboardGitResponse[1].path;
+ const panelRef = 'dashboard-panel-response-metrics-aws-elb-4-1'; // skip expanded panel
+
+ // While the recommendation in the documentation is to test
+ // with a data-testid attribute, I want to make sure that
+ // the dashboard panels have a ref attribute set.
+ const getDashboardPanel = () => wrapper.find({ ref: panelRef });
+
+ beforeEach(() => {
+ setupStoreWithData(store);
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard,
+ });
+ createShallowWrapper({ hasMetrics: true });
+
+ wrapper.setData({ hoveredPanel: panelRef });
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('contains a ref attribute inside a DashboardPanel component', () => {
+ const dashboardPanel = getDashboardPanel();
+
+ expect(dashboardPanel.exists()).toBe(true);
+ });
+ });
+
describe('add custom metrics', () => {
const findAddMetricButton = () => wrapper.find(DashboardHeader).find({ ref: 'addMetricBtn' });
@@ -1082,7 +1285,7 @@ describe('Dashboard', () => {
it('uses modal for custom metrics form', () => {
expect(wrapper.find(GlModal).exists()).toBe(true);
- expect(wrapper.find(GlModal).attributes().modalid).toBe('add-metric');
+ expect(wrapper.find(GlModal).attributes().modalid).toBe('addMetric');
});
it('adding new metric is tracked', done => {
const submitButton = wrapper
diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js
index a1a450d4abe..8941e57c4ce 100644
--- a/spec/frontend/monitoring/components/dashboard_template_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_template_spec.js
@@ -5,7 +5,7 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import { createStore } from '~/monitoring/stores';
import { setupAllDashboards } from '../store_utils';
-import { propsData } from '../mock_data';
+import { dashboardProps } from '../fixture_data';
jest.mock('~/lib/utils/url_utility');
@@ -29,7 +29,7 @@ describe('Dashboard template', () => {
it('matches the default snapshot', () => {
wrapper = shallowMount(Dashboard, {
- propsData: { ...propsData },
+ propsData: { ...dashboardProps },
store,
stubs: {
DashboardHeader,
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index a74c621db9b..276e20bae6a 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -9,7 +9,8 @@ import {
updateHistory,
} from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
-import { mockProjectDir, propsData } from '../mock_data';
+import { mockProjectDir } from '../mock_data';
+import { dashboardProps } from '../fixture_data';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
@@ -26,7 +27,7 @@ describe('dashboard invalid url parameters', () => {
const createMountedWrapper = (props = { hasMetrics: true }, options = {}) => {
wrapper = mount(Dashboard, {
- propsData: { ...propsData, ...props },
+ propsData: { ...dashboardProps, ...props },
store,
stubs: { 'graph-group': true, 'dashboard-panel': true, 'dashboard-header': DashboardHeader },
...options,
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index b29d86cbc5b..d09fcc92ee7 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -1,14 +1,12 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert, GlIcon } from '@gitlab/ui';
-import waitForPromises from 'helpers/wait_for_promises';
+import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
-import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
-import { dashboardGitResponse } from '../mock_data';
+import { dashboardGitResponse, selfMonitoringDashboardGitResponse } from '../mock_data';
const defaultBranch = 'master';
-
+const modalId = 'duplicateDashboardModalId';
const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred);
const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred);
@@ -32,6 +30,7 @@ describe('DashboardsDropdown', () => {
propsData: {
...props,
defaultBranch,
+ modalId,
},
sync: false,
...storeOpts,
@@ -82,7 +81,7 @@ describe('DashboardsDropdown', () => {
const searchTerm = 'Default';
setSearchTerm(searchTerm);
- return wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick().then(() => {
expect(findItems()).toHaveLength(1);
});
});
@@ -91,7 +90,7 @@ describe('DashboardsDropdown', () => {
const searchTerm = 'does-not-exist';
setSearchTerm(searchTerm);
- return wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick().then(() => {
expect(findNoItemsMsg().isVisible()).toBe(true);
});
});
@@ -151,12 +150,18 @@ describe('DashboardsDropdown', () => {
});
});
- describe('when a system dashboard is selected', () => {
+ 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] = dashboardGitResponse;
+ mockSelectedDashboard = dashboard;
modalDirective = jest.fn();
duplicateDashboardAction = jest.fn().mockResolvedValue();
@@ -172,152 +177,59 @@ describe('DashboardsDropdown', () => {
},
},
);
-
- wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn();
});
- it('displays an item for each dashboard plus a "duplicate dashboard" item', () => {
- const item = wrapper.findAll({ ref: 'duplicateDashboardItem' });
-
+ it('displays a dropdown item for each dashboard', () => {
expect(findItems().length).toEqual(dashboardGitResponse.length + 1);
- expect(item.length).toBe(1);
});
- describe('modal form', () => {
- let okEvent;
-
- const findModal = () => wrapper.find(GlModal);
- const findAlert = () => wrapper.find(GlAlert);
-
- beforeEach(() => {
- okEvent = {
- preventDefault: jest.fn(),
- };
- });
-
- it('exists and contains a form to duplicate a dashboard', () => {
- expect(findModal().exists()).toBe(true);
- expect(findModal().contains(DuplicateDashboardForm)).toBe(true);
- });
-
- it('saves a new dashboard', () => {
- findModal().vm.$emit('ok', okEvent);
-
- return waitForPromises().then(() => {
- expect(okEvent.preventDefault).toHaveBeenCalled();
-
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
- expect(wrapper.emitted().selectDashboard).toBeTruthy();
- expect(findAlert().exists()).toBe(false);
- });
- });
-
- describe('when a new dashboard is saved succesfully', () => {
- const newDashboard = {
- can_edit: true,
- default: false,
- display_name: 'A new dashboard',
- system_dashboard: false,
- };
-
- const submitForm = formVals => {
- duplicateDashboardAction.mockResolvedValueOnce(newDashboard);
- findModal()
- .find(DuplicateDashboardForm)
- .vm.$emit('change', {
- dashboard: 'common_metrics.yml',
- commitMessage: 'A commit message',
- ...formVals,
- });
- findModal().vm.$emit('ok', okEvent);
- };
-
- it('to the default branch, redirects to the new dashboard', () => {
- submitForm({
- branch: defaultBranch,
- });
-
- return waitForPromises().then(() => {
- expect(wrapper.emitted().selectDashboard[0][0]).toEqual(newDashboard);
- });
- });
-
- it('to a new branch refreshes in the current dashboard', () => {
- submitForm({
- branch: 'another-branch',
- });
-
- return waitForPromises().then(() => {
- expect(wrapper.emitted().selectDashboard[0][0]).toEqual(dashboardGitResponse[0]);
- });
- });
- });
-
- it('handles error when a new dashboard is not saved', () => {
- const errMsg = 'An error occurred';
-
- duplicateDashboardAction.mockRejectedValueOnce(errMsg);
- findModal().vm.$emit('ok', okEvent);
+ it('displays one "duplicate dashboard" dropdown item with a directive attached', () => {
+ const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]');
- return waitForPromises().then(() => {
- expect(okEvent.preventDefault).toHaveBeenCalled();
+ expect(item.length).toBe(1);
+ });
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(errMsg);
+ it('"duplicate dashboard" dropdown item directive works', () => {
+ const item = wrapper.find('[data-testid="duplicateDashboardItem"]');
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
- });
- });
+ item.trigger('click');
- 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: findModal().props('modalId'),
- }),
- );
+ return wrapper.vm.$nextTick().then(() => {
+ expect(modalDirective).toHaveBeenCalled();
});
+ });
- it('updates the form on changes', () => {
- const formVals = {
- dashboard: 'common_metrics.yml',
- commitMessage: 'A commit message',
- };
-
- findModal()
- .find(DuplicateDashboardForm)
- .vm.$emit('change', formVals);
+ 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(wrapper.vm.form).toEqual(formVals);
- });
+ // Binding's second argument contains the modal id
+ expect(modalDirective.mock.calls[0][1]).toEqual(
+ expect.objectContaining({
+ value: modalId,
+ }),
+ );
});
});
- describe('when a custom dashboard is selected', () => {
- const findModal = () => wrapper.find(GlModal);
+ const nonDuplicableCases = [dashboardGitResponse[1], selfMonitoringDashboardGitResponse[1]];
- beforeEach(() => {
- wrapper = createComponent({
- selectedDashboard: dashboardGitResponse[1],
+ describe.each(nonDuplicableCases)(
+ 'when the selected dashboard can not be duplicated',
+ dashboard => {
+ beforeEach(() => {
+ mockSelectedDashboard = dashboard;
+
+ wrapper = createComponent();
});
- });
- it('displays an item for each dashboard', () => {
- const item = wrapper.findAll({ ref: 'duplicateDashboardItem' });
+ 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);
- });
-
- it('modal form does not exist and contains a form to duplicate a dashboard', () => {
- expect(findModal().exists()).toBe(false);
- });
- });
+ expect(findItems()).toHaveLength(dashboardGitResponse.length);
+ expect(item.length).toBe(0);
+ });
+ },
+ );
describe('when a dashboard gets selected by the user', () => {
beforeEach(() => {
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
new file mode 100644
index 00000000000..d8ffb4443ac
--- /dev/null
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
@@ -0,0 +1,111 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
+
+import waitForPromises from 'helpers/wait_for_promises';
+
+import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
+import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
+
+import { dashboardGitResponse } from '../mock_data';
+
+describe('duplicate dashboard modal', () => {
+ let wrapper;
+ let mockDashboards;
+ let mockSelectedDashboard;
+ let duplicateDashboardAction;
+ let okEvent;
+
+ function createComponent(opts = {}) {
+ const storeOpts = {
+ methods: {
+ duplicateSystemDashboard: jest.fn(),
+ },
+ computed: {
+ allDashboards: () => mockDashboards,
+ selectedDashboard: () => mockSelectedDashboard,
+ },
+ };
+
+ return shallowMount(DuplicateDashboardModal, {
+ propsData: {
+ defaultBranch: 'master',
+ modalId: 'id',
+ },
+ sync: false,
+ ...storeOpts,
+ ...opts,
+ });
+ }
+
+ const findAlert = () => wrapper.find(GlAlert);
+ const findModal = () => wrapper.find(GlModal);
+ const findDuplicateDashboardForm = () => wrapper.find(DuplicateDashboardForm);
+
+ beforeEach(() => {
+ mockDashboards = dashboardGitResponse;
+ [mockSelectedDashboard] = dashboardGitResponse;
+
+ duplicateDashboardAction = jest.fn().mockResolvedValue();
+
+ okEvent = {
+ preventDefault: jest.fn(),
+ };
+
+ wrapper = createComponent({
+ methods: {
+ // Mock vuex actions
+ duplicateSystemDashboard: duplicateDashboardAction,
+ },
+ });
+
+ wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn();
+ });
+
+ it('contains a form to duplicate a dashboard', () => {
+ expect(findDuplicateDashboardForm().exists()).toBe(true);
+ });
+
+ it('saves a new dashboard', () => {
+ findModal().vm.$emit('ok', okEvent);
+
+ return waitForPromises().then(() => {
+ expect(okEvent.preventDefault).toHaveBeenCalled();
+ expect(wrapper.emitted().dashboardDuplicated).toBeTruthy();
+ expect(wrapper.emitted().dashboardDuplicated[0]).toEqual([dashboardGitResponse[0]]);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
+ it('handles error when a new dashboard is not saved', () => {
+ const errMsg = 'An error occurred';
+
+ duplicateDashboardAction.mockRejectedValueOnce(errMsg);
+ findModal().vm.$emit('ok', okEvent);
+
+ return waitForPromises().then(() => {
+ expect(okEvent.preventDefault).toHaveBeenCalled();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(errMsg);
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
+ });
+ });
+
+ it('updates the form on changes', () => {
+ const formVals = {
+ dashboard: 'common_metrics.yml',
+ commitMessage: 'A commit message',
+ };
+
+ findModal()
+ .find(DuplicateDashboardForm)
+ .vm.$emit('change', formVals);
+
+ // Binding's second argument contains the modal id
+ expect(wrapper.vm.form).toEqual(formVals);
+ });
+});
diff --git a/spec/frontend/monitoring/components/empty_state_spec.js b/spec/frontend/monitoring/components/empty_state_spec.js
index e985e5fb443..abb8b21e9f4 100644
--- a/spec/frontend/monitoring/components/empty_state_spec.js
+++ b/spec/frontend/monitoring/components/empty_state_spec.js
@@ -1,10 +1,11 @@
import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { dashboardEmptyStates } from '~/monitoring/constants';
import EmptyState from '~/monitoring/components/empty_state.vue';
function createComponent(props) {
return shallowMount(EmptyState, {
propsData: {
- ...props,
settingsPath: '/settingsPath',
clustersPath: '/clustersPath',
documentationPath: '/documentationPath',
@@ -13,30 +14,40 @@ function createComponent(props) {
emptyNoDataSvgPath: '/path/to/no-data.svg',
emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
+ ...props,
},
});
}
describe('EmptyState', () => {
+ it('shows loading state with a loading icon', () => {
+ const wrapper = createComponent({
+ selectedState: dashboardEmptyStates.LOADING,
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.find(GlEmptyState).exists()).toBe(false);
+ });
+
it('shows gettingStarted state', () => {
const wrapper = createComponent({
- selectedState: 'gettingStarted',
+ selectedState: dashboardEmptyStates.GETTING_STARTED,
});
expect(wrapper.element).toMatchSnapshot();
});
- it('shows loading state', () => {
+ it('shows unableToConnect state', () => {
const wrapper = createComponent({
- selectedState: 'loading',
+ selectedState: dashboardEmptyStates.UNABLE_TO_CONNECT,
});
expect(wrapper.element).toMatchSnapshot();
});
- it('shows unableToConnect state', () => {
+ it('shows noData state', () => {
const wrapper = createComponent({
- selectedState: 'unableToConnect',
+ selectedState: dashboardEmptyStates.NO_DATA,
});
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index 92829135c0f..81f5d90c310 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -1,13 +1,14 @@
import { shallowMount } from '@vue/test-utils';
import GraphGroup from '~/monitoring/components/graph_group.vue';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
describe('Graph group component', () => {
let wrapper;
const findGroup = () => wrapper.find({ ref: 'graph-group' });
const findContent = () => wrapper.find({ ref: 'graph-group-content' });
- const findCaretIcon = () => wrapper.find(Icon);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findCaretIcon = () => wrapper.find(GlIcon);
const findToggleButton = () => wrapper.find('[data-testid="group-toggle-button"]');
const createComponent = propsData => {
@@ -28,28 +29,28 @@ describe('Graph group component', () => {
});
});
+ it('should not show a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
it('should show the angle-down caret icon', () => {
expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().props('name')).toBe('angle-down');
});
it('should show the angle-right caret icon when the user collapses the group', () => {
- wrapper.vm.collapse();
+ findToggleButton().trigger('click');
- return wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick().then(() => {
expect(findContent().isVisible()).toBe(false);
expect(findCaretIcon().props('name')).toBe('angle-right');
});
});
- it('should contain a tabindex', () => {
- expect(findGroup().contains('[tabindex]')).toBe(true);
- });
-
it('should contain a tab index for the collapse button', () => {
const groupToggle = findToggleButton();
- expect(groupToggle.contains('[tabindex]')).toBe(true);
+ expect(groupToggle.is('[tabindex]')).toBe(true);
});
it('should show the open the group when collapseGroup is set to true', () => {
@@ -57,77 +58,94 @@ describe('Graph group component', () => {
collapseGroup: true,
});
- return wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick().then(() => {
expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().props('name')).toBe('angle-down');
});
});
+ });
- describe('When group is collapsed', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- collapseGroup: true,
- });
+ describe('When group is collapsed', () => {
+ beforeEach(() => {
+ createComponent({
+ name: 'panel',
+ collapseGroup: true,
});
+ });
- it('should show the angle-down caret icon when collapseGroup is true', () => {
- expect(wrapper.vm.caretIcon).toBe('angle-right');
- });
+ it('should show the angle-down caret icon when collapseGroup is true', () => {
+ expect(findCaretIcon().props('name')).toBe('angle-right');
+ });
- it('should show the angle-right caret icon when collapseGroup is false', () => {
- wrapper.vm.collapse();
+ it('should show the angle-right caret icon when collapseGroup is false', () => {
+ findToggleButton().trigger('click');
- expect(wrapper.vm.caretIcon).toBe('angle-down');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCaretIcon().props('name')).toBe('angle-down');
});
+ });
- it('should call collapse the graph group content when enter is pressed on the caret icon', () => {
- const graphGroupContent = findContent();
- const button = findToggleButton();
+ it('should call collapse the graph group content when enter is pressed on the caret icon', () => {
+ const graphGroupContent = findContent();
+ const button = findToggleButton();
- button.trigger('keyup.enter');
+ button.trigger('keyup.enter');
+
+ expect(graphGroupContent.isVisible()).toBe(false);
+ });
+ });
- expect(graphGroupContent.isVisible()).toBe(false);
+ describe('When groups can not be collapsed', () => {
+ beforeEach(() => {
+ createComponent({
+ name: 'panel',
+ showPanels: false,
+ collapseGroup: false,
});
});
- describe('When groups can not be collapsed', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- showPanels: false,
- collapseGroup: false,
- });
+ it('should not have a container when showPanels is false', () => {
+ expect(findGroup().exists()).toBe(false);
+ expect(findContent().exists()).toBe(true);
+ });
+ });
+
+ describe('When group is loading', () => {
+ beforeEach(() => {
+ createComponent({
+ name: 'panel',
+ isLoading: true,
});
+ });
- it('should not have a container when showPanels is false', () => {
- expect(findGroup().exists()).toBe(false);
- expect(findContent().exists()).toBe(true);
+ it('should show a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('When group does not show a panel heading', () => {
+ beforeEach(() => {
+ createComponent({
+ name: 'panel',
+ showPanels: false,
+ collapseGroup: false,
});
});
- describe('When group does not show a panel heading', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- showPanels: false,
- collapseGroup: false,
- });
+ it('should collapse the panel content', () => {
+ expect(findContent().isVisible()).toBe(true);
+ expect(findCaretIcon().exists()).toBe(false);
+ });
+
+ it('should show the panel content when collapse is set to false', () => {
+ wrapper.setProps({
+ collapseGroup: false,
});
- it('should collapse the panel content', () => {
+ return wrapper.vm.$nextTick().then(() => {
expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().exists()).toBe(false);
});
-
- it('should show the panel content when clicked', () => {
- wrapper.vm.collapse();
-
- return wrapper.vm.$nextTick(() => {
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().exists()).toBe(false);
- });
- });
});
});
});
diff --git a/spec/frontend/monitoring/components/links_section_spec.js b/spec/frontend/monitoring/components/links_section_spec.js
index 3b5b72d84ee..b771d63d51f 100644
--- a/spec/frontend/monitoring/components/links_section_spec.js
+++ b/spec/frontend/monitoring/components/links_section_spec.js
@@ -15,7 +15,7 @@ describe('Links Section component', () => {
const setState = links => {
store.state.monitoringDashboard = {
...store.state.monitoringDashboard,
- showEmptyState: false,
+ emptyState: null,
links,
};
};
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
new file mode 100644
index 00000000000..29615638453
--- /dev/null
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -0,0 +1,143 @@
+import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/monitoring/stores';
+import { GlNewDropdown, GlNewDropdownItem, GlButton } from '@gitlab/ui';
+
+import RefreshButton from '~/monitoring/components/refresh_button.vue';
+
+describe('RefreshButton', () => {
+ let wrapper;
+ let store;
+ let dispatch;
+ let documentHidden;
+
+ const createWrapper = () => {
+ wrapper = shallowMount(RefreshButton, { store });
+ };
+
+ const findRefreshBtn = () => wrapper.find(GlButton);
+ const findDropdown = () => wrapper.find(GlNewDropdown);
+ const findOptions = () => findDropdown().findAll(GlNewDropdownItem);
+ const findOptionAt = index => findOptions().at(index);
+
+ const expectFetchDataToHaveBeenCalledTimes = times => {
+ const refreshCalls = dispatch.mock.calls.filter(([action, payload]) => {
+ return action === 'monitoringDashboard/fetchDashboardData' && payload === undefined;
+ });
+ expect(refreshCalls).toHaveLength(times);
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ 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;
+ },
+ });
+
+ createWrapper();
+ });
+
+ afterEach(() => {
+ dispatch.mockReset();
+ wrapper.destroy();
+ });
+
+ it('refreshes data when "refresh" is clicked', () => {
+ findRefreshBtn().vm.$emit('click');
+ expectFetchDataToHaveBeenCalledTimes(1);
+ });
+
+ it('refresh rate is "Off" in the dropdown', () => {
+ expect(findDropdown().props('text')).toBe('Off');
+ });
+
+ describe('refresh rate options', () => {
+ it('presents multiple options', () => {
+ expect(findOptions().length).toBeGreaterThan(1);
+ });
+
+ it('presents an "Off" option as the default option', () => {
+ expect(findOptionAt(0).text()).toBe('Off');
+ expect(findOptionAt(0).props('isChecked')).toBe(true);
+ });
+ });
+
+ describe('when a refresh rate is chosen', () => {
+ const optIndex = 2; // Other option than "Off"
+
+ beforeEach(() => {
+ findOptionAt(optIndex).vm.$emit('click');
+ return wrapper.vm.$nextTick;
+ });
+
+ it('refresh rate appears in the dropdown', () => {
+ expect(findDropdown().props('text')).toBe('10s');
+ });
+
+ it('refresh rate option is checked', () => {
+ expect(findOptionAt(0).props('isChecked')).toBe(false);
+ expect(findOptionAt(optIndex).props('isChecked')).toBe(true);
+ });
+
+ it('refreshes data when a new refresh rate is chosen', () => {
+ expectFetchDataToHaveBeenCalledTimes(1);
+ });
+
+ it('refreshes data after two intervals of time have passed', async () => {
+ jest.runOnlyPendingTimers();
+ expectFetchDataToHaveBeenCalledTimes(2);
+
+ await wrapper.vm.$nextTick();
+
+ jest.runOnlyPendingTimers();
+ expectFetchDataToHaveBeenCalledTimes(3);
+ });
+
+ it('does not refresh data if the document is hidden', async () => {
+ documentHidden = true;
+
+ jest.runOnlyPendingTimers();
+ expectFetchDataToHaveBeenCalledTimes(1);
+
+ await wrapper.vm.$nextTick();
+
+ jest.runOnlyPendingTimers();
+ expectFetchDataToHaveBeenCalledTimes(1);
+ });
+
+ it('data is not refreshed anymore after component is destroyed', () => {
+ expect(jest.getTimerCount()).toBe(1);
+
+ wrapper.destroy();
+
+ expect(jest.getTimerCount()).toBe(0);
+ });
+
+ describe('when "Off" refresh rate is chosen', () => {
+ beforeEach(() => {
+ findOptionAt(0).vm.$emit('click');
+ return wrapper.vm.$nextTick;
+ });
+
+ it('refresh rate is "Off" in the dropdown', () => {
+ expect(findDropdown().props('text')).toBe('Off');
+ });
+
+ it('refresh rate option is appears selected', () => {
+ expect(findOptionAt(0).props('isChecked')).toBe(true);
+ expect(findOptionAt(optIndex).props('isChecked')).toBe(false);
+ });
+
+ it('stops refreshing data', () => {
+ jest.runOnlyPendingTimers();
+ expectFetchDataToHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/variables/custom_variable_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index 5a2b26219b6..cc384aef231 100644
--- a/spec/frontend/monitoring/components/variables/custom_variable_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -1,18 +1,25 @@
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import CustomVariable from '~/monitoring/components/variables/custom_variable.vue';
+import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
describe('Custom variable component', () => {
let wrapper;
- const propsData = {
+
+ const defaultProps = {
name: 'env',
label: 'Select environment',
value: 'Production',
- options: [{ text: 'Production', value: 'prod' }, { text: 'Canary', value: 'canary' }],
+ options: {
+ values: [{ text: 'Production', value: 'prod' }, { text: 'Canary', value: 'canary' }],
+ },
};
- const createShallowWrapper = () => {
- wrapper = shallowMount(CustomVariable, {
- propsData,
+
+ const createShallowWrapper = props => {
+ wrapper = shallowMount(DropdownField, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
});
};
@@ -22,19 +29,25 @@ describe('Custom variable component', () => {
it('renders dropdown element when all necessary props are passed', () => {
createShallowWrapper();
- expect(findDropdown()).toExist();
+ expect(findDropdown().exists()).toBe(true);
});
it('renders dropdown element with a text', () => {
createShallowWrapper();
- expect(findDropdown().attributes('text')).toBe(propsData.value);
+ expect(findDropdown().attributes('text')).toBe(defaultProps.value);
});
it('renders all the dropdown items', () => {
createShallowWrapper();
- expect(findDropdownItems()).toHaveLength(propsData.options.length);
+ expect(findDropdownItems()).toHaveLength(defaultProps.options.values.length);
+ });
+
+ it('renders dropdown when values are missing', () => {
+ createShallowWrapper({ options: {} });
+
+ expect(findDropdown().exists()).toBe(true);
});
it('changing dropdown items triggers update', () => {
@@ -46,7 +59,7 @@ describe('Custom variable component', () => {
.vm.$emit('click');
return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'env', 'canary');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary');
});
});
});
diff --git a/spec/frontend/monitoring/components/variables/text_variable_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js
index f01584ae8bc..99c6facac38 100644
--- a/spec/frontend/monitoring/components/variables/text_variable_spec.js
+++ b/spec/frontend/monitoring/components/variables/text_field_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
-import TextVariable from '~/monitoring/components/variables/text_variable.vue';
+import TextField from '~/monitoring/components/variables/text_field.vue';
describe('Text variable component', () => {
let wrapper;
@@ -10,7 +10,7 @@ describe('Text variable component', () => {
value: 'test-pod',
};
const createShallowWrapper = () => {
- wrapper = shallowMount(TextVariable, {
+ wrapper = shallowMount(TextField, {
propsData,
});
};
@@ -40,7 +40,7 @@ describe('Text variable component', () => {
findInput().trigger('keyup.enter');
return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'prod-pod');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'prod-pod');
});
});
@@ -53,7 +53,7 @@ describe('Text variable component', () => {
findInput().trigger('blur');
return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'canary-pod');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary-pod');
});
});
});
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
index fd814e81c8f..3097906ee68 100644
--- a/spec/frontend/monitoring/components/variables_section_spec.js
+++ b/spec/frontend/monitoring/components/variables_section_spec.js
@@ -1,13 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VariablesSection from '~/monitoring/components/variables_section.vue';
-import CustomVariable from '~/monitoring/components/variables/custom_variable.vue';
-import TextVariable from '~/monitoring/components/variables/text_variable.vue';
+import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
+import TextField from '~/monitoring/components/variables/text_field.vue';
import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import { createStore } from '~/monitoring/stores';
import { convertVariablesForURL } from '~/monitoring/utils';
-import * as types from '~/monitoring/stores/mutation_types';
-import { mockTemplatingDataResponses } from '../mock_data';
+import { storeVariables } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(),
@@ -17,11 +16,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('Metrics dashboard/variables section component', () => {
let store;
let wrapper;
- const sampleVariables = {
- label1: mockTemplatingDataResponses.simpleText.simpleText,
- label2: mockTemplatingDataResponses.advText.advText,
- label3: mockTemplatingDataResponses.simpleCustom.simpleCustom,
- };
const createShallowWrapper = () => {
wrapper = shallowMount(VariablesSection, {
@@ -29,30 +23,41 @@ describe('Metrics dashboard/variables section component', () => {
});
};
- const findTextInput = () => wrapper.findAll(TextVariable);
- const findCustomInput = () => wrapper.findAll(CustomVariable);
+ const findTextInputs = () => wrapper.findAll(TextField);
+ const findCustomInputs = () => wrapper.findAll(DropdownField);
beforeEach(() => {
store = createStore();
- store.state.monitoringDashboard.showEmptyState = false;
+ store.state.monitoringDashboard.emptyState = null;
});
it('does not show the variables section', () => {
createShallowWrapper();
- const allInputs = findTextInput().length + findCustomInput().length;
+ const allInputs = findTextInputs().length + findCustomInputs().length;
expect(allInputs).toBe(0);
});
- it('shows the variables section', () => {
- createShallowWrapper();
- store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
+ describe('when variables are set', () => {
+ beforeEach(() => {
+ store.state.monitoringDashboard.variables = storeVariables;
+ createShallowWrapper();
+
+ return wrapper.vm.$nextTick;
+ });
+
+ it('shows the variables section', () => {
+ const allInputs = findTextInputs().length + findCustomInputs().length;
+
+ expect(allInputs).toBe(storeVariables.length);
+ });
- return wrapper.vm.$nextTick(() => {
- const allInputs = findTextInput().length + findCustomInput().length;
+ it('shows the right custom variable inputs', () => {
+ const customInputs = findCustomInputs();
- expect(allInputs).toBe(Object.keys(sampleVariables).length);
+ expect(customInputs.at(0).props('name')).toBe('customSimple');
+ expect(customInputs.at(1).props('name')).toBe('customAdvanced');
});
});
@@ -65,8 +70,8 @@ describe('Metrics dashboard/variables section component', () => {
monitoringDashboard: {
namespaced: true,
state: {
- showEmptyState: false,
- variables: sampleVariables,
+ emptyState: null,
+ variables: storeVariables,
},
actions: {
updateVariablesAndFetchData,
@@ -79,14 +84,14 @@ describe('Metrics dashboard/variables section component', () => {
});
it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => {
- const firstInput = findTextInput().at(0);
+ const firstInput = findTextInputs().at(0);
- firstInput.vm.$emit('onUpdate', 'label1', 'test');
+ firstInput.vm.$emit('input', 'test');
return wrapper.vm.$nextTick(() => {
expect(updateVariablesAndFetchData).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(
- convertVariablesForURL(sampleVariables),
+ convertVariablesForURL(storeVariables),
window.location.href,
);
expect(updateHistory).toHaveBeenCalled();
@@ -94,14 +99,14 @@ describe('Metrics dashboard/variables section component', () => {
});
it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => {
- const firstInput = findCustomInput().at(0);
+ const firstInput = findCustomInputs().at(0);
- firstInput.vm.$emit('onUpdate', 'label1', 'test');
+ firstInput.vm.$emit('input', 'test');
return wrapper.vm.$nextTick(() => {
expect(updateVariablesAndFetchData).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(
- convertVariablesForURL(sampleVariables),
+ convertVariablesForURL(storeVariables),
window.location.href,
);
expect(updateHistory).toHaveBeenCalled();
@@ -109,9 +114,9 @@ describe('Metrics dashboard/variables section component', () => {
});
it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
- const firstInput = findTextInput().at(0);
+ const firstInput = findTextInputs().at(0);
- firstInput.vm.$emit('onUpdate', 'label1', 'Simple text');
+ firstInput.vm.$emit('input', 'My default value');
expect(updateVariablesAndFetchData).not.toHaveBeenCalled();
expect(mergeUrlParams).not.toHaveBeenCalled();
diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js
index b7b72a15992..97edf7bda74 100644
--- a/spec/frontend/monitoring/fixture_data.js
+++ b/spec/frontend/monitoring/fixture_data.js
@@ -1,5 +1,8 @@
+import { stateAndPropsFromDataset } from '~/monitoring/utils';
import { mapToDashboardViewModel } from '~/monitoring/stores/utils';
import { metricStates } from '~/monitoring/constants';
+import { convertObjectProps } from '~/lib/utils/common_utils';
+import { convertToCamelCase } from '~/lib/utils/text_utility';
import { metricsResult } from './mock_data';
@@ -7,23 +10,54 @@ import { metricsResult } from './mock_data';
export const metricsDashboardResponse = getJSONFixture(
'metrics_dashboard/environment_metrics_dashboard.json',
);
+
export const metricsDashboardPayload = metricsDashboardResponse.dashboard;
+
+const datasetState = stateAndPropsFromDataset(
+ // It's preferable to have props in snake_case, this will be addressed at:
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33574
+ convertObjectProps(
+ // Some props use kebab-case, convert to snake_case first
+ key => convertToCamelCase(key.replace(/-/g, '_')),
+ metricsDashboardResponse.metrics_data,
+ ),
+);
+
+// new properties like addDashboardDocumentationPath prop and alertsEndpoint
+// was recently added to dashboard.vue component this needs to be
+// added to fixtures data
+// 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',
- result: metricsResult,
+ 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',
- result: metricsResult,
+ data: {
+ resultType: 'matrix',
+ result: metricsResult,
+ },
};
export const metricResultEmpty = {
metricId: 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code',
- result: [],
+ data: {
+ resultType: 'matrix',
+ result: [],
+ },
};
// Graph data
diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js
new file mode 100644
index 00000000000..e1b95723f3d
--- /dev/null
+++ b/spec/frontend/monitoring/graph_data.js
@@ -0,0 +1,164 @@
+import { mapPanelToViewModel, normalizeQueryResponseData } from '~/monitoring/stores/utils';
+import { panelTypes, metricStates } from '~/monitoring/constants';
+
+const initTime = 1435781451.781;
+
+const makeValue = val => [initTime, val];
+const makeValues = vals => vals.map((val, i) => [initTime + 15 * i, val]);
+
+// Normalized Prometheus Responses
+
+const scalarResult = ({ value = '1' } = {}) =>
+ normalizeQueryResponseData({
+ resultType: 'scalar',
+ result: makeValue(value),
+ });
+
+const vectorResult = ({ value1 = '1', value2 = '2' } = {}) =>
+ normalizeQueryResponseData({
+ resultType: 'vector',
+ result: [
+ {
+ metric: {
+ __name__: 'up',
+ job: 'prometheus',
+ instance: 'localhost:9090',
+ },
+ value: makeValue(value1),
+ },
+ {
+ metric: {
+ __name__: 'up',
+ job: 'node',
+ instance: 'localhost:9100',
+ },
+ value: makeValue(value2),
+ },
+ ],
+ });
+
+const matrixSingleResult = ({ values = ['1', '2', '3'] } = {}) =>
+ normalizeQueryResponseData({
+ resultType: 'matrix',
+ result: [
+ {
+ metric: {},
+ values: makeValues(values),
+ },
+ ],
+ });
+
+const matrixMultiResult = ({ values1 = ['1', '2', '3'], values2 = ['4', '5', '6'] } = {}) =>
+ normalizeQueryResponseData({
+ 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),
+ },
+ ],
+ });
+
+// GraphData factory
+
+/**
+ * Generate mock graph data according to options
+ *
+ * @param {Object} panelOptions - Panel options as in YML.
+ * @param {Object} dataOptions
+ * @param {Object} dataOptions.metricCount
+ * @param {Object} dataOptions.isMultiSeries
+ */
+export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
+ const { metricCount = 1, isMultiSeries = false } = dataOptions;
+
+ return mapPanelToViewModel({
+ title: 'Time Series Panel',
+ type: panelTypes.LINE_CHART,
+ x_label: 'X Axis',
+ y_label: 'Y Axis',
+ metrics: Array.from(Array(metricCount), (_, i) => ({
+ label: `Metric ${i + 1}`,
+ state: metricStates.OK,
+ result: isMultiSeries ? matrixMultiResult() : matrixSingleResult(),
+ })),
+ ...panelOptions,
+ });
+};
+
+/**
+ * Generate mock graph data according to options
+ *
+ * @param {Object} panelOptions - Panel options as in YML.
+ * @param {Object} dataOptions
+ * @param {Object} dataOptions.unit
+ * @param {Object} dataOptions.value
+ * @param {Object} dataOptions.isVector
+ */
+export const singleStatGraphData = (panelOptions = {}, dataOptions = {}) => {
+ const { unit, value = '1', isVector = false } = dataOptions;
+
+ return mapPanelToViewModel({
+ title: 'Single Stat Panel',
+ type: panelTypes.SINGLE_STAT,
+ metrics: [
+ {
+ label: 'Metric Label',
+ state: metricStates.OK,
+ result: isVector ? vectorResult({ value }) : scalarResult({ value }),
+ unit,
+ },
+ ],
+ ...panelOptions,
+ });
+};
+
+/**
+ * Generate mock graph data according to options
+ *
+ * @param {Object} panelOptions - Panel options as in YML.
+ * @param {Object} dataOptions
+ * @param {Array} dataOptions.values - Metric values
+ * @param {Array} dataOptions.upper - Upper boundary values
+ * @param {Array} dataOptions.lower - Lower boundary values
+ */
+export const anomalyGraphData = (panelOptions = {}, dataOptions = {}) => {
+ const { values, upper, lower } = dataOptions;
+
+ return mapPanelToViewModel({
+ title: 'Anomaly Panel',
+ type: panelTypes.ANOMALY_CHART,
+ x_label: 'X Axis',
+ y_label: 'Y Axis',
+ metrics: [
+ {
+ label: `Metric`,
+ state: metricStates.OK,
+ result: matrixSingleResult({ values }),
+ },
+ {
+ label: `Upper boundary`,
+ state: metricStates.OK,
+ result: matrixSingleResult({ values: upper }),
+ },
+ {
+ label: `Lower boundary`,
+ state: metricStates.OK,
+ result: matrixSingleResult({ values: lower }),
+ },
+ ],
+ ...panelOptions,
+ });
+};
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 05b29e78ecd..49ad33402c6 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -5,28 +5,14 @@ import { TEST_HOST } from '../helpers/test_constants';
export const mockProjectDir = '/frontend-fixtures/environments-project';
export const mockApiEndpoint = `${TEST_HOST}/monitoring/mock`;
-export const propsData = {
- hasMetrics: false,
- documentationPath: '/path/to/docs',
- settingsPath: '/path/to/settings',
- clustersPath: '/path/to/clusters',
- tagsPath: '/path/to/tags',
- defaultBranch: 'master',
- emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
- emptyLoadingSvgPath: '/path/to/loading.svg',
- emptyNoDataSvgPath: '/path/to/no-data.svg',
- emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg',
- emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
- customMetricsAvailable: false,
- customMetricsPath: '',
- validateQueryPath: '',
-};
+export const customDashboardBasePath = '.gitlab/dashboards';
const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({
default: false,
display_name: `Custom Dashboard ${idx}`,
can_edit: true,
system_dashboard: false,
+ out_of_the_box_dashboard: false,
project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`,
path: `.gitlab/dashboards/dashboard_${idx}.yml`,
starred: false,
@@ -65,136 +51,6 @@ export const anomalyDeploymentData = [
},
];
-export const anomalyMockResultValues = {
- noAnomaly: [
- [
- ['2019-08-19T19:00:00.000Z', 1.25],
- ['2019-08-19T20:00:00.000Z', 1.45],
- ['2019-08-19T21:00:00.000Z', 1.55],
- ['2019-08-19T22:00:00.000Z', 1.48],
- ],
- [
- // upper boundary
- ['2019-08-19T19:00:00.000Z', 2],
- ['2019-08-19T20:00:00.000Z', 2.55],
- ['2019-08-19T21:00:00.000Z', 2.65],
- ['2019-08-19T22:00:00.000Z', 3.0],
- ],
- [
- // lower boundary
- ['2019-08-19T19:00:00.000Z', 0.45],
- ['2019-08-19T20:00:00.000Z', 0.65],
- ['2019-08-19T21:00:00.000Z', 0.7],
- ['2019-08-19T22:00:00.000Z', 0.8],
- ],
- ],
- noBoundary: [
- [
- ['2019-08-19T19:00:00.000Z', 1.25],
- ['2019-08-19T20:00:00.000Z', 1.45],
- ['2019-08-19T21:00:00.000Z', 1.55],
- ['2019-08-19T22:00:00.000Z', 1.48],
- ],
- [
- // empty upper boundary
- ],
- [
- // empty lower boundary
- ],
- ],
- oneAnomaly: [
- [
- ['2019-08-19T19:00:00.000Z', 1.25],
- ['2019-08-19T20:00:00.000Z', 3.45], // anomaly
- ['2019-08-19T21:00:00.000Z', 1.55],
- ],
- [
- // upper boundary
- ['2019-08-19T19:00:00.000Z', 2],
- ['2019-08-19T20:00:00.000Z', 2.55],
- ['2019-08-19T21:00:00.000Z', 2.65],
- ],
- [
- // lower boundary
- ['2019-08-19T19:00:00.000Z', 0.45],
- ['2019-08-19T20:00:00.000Z', 0.65],
- ['2019-08-19T21:00:00.000Z', 0.7],
- ],
- ],
- negativeBoundary: [
- [
- ['2019-08-19T19:00:00.000Z', 1.25],
- ['2019-08-19T20:00:00.000Z', 3.45], // anomaly
- ['2019-08-19T21:00:00.000Z', 1.55],
- ],
- [
- // upper boundary
- ['2019-08-19T19:00:00.000Z', 2],
- ['2019-08-19T20:00:00.000Z', 2.55],
- ['2019-08-19T21:00:00.000Z', 2.65],
- ],
- [
- // lower boundary
- ['2019-08-19T19:00:00.000Z', -1.25],
- ['2019-08-19T20:00:00.000Z', -2.65],
- ['2019-08-19T21:00:00.000Z', -3.7], // lowest point
- ],
- ],
-};
-
-export const anomalyMockGraphData = {
- title: 'Requests Per Second Mock Data',
- type: 'anomaly-chart',
- weight: 3,
- metrics: [
- {
- metricId: '90',
- id: 'metric',
- query_range: 'MOCK_PROMETHEUS_METRIC_QUERY_RANGE',
- unit: 'RPS',
- label: 'Metrics RPS',
- metric_id: 90,
- prometheus_endpoint_path: 'MOCK_METRIC_PEP',
- result: [
- {
- metric: {},
- values: [['2019-08-19T19:00:00.000Z', 0]],
- },
- ],
- },
- {
- metricId: '91',
- id: 'upper',
- query_range: '...',
- unit: 'RPS',
- label: 'Upper Limit Metrics RPS',
- metric_id: 91,
- prometheus_endpoint_path: 'MOCK_UPPER_PEP',
- result: [
- {
- metric: {},
- values: [['2019-08-19T19:00:00.000Z', 0]],
- },
- ],
- },
- {
- metricId: '92',
- id: 'lower',
- query_range: '...',
- unit: 'RPS',
- label: 'Lower Limit Metrics RPS',
- metric_id: 92,
- prometheus_endpoint_path: 'MOCK_LOWER_PEP',
- result: [
- {
- metric: {},
- values: [['2019-08-19T19:00:00.000Z', 0]],
- },
- ],
- },
- ],
-};
-
export const deploymentData = [
{
id: 111,
@@ -317,6 +173,7 @@ export const dashboardGitResponse = [
display_name: 'Default',
can_edit: false,
system_dashboard: true,
+ out_of_the_box_dashboard: true,
project_blob_path: null,
path: 'config/prometheus/common_metrics.yml',
starred: false,
@@ -327,6 +184,44 @@ export const dashboardGitResponse = [
display_name: 'dashboard.yml',
can_edit: true,
system_dashboard: false,
+ out_of_the_box_dashboard: false,
+ project_blob_path: `${mockProjectDir}/-/blob/master/.gitlab/dashboards/dashboard.yml`,
+ path: '.gitlab/dashboards/dashboard.yml',
+ starred: true,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
+ },
+ {
+ default: false,
+ display_name: 'Pod Health',
+ can_edit: false,
+ system_dashboard: false,
+ out_of_the_box_dashboard: true,
+ project_blob_path: null,
+ path: 'config/prometheus/pod_metrics.yml',
+ starred: false,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/pod_metrics.yml`,
+ },
+ ...customDashboardsData,
+];
+
+export const selfMonitoringDashboardGitResponse = [
+ {
+ default: true,
+ display_name: 'Default',
+ can_edit: false,
+ system_dashboard: false,
+ out_of_the_box_dashboard: true,
+ project_blob_path: null,
+ path: 'config/prometheus/self_monitoring_default.yml',
+ starred: false,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/self_monitoring_default.yml`,
+ },
+ {
+ default: false,
+ display_name: 'dashboard.yml',
+ can_edit: true,
+ system_dashboard: false,
+ out_of_the_box_dashboard: false,
project_blob_path: `${mockProjectDir}/-/blob/master/.gitlab/dashboards/dashboard.yml`,
path: '.gitlab/dashboards/dashboard.yml',
starred: true,
@@ -349,30 +244,6 @@ export const metricsResult = [
},
];
-export const singleStatMetricsResult = {
- title: 'Super Chart A2',
- type: 'single-stat',
- weight: 2,
- metrics: [
- {
- id: 'metric_a1',
- metricId: '2',
- query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024',
- unit: 'MB',
- label: 'Total Consumption',
- metric_id: 2,
- prometheus_endpoint_path:
- '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
- result: [
- {
- metric: { job: 'prometheus' },
- value: ['2019-06-26T21:03:20.881Z', 91],
- },
- ],
- },
- ],
-};
-
export const graphDataPrometheusQueryRangeMultiTrack = {
title: 'Super Chart A3',
type: 'heatmap',
@@ -641,253 +512,186 @@ export const mockLinks = [
},
];
-const templatingVariableTypes = {
+export const templatingVariablesExamples = {
text: {
- simple: 'Simple text',
- advanced: {
- label: 'Variable 4',
+ textSimple: 'My default value',
+ textAdvanced: {
+ label: 'Advanced text variable',
type: 'text',
options: {
- default_value: 'default',
+ default_value: 'A default value',
},
},
},
custom: {
- simple: ['value1', 'value2', 'value3'],
- advanced: {
- normal: {
- label: 'Advanced Var',
- type: 'custom',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- },
- withoutOpts: {
- type: 'custom',
- options: {},
+ customSimple: ['value1', 'value2', 'value3'],
+ customAdvanced: {
+ label: 'Advanced Var',
+ type: 'custom',
+ options: {
+ values: [
+ { value: 'value1', text: 'Var 1 Option 1' },
+ {
+ value: 'value2',
+ text: 'Var 1 Option 2',
+ default: true,
+ },
+ ],
},
- withoutLabel: {
- type: 'custom',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
+ },
+ customAdvancedWithoutOpts: {
+ type: 'custom',
+ options: {},
+ },
+ customAdvancedWithoutLabel: {
+ type: 'custom',
+ options: {
+ values: [
+ { value: 'value1', text: 'Var 1 Option 1' },
+ {
+ value: 'value2',
+ text: 'Var 1 Option 2',
+ default: true,
+ },
+ ],
},
- withoutType: {
- label: 'Variable 2',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
+ },
+ customAdvancedWithoutType: {
+ label: 'Variable 2',
+ options: {
+ values: [
+ { value: 'value1', text: 'Var 1 Option 1' },
+ {
+ value: 'value2',
+ text: 'Var 1 Option 2',
+ default: true,
+ },
+ ],
},
- withoutOptText: {
- label: 'Options without text',
- type: 'custom',
- options: {
- values: [
- { value: 'value1' },
- {
- value: 'value2',
- default: true,
- },
- ],
- },
+ },
+ customAdvancedWithoutOptText: {
+ label: 'Options without text',
+ type: 'custom',
+ options: {
+ values: [
+ { value: 'value1' },
+ {
+ value: 'value2',
+ default: true,
+ },
+ ],
},
},
},
-};
-
-const generateMockTemplatingData = data => {
- const vars = data
- ? {
- variables: {
- ...data,
- },
- }
- : {};
- return {
- dashboard: {
- templating: vars,
+ metricLabelValues: {
+ metricLabelValuesSimple: {
+ label: 'Metric Label Values',
+ type: 'metric_label_values',
+ options: {
+ prometheus_endpoint_path: '/series',
+ series_selector: 'backend:haproxy_backend_availability:ratio{env="{{env}}"}',
+ label: 'backend',
+ },
},
- };
+ },
};
-const responseForSimpleTextVariable = {
- simpleText: {
- label: 'simpleText',
+export const storeTextVariables = [
+ {
type: 'text',
- value: 'Simple text',
+ name: 'textSimple',
+ label: 'textSimple',
+ value: 'My default value',
},
-};
-
-const responseForAdvTextVariable = {
- advText: {
- label: 'Variable 4',
+ {
type: 'text',
- value: 'default',
+ name: 'textAdvanced',
+ label: 'Advanced text variable',
+ value: 'A default value',
},
-};
+];
-const responseForSimpleCustomVariable = {
- simpleCustom: {
- label: 'simpleCustom',
+export const storeCustomVariables = [
+ {
+ type: 'custom',
+ name: 'customSimple',
+ label: 'customSimple',
+ options: {
+ values: [
+ { default: false, text: 'value1', value: 'value1' },
+ { default: false, text: 'value2', value: 'value2' },
+ { default: false, text: 'value3', value: 'value3' },
+ ],
+ },
value: 'value1',
- options: [
- {
- default: false,
- text: 'value1',
- value: 'value1',
- },
- {
- default: false,
- text: 'value2',
- value: 'value2',
- },
- {
- default: false,
- text: 'value3',
- value: 'value3',
- },
- ],
+ },
+ {
type: 'custom',
+ name: 'customAdvanced',
+ label: 'Advanced Var',
+ options: {
+ values: [
+ { default: false, text: 'Var 1 Option 1', value: 'value1' },
+ { default: true, text: 'Var 1 Option 2', value: 'value2' },
+ ],
+ },
+ value: 'value2',
},
-};
-
-const responseForAdvancedCustomVariableWithoutOptions = {
- advCustomWithoutOpts: {
- label: 'advCustomWithoutOpts',
- options: [],
+ {
type: 'custom',
+ name: 'customAdvancedWithoutOpts',
+ label: 'customAdvancedWithoutOpts',
+ options: { values: [] },
+ value: null,
},
-};
-
-const responseForAdvancedCustomVariableWithoutLabel = {
- advCustomWithoutLabel: {
- label: 'advCustomWithoutLabel',
- value: 'value2',
- options: [
- {
- default: false,
- text: 'Var 1 Option 1',
- value: 'value1',
- },
- {
- default: true,
- text: 'Var 1 Option 2',
- value: 'value2',
- },
- ],
+ {
type: 'custom',
+ name: 'customAdvancedWithoutLabel',
+ label: 'customAdvancedWithoutLabel',
+ value: 'value2',
+ options: {
+ values: [
+ { default: false, text: 'Var 1 Option 1', value: 'value1' },
+ { default: true, text: 'Var 1 Option 2', value: 'value2' },
+ ],
+ },
},
-};
-
-const responseForAdvancedCustomVariableWithoutOptText = {
- advCustomWithoutOptText: {
+ {
+ type: 'custom',
+ name: 'customAdvancedWithoutOptText',
label: 'Options without text',
+ options: {
+ values: [
+ { default: false, text: 'value1', value: 'value1' },
+ { default: true, text: 'value2', value: 'value2' },
+ ],
+ },
value: 'value2',
- options: [
- {
- default: false,
- text: 'value1',
- value: 'value1',
- },
- {
- default: true,
- text: 'value2',
- value: 'value2',
- },
- ],
- type: 'custom',
},
-};
+];
-const responseForAdvancedCustomVariable = {
- ...responseForSimpleCustomVariable,
- advCustomNormal: {
- label: 'Advanced Var',
- value: 'value2',
- options: [
- {
- default: false,
- text: 'Var 1 Option 1',
- value: 'value1',
- },
- {
- default: true,
- text: 'Var 1 Option 2',
- value: 'value2',
- },
- ],
- type: 'custom',
+export const storeMetricLabelValuesVariables = [
+ {
+ type: 'metric_label_values',
+ name: 'metricLabelValuesSimple',
+ label: 'Metric Label Values',
+ options: { prometheusEndpointPath: '/series', label: 'backend', values: [] },
+ value: null,
},
-};
-
-const responsesForAllVariableTypes = {
- ...responseForSimpleTextVariable,
- ...responseForAdvTextVariable,
- ...responseForSimpleCustomVariable,
- ...responseForAdvancedCustomVariable,
-};
+];
-export const mockTemplatingData = {
- emptyTemplatingProp: generateMockTemplatingData(),
- emptyVariablesProp: generateMockTemplatingData({}),
- simpleText: generateMockTemplatingData({ simpleText: templatingVariableTypes.text.simple }),
- advText: generateMockTemplatingData({ advText: templatingVariableTypes.text.advanced }),
- simpleCustom: generateMockTemplatingData({ simpleCustom: templatingVariableTypes.custom.simple }),
- advCustomWithoutOpts: generateMockTemplatingData({
- advCustomWithoutOpts: templatingVariableTypes.custom.advanced.withoutOpts,
- }),
- advCustomWithoutType: generateMockTemplatingData({
- advCustomWithoutType: templatingVariableTypes.custom.advanced.withoutType,
- }),
- advCustomWithoutLabel: generateMockTemplatingData({
- advCustomWithoutLabel: templatingVariableTypes.custom.advanced.withoutLabel,
- }),
- advCustomWithoutOptText: generateMockTemplatingData({
- advCustomWithoutOptText: templatingVariableTypes.custom.advanced.withoutOptText,
- }),
- simpleAndAdv: generateMockTemplatingData({
- simpleCustom: templatingVariableTypes.custom.simple,
- advCustomNormal: templatingVariableTypes.custom.advanced.normal,
- }),
- allVariableTypes: generateMockTemplatingData({
- simpleText: templatingVariableTypes.text.simple,
- advText: templatingVariableTypes.text.advanced,
- simpleCustom: templatingVariableTypes.custom.simple,
- advCustomNormal: templatingVariableTypes.custom.advanced.normal,
- }),
-};
+export const storeVariables = [
+ ...storeTextVariables,
+ ...storeCustomVariables,
+ ...storeMetricLabelValuesVariables,
+];
-export const mockTemplatingDataResponses = {
- emptyTemplatingProp: {},
- emptyVariablesProp: {},
- simpleText: responseForSimpleTextVariable,
- advText: responseForAdvTextVariable,
- simpleCustom: responseForSimpleCustomVariable,
- advCustomWithoutOpts: responseForAdvancedCustomVariableWithoutOptions,
- advCustomWithoutType: {},
- advCustomWithoutLabel: responseForAdvancedCustomVariableWithoutLabel,
- advCustomWithoutOptText: responseForAdvancedCustomVariableWithoutOptText,
- simpleAndAdv: responseForAdvancedCustomVariable,
- allVariableTypes: responsesForAllVariableTypes,
+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',
+ },
};
diff --git a/spec/frontend/monitoring/pages/dashboard_page_spec.js b/spec/frontend/monitoring/pages/dashboard_page_spec.js
index e3c56ef4cbf..675165e9e56 100644
--- a/spec/frontend/monitoring/pages/dashboard_page_spec.js
+++ b/spec/frontend/monitoring/pages/dashboard_page_spec.js
@@ -1,21 +1,42 @@
import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/monitoring/stores';
import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
import Dashboard from '~/monitoring/components/dashboard.vue';
-import { propsData } from '../mock_data';
+import { dashboardProps } from '../fixture_data';
describe('monitoring/pages/dashboard_page', () => {
let wrapper;
+ let store;
+ let $route;
+
+ const buildRouter = () => {
+ const dashboard = {};
+ $route = {
+ params: { dashboard },
+ query: { dashboard },
+ };
+ };
const buildWrapper = (props = {}) => {
wrapper = shallowMount(DashboardPage, {
+ store,
propsData: {
...props,
},
+ mocks: {
+ $route,
+ },
});
};
const findDashboardComponent = () => wrapper.find(Dashboard);
+ beforeEach(() => {
+ buildRouter();
+ store = createStore();
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ });
+
afterEach(() => {
if (wrapper) {
wrapper.destroy();
@@ -28,9 +49,18 @@ describe('monitoring/pages/dashboard_page', () => {
});
it('renders the dashboard page with dashboard component', () => {
- buildWrapper({ dashboardProps: propsData });
+ buildWrapper({ dashboardProps });
+
+ const allProps = {
+ ...dashboardProps,
+ // default props values
+ rearrangePanelsAvailable: false,
+ showHeader: true,
+ showPanels: true,
+ smallEmptyState: false,
+ };
- expect(findDashboardComponent().props()).toMatchObject(propsData);
expect(findDashboardComponent()).toExist();
+ expect(allProps).toMatchObject(findDashboardComponent().props());
});
});
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
new file mode 100644
index 00000000000..5b8f4b3c83e
--- /dev/null
+++ b/spec/frontend/monitoring/router_spec.js
@@ -0,0 +1,81 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import VueRouter from 'vue-router';
+import DashboardPage from '~/monitoring/pages/dashboard_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';
+
+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();
+ localVue.use(VueRouter);
+
+ router = createRouter(basePath);
+ if (routeArg !== undefined) {
+ router.push(routeArg);
+ }
+
+ return mount(DashboardPage, {
+ localVue,
+ store,
+ router,
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ });
+
+ afterEach(() => {
+ window.location.hash = '';
+ });
+
+ describe('support old URL with full dashboard path', () => {
+ it.each`
+ route | 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);
+
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
+ currentDashboard,
+ });
+
+ expect(wrapper.find(Dashboard)).toExist();
+ });
+ });
+
+ describe('supports new URL with short dashboard path', () => {
+ it.each`
+ route | currentDashboard
+ ${'/'} | ${null}
+ ${'/dashboard.yml'} | ${'dashboard.yml'}
+ ${'/folder1/dashboard.yml'} | ${'folder1/dashboard.yml'}
+ ${'/folder1%2Fdashboard.yml'} | ${'folder1/dashboard.yml'}
+ ${'/dashboard.yml'} | ${'dashboard.yml'}
+ ${'/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);
+
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
+ currentDashboard,
+ });
+
+ expect(wrapper.find(Dashboard)).toExist();
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index d0290386f12..22f2b2e3c77 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -6,27 +6,30 @@ import statusCodes from '~/lib/utils/http_status';
import * as commonUtils from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import { defaultTimeRange } from '~/vue_shared/constants';
+import * as getters from '~/monitoring/stores/getters';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import {
+ setGettingStartedEmptyState,
+ setInitialState,
+ setExpandedPanel,
+ clearExpandedPanel,
+ filterEnvironments,
fetchData,
fetchDashboard,
receiveMetricsDashboardSuccess,
+ fetchDashboardData,
+ fetchPrometheusMetric,
fetchDeploymentsData,
fetchEnvironmentsData,
- fetchDashboardData,
fetchAnnotations,
+ fetchDashboardValidationWarnings,
toggleStarredValue,
- fetchPrometheusMetric,
- setInitialState,
- filterEnvironments,
- setExpandedPanel,
- clearExpandedPanel,
- setGettingStartedEmptyState,
duplicateSystemDashboard,
updateVariablesAndFetchData,
+ fetchVariableMetricLabelValues,
} from '~/monitoring/stores/actions';
import {
gqClient,
@@ -35,12 +38,12 @@ import {
} from '~/monitoring/stores/utils';
import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
+import getDashboardValidationWarnings from '~/monitoring/queries/getDashboardValidationWarnings.query.graphql';
import storeState from '~/monitoring/stores/state';
import {
deploymentData,
environmentData,
annotationsData,
- mockTemplatingData,
dashboardGitResponse,
mockDashboardsErrorResponse,
} from '../mock_data';
@@ -59,11 +62,17 @@ describe('Monitoring store actions', () => {
let store;
let state;
+ let dispatch;
+ let commit;
+
beforeEach(() => {
- store = createStore();
+ store = createStore({ getters });
state = store.state.monitoringDashboard;
mock = new MockAdapter(axios);
+ 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));
@@ -78,6 +87,7 @@ describe('Monitoring store actions', () => {
return q;
});
});
+
afterEach(() => {
mock.reset();
@@ -85,377 +95,122 @@ describe('Monitoring store actions', () => {
createFlash.mockReset();
});
- describe('fetchData', () => {
- it('dispatches fetchEnvironmentsData and fetchEnvironmentsData', () => {
- return testAction(
- fetchData,
- null,
- state,
- [],
- [
- { type: 'fetchEnvironmentsData' },
- { type: 'fetchDashboard' },
- { type: 'fetchAnnotations' },
- ],
- );
- });
+ // Setup
- it('dispatches when feature metricsDashboardAnnotations is on', () => {
- const origGon = window.gon;
- window.gon = { features: { metricsDashboardAnnotations: true } };
-
- return testAction(
- fetchData,
+ describe('setGettingStartedEmptyState', () => {
+ it('should commit SET_GETTING_STARTED_EMPTY_STATE mutation', done => {
+ testAction(
+ setGettingStartedEmptyState,
null,
state,
- [],
[
- { type: 'fetchEnvironmentsData' },
- { type: 'fetchDashboard' },
- { type: 'fetchAnnotations' },
+ {
+ type: types.SET_GETTING_STARTED_EMPTY_STATE,
+ },
],
- ).then(() => {
- window.gon = origGon;
- });
- });
- });
-
- describe('fetchDeploymentsData', () => {
- it('dispatches receiveDeploymentsDataSuccess on success', () => {
- state.deploymentsEndpoint = '/success';
- mock.onGet(state.deploymentsEndpoint).reply(200, {
- deployments: deploymentData,
- });
-
- return testAction(
- fetchDeploymentsData,
- null,
- state,
- [],
- [{ type: 'receiveDeploymentsDataSuccess', payload: deploymentData }],
- );
- });
- it('dispatches receiveDeploymentsDataFailure on error', () => {
- state.deploymentsEndpoint = '/error';
- mock.onGet(state.deploymentsEndpoint).reply(500);
-
- return testAction(
- fetchDeploymentsData,
- null,
- state,
[],
- [{ type: 'receiveDeploymentsDataFailure' }],
- () => {
- expect(createFlash).toHaveBeenCalled();
- },
+ done,
);
});
});
- describe('fetchEnvironmentsData', () => {
- beforeEach(() => {
- state.projectPath = 'gitlab-org/gitlab-test';
- });
-
- it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
- jest.spyOn(gqClient, 'mutate').mockReturnValue({
- data: {
- project: {
- data: {
- environments: [],
- },
- },
+ describe('setInitialState', () => {
+ it('should commit SET_INITIAL_STATE mutation', done => {
+ testAction(
+ setInitialState,
+ {
+ currentDashboard: '.gitlab/dashboards/dashboard.yml',
+ deploymentsEndpoint: 'deployments.json',
},
- });
-
- return testAction(
- filterEnvironments,
- {},
state,
[
{
- type: 'SET_ENVIRONMENTS_FILTER',
- payload: {},
- },
- ],
- [
- {
- type: 'fetchEnvironmentsData',
+ type: types.SET_INITIAL_STATE,
+ payload: {
+ currentDashboard: '.gitlab/dashboards/dashboard.yml',
+ deploymentsEndpoint: 'deployments.json',
+ },
},
],
- );
- });
-
- it('fetch environments data call takes in search param', () => {
- const mockMutate = jest.spyOn(gqClient, 'mutate');
- const searchTerm = 'Something';
- const mutationVariables = {
- mutation: getEnvironments,
- variables: {
- projectPath: state.projectPath,
- search: searchTerm,
- states: [ENVIRONMENT_AVAILABLE_STATE],
- },
- };
- state.environmentsSearchTerm = searchTerm;
- mockMutate.mockResolvedValue({});
-
- return testAction(
- fetchEnvironmentsData,
- null,
- state,
[],
- [
- { type: 'requestEnvironmentsData' },
- { type: 'receiveEnvironmentsDataSuccess', payload: [] },
- ],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
+ done,
);
});
+ });
- it('dispatches receiveEnvironmentsDataSuccess on success', () => {
- jest.spyOn(gqClient, 'mutate').mockResolvedValue({
- data: {
- project: {
- data: {
- environments: environmentData,
- },
- },
- },
- });
+ describe('setExpandedPanel', () => {
+ it('Sets a panel as expanded', () => {
+ const group = 'group_1';
+ const panel = { title: 'A Panel' };
return testAction(
- fetchEnvironmentsData,
- null,
+ setExpandedPanel,
+ { group, panel },
state,
+ [{ type: types.SET_EXPANDED_PANEL, payload: { group, panel } }],
[],
- [
- { type: 'requestEnvironmentsData' },
- {
- type: 'receiveEnvironmentsDataSuccess',
- payload: parseEnvironmentsResponse(environmentData, state.projectPath),
- },
- ],
);
});
+ });
- it('dispatches receiveEnvironmentsDataFailure on error', () => {
- jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
-
+ describe('clearExpandedPanel', () => {
+ it('Clears a panel as expanded', () => {
return testAction(
- fetchEnvironmentsData,
- null,
+ clearExpandedPanel,
+ undefined,
state,
+ [{ type: types.SET_EXPANDED_PANEL, payload: { group: null, panel: null } }],
[],
- [{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }],
);
});
});
- describe('fetchAnnotations', () => {
- beforeEach(() => {
- state.timeRange = {
- start: '2020-04-15T12:54:32.137Z',
- end: '2020-08-15T12:54:32.137Z',
- };
- state.projectPath = 'gitlab-org/gitlab-test';
- state.currentEnvironmentName = 'production';
- state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
- });
-
- it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
- const mockMutate = jest.spyOn(gqClient, 'mutate');
- const mutationVariables = {
- mutation: getAnnotations,
- variables: {
- projectPath: state.projectPath,
- environmentName: state.currentEnvironmentName,
- dashboardPath: state.currentDashboard,
- startingFrom: state.timeRange.start,
- },
- };
- const parsedResponse = parseAnnotationsResponse(annotationsData);
-
- mockMutate.mockResolvedValue({
- data: {
- project: {
- environments: {
- nodes: [
- {
- metricsDashboard: {
- annotations: {
- nodes: parsedResponse,
- },
- },
- },
- ],
- },
- },
- },
- });
+ // All Data
+ describe('fetchData', () => {
+ it('dispatches fetchEnvironmentsData and fetchEnvironmentsData', () => {
return testAction(
- fetchAnnotations,
+ fetchData,
null,
state,
[],
- [{ type: 'receiveAnnotationsSuccess', payload: parsedResponse }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
+ [
+ { type: 'fetchEnvironmentsData' },
+ { type: 'fetchDashboard' },
+ { type: 'fetchAnnotations' },
+ ],
);
});
- it('dispatches receiveAnnotationsFailure if the annotations API call fails', () => {
- const mockMutate = jest.spyOn(gqClient, 'mutate');
- const mutationVariables = {
- mutation: getAnnotations,
- variables: {
- projectPath: state.projectPath,
- environmentName: state.currentEnvironmentName,
- dashboardPath: state.currentDashboard,
- startingFrom: state.timeRange.start,
- },
- };
-
- mockMutate.mockRejectedValue({});
+ it('dispatches when feature metricsDashboardAnnotations is on', () => {
+ const origGon = window.gon;
+ window.gon = { features: { metricsDashboardAnnotations: true } };
return testAction(
- fetchAnnotations,
+ fetchData,
null,
state,
[],
- [{ type: 'receiveAnnotationsFailure' }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
- });
-
- describe('Toggles starred value of current dashboard', () => {
- let unstarredDashboard;
- let starredDashboard;
-
- beforeEach(() => {
- state.isUpdatingStarredValue = false;
- [unstarredDashboard, starredDashboard] = dashboardGitResponse;
- });
-
- describe('toggleStarredValue', () => {
- it('performs no changes if no dashboard is selected', () => {
- return testAction(toggleStarredValue, null, state, [], []);
- });
-
- it('performs no changes if already changing starred value', () => {
- state.selectedDashboard = unstarredDashboard;
- state.isUpdatingStarredValue = true;
- return testAction(toggleStarredValue, null, state, [], []);
- });
-
- it('stars dashboard if it is not starred', () => {
- state.selectedDashboard = unstarredDashboard;
- mock.onPost(unstarredDashboard.user_starred_path).reply(200);
-
- return testAction(toggleStarredValue, null, state, [
- { type: types.REQUEST_DASHBOARD_STARRING },
- {
- type: types.RECEIVE_DASHBOARD_STARRING_SUCCESS,
- payload: {
- newStarredValue: true,
- selectedDashboard: unstarredDashboard,
- },
- },
- ]);
- });
-
- it('unstars dashboard if it is starred', () => {
- state.selectedDashboard = starredDashboard;
- mock.onPost(starredDashboard.user_starred_path).reply(200);
-
- return testAction(toggleStarredValue, null, state, [
- { type: types.REQUEST_DASHBOARD_STARRING },
- { type: types.RECEIVE_DASHBOARD_STARRING_FAILURE },
- ]);
- });
- });
- });
-
- describe('Set initial state', () => {
- it('should commit SET_INITIAL_STATE mutation', done => {
- testAction(
- setInitialState,
- {
- currentDashboard: '.gitlab/dashboards/dashboard.yml',
- deploymentsEndpoint: 'deployments.json',
- },
- state,
- [
- {
- type: types.SET_INITIAL_STATE,
- payload: {
- currentDashboard: '.gitlab/dashboards/dashboard.yml',
- deploymentsEndpoint: 'deployments.json',
- },
- },
- ],
- [],
- done,
- );
- });
- });
- describe('Set empty states', () => {
- it('should commit SET_METRICS_ENDPOINT mutation', done => {
- testAction(
- setGettingStartedEmptyState,
- null,
- state,
[
- {
- type: types.SET_GETTING_STARTED_EMPTY_STATE,
- },
+ { type: 'fetchEnvironmentsData' },
+ { type: 'fetchDashboard' },
+ { type: 'fetchAnnotations' },
],
- [],
- done,
- );
+ ).then(() => {
+ window.gon = origGon;
+ });
});
});
- describe('updateVariablesAndFetchData', () => {
- it('should commit UPDATE_VARIABLES mutation and fetch data', done => {
- testAction(
- updateVariablesAndFetchData,
- { pod: 'POD' },
- state,
- [
- {
- type: types.UPDATE_VARIABLES,
- payload: { pod: 'POD' },
- },
- ],
- [
- {
- type: 'fetchDashboardData',
- },
- ],
- done,
- );
- });
- });
+ // Metrics dashboard
describe('fetchDashboard', () => {
- let dispatch;
- let commit;
const response = metricsDashboardResponse;
beforeEach(() => {
- dispatch = jest.fn();
- commit = jest.fn();
state.dashboardEndpoint = '/dashboard';
});
- it('on success, dispatches receive and success actions', () => {
+ it('on success, dispatches receive and success actions, then fetches dashboard warnings', () => {
document.body.dataset.page = 'projects:environments:metrics';
mock.onGet(state.dashboardEndpoint).reply(200, response);
@@ -470,6 +225,7 @@ describe('Monitoring store actions', () => {
type: 'receiveMetricsDashboardSuccess',
payload: { response },
},
+ { type: 'fetchDashboardValidationWarnings' },
],
);
});
@@ -478,9 +234,12 @@ describe('Monitoring store actions', () => {
let result;
beforeEach(() => {
const params = {};
+ const localGetters = {
+ fullDashboardPath: store.getters['monitoringDashboard/fullDashboardPath'],
+ };
result = () => {
mock.onGet(state.dashboardEndpoint).replyOnce(500, mockDashboardsErrorResponse);
- return fetchDashboard({ state, commit, dispatch }, params);
+ return fetchDashboard({ state, commit, dispatch, getters: localGetters }, params);
};
});
@@ -532,15 +291,8 @@ describe('Monitoring store actions', () => {
});
});
});
- describe('receiveMetricsDashboardSuccess', () => {
- let commit;
- let dispatch;
-
- beforeEach(() => {
- commit = jest.fn();
- dispatch = jest.fn();
- });
+ describe('receiveMetricsDashboardSuccess', () => {
it('stores groups', () => {
const response = metricsDashboardResponse;
receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response });
@@ -552,32 +304,6 @@ describe('Monitoring store actions', () => {
expect(dispatch).toHaveBeenCalledWith('fetchDashboardData');
});
- it('stores templating variables', () => {
- const response = {
- ...metricsDashboardResponse.dashboard,
- ...mockTemplatingData.allVariableTypes.dashboard,
- };
-
- receiveMetricsDashboardSuccess(
- { state, commit, dispatch },
- {
- response: {
- ...metricsDashboardResponse,
- dashboard: {
- ...metricsDashboardResponse.dashboard,
- ...mockTemplatingData.allVariableTypes.dashboard,
- },
- },
- },
- );
-
- expect(commit).toHaveBeenCalledWith(
- types.RECEIVE_METRICS_DASHBOARD_SUCCESS,
-
- response,
- );
- });
-
it('sets the dashboards loaded from the repository', () => {
const params = {};
const response = metricsDashboardResponse;
@@ -596,23 +322,21 @@ describe('Monitoring store actions', () => {
expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse);
});
});
- describe('fetchDashboardData', () => {
- let commit;
- let dispatch;
+ // Metrics
+
+ describe('fetchDashboardData', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
- commit = jest.fn();
- dispatch = jest.fn();
state.timeRange = defaultTimeRange;
});
it('commits empty state when state.groups is empty', done => {
- const getters = {
+ const localGetters = {
metricsWithData: () => [],
};
- fetchDashboardData({ state, commit, dispatch, getters })
+ fetchDashboardData({ state, commit, dispatch, getters: localGetters })
.then(() => {
expect(Tracking.event).toHaveBeenCalledWith(
document.body.dataset.page,
@@ -623,25 +347,33 @@ describe('Monitoring store actions', () => {
value: 0,
},
);
- expect(dispatch).toHaveBeenCalledTimes(1);
+ expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
+ expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
+ defaultQueryParams: {
+ start_time: expect.any(String),
+ end_time: expect.any(String),
+ step: expect.any(Number),
+ },
+ });
expect(createFlash).not.toHaveBeenCalled();
done();
})
.catch(done.fail);
});
+
it('dispatches fetchPrometheusMetric for each panel query', done => {
state.dashboard.panelGroups = convertObjectPropsToCamelCase(
metricsDashboardResponse.dashboard.panel_groups,
);
const [metric] = state.dashboard.panelGroups[0].panels[0].metrics;
- const getters = {
+ const localGetters = {
metricsWithData: () => [metric.id],
};
- fetchDashboardData({ state, commit, dispatch, getters })
+ fetchDashboardData({ state, commit, dispatch, getters: localGetters })
.then(() => {
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
metric,
@@ -673,21 +405,27 @@ describe('Monitoring store actions', () => {
const metric = state.dashboard.panelGroups[0].panels[0].metrics[0];
dispatch.mockResolvedValueOnce(); // fetchDeploymentsData
+ dispatch.mockResolvedValueOnce(); // fetchVariableMetricLabelValues
// Mock having one out of four metrics failing
dispatch.mockRejectedValueOnce(new Error('Error fetching this metric'));
dispatch.mockResolvedValue();
fetchDashboardData({ state, commit, dispatch })
.then(() => {
- expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 1); // plus 1 for deployments
+ const defaultQueryParams = {
+ start_time: expect.any(String),
+ end_time: expect.any(String),
+ step: expect.any(Number),
+ };
+
+ expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 2); // plus 1 for deployments
expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
+ expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
+ defaultQueryParams,
+ });
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
metric,
- defaultQueryParams: {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- },
+ defaultQueryParams,
});
expect(createFlash).toHaveBeenCalledTimes(1);
@@ -698,6 +436,7 @@ describe('Monitoring store actions', () => {
done();
});
});
+
describe('fetchPrometheusMetric', () => {
const defaultQueryParams = {
start_time: '2019-08-06T12:40:02.184Z',
@@ -738,7 +477,7 @@ describe('Monitoring store actions', () => {
type: types.RECEIVE_METRIC_RESULT_SUCCESS,
payload: {
metricId: metric.metricId,
- result: data.result,
+ data,
},
},
],
@@ -775,7 +514,7 @@ describe('Monitoring store actions', () => {
type: types.RECEIVE_METRIC_RESULT_SUCCESS,
payload: {
metricId: metric.metricId,
- result: data.result,
+ data,
},
},
],
@@ -817,7 +556,7 @@ describe('Monitoring store actions', () => {
type: types.RECEIVE_METRIC_RESULT_SUCCESS,
payload: {
metricId: metric.metricId,
- result: data.result,
+ data,
},
},
],
@@ -852,7 +591,7 @@ describe('Monitoring store actions', () => {
type: types.RECEIVE_METRIC_RESULT_SUCCESS,
payload: {
metricId: metric.metricId,
- result: data.result,
+ data,
},
},
],
@@ -901,6 +640,402 @@ describe('Monitoring store actions', () => {
});
});
+ // Deployments
+
+ describe('fetchDeploymentsData', () => {
+ it('dispatches receiveDeploymentsDataSuccess on success', () => {
+ state.deploymentsEndpoint = '/success';
+ mock.onGet(state.deploymentsEndpoint).reply(200, {
+ deployments: deploymentData,
+ });
+
+ return testAction(
+ fetchDeploymentsData,
+ null,
+ state,
+ [],
+ [{ type: 'receiveDeploymentsDataSuccess', payload: deploymentData }],
+ );
+ });
+ it('dispatches receiveDeploymentsDataFailure on error', () => {
+ state.deploymentsEndpoint = '/error';
+ mock.onGet(state.deploymentsEndpoint).reply(500);
+
+ return testAction(
+ fetchDeploymentsData,
+ null,
+ state,
+ [],
+ [{ type: 'receiveDeploymentsDataFailure' }],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ },
+ );
+ });
+ });
+
+ // Environments
+
+ describe('fetchEnvironmentsData', () => {
+ beforeEach(() => {
+ state.projectPath = 'gitlab-org/gitlab-test';
+ });
+
+ it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
+ jest.spyOn(gqClient, 'mutate').mockReturnValue({
+ data: {
+ project: {
+ data: {
+ environments: [],
+ },
+ },
+ },
+ });
+
+ return testAction(
+ filterEnvironments,
+ {},
+ state,
+ [
+ {
+ type: 'SET_ENVIRONMENTS_FILTER',
+ payload: {},
+ },
+ ],
+ [
+ {
+ type: 'fetchEnvironmentsData',
+ },
+ ],
+ );
+ });
+
+ it('fetch environments data call takes in search param', () => {
+ const mockMutate = jest.spyOn(gqClient, 'mutate');
+ const searchTerm = 'Something';
+ const mutationVariables = {
+ mutation: getEnvironments,
+ variables: {
+ projectPath: state.projectPath,
+ search: searchTerm,
+ states: [ENVIRONMENT_AVAILABLE_STATE],
+ },
+ };
+ state.environmentsSearchTerm = searchTerm;
+ mockMutate.mockResolvedValue({});
+
+ return testAction(
+ fetchEnvironmentsData,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestEnvironmentsData' },
+ { type: 'receiveEnvironmentsDataSuccess', payload: [] },
+ ],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+
+ it('dispatches receiveEnvironmentsDataSuccess on success', () => {
+ jest.spyOn(gqClient, 'mutate').mockResolvedValue({
+ data: {
+ project: {
+ data: {
+ environments: environmentData,
+ },
+ },
+ },
+ });
+
+ return testAction(
+ fetchEnvironmentsData,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestEnvironmentsData' },
+ {
+ type: 'receiveEnvironmentsDataSuccess',
+ payload: parseEnvironmentsResponse(environmentData, state.projectPath),
+ },
+ ],
+ );
+ });
+
+ it('dispatches receiveEnvironmentsDataFailure on error', () => {
+ jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
+
+ return testAction(
+ fetchEnvironmentsData,
+ null,
+ state,
+ [],
+ [{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }],
+ );
+ });
+ });
+
+ describe('fetchAnnotations', () => {
+ beforeEach(() => {
+ state.timeRange = {
+ start: '2020-04-15T12:54:32.137Z',
+ end: '2020-08-15T12:54:32.137Z',
+ };
+ state.projectPath = 'gitlab-org/gitlab-test';
+ state.currentEnvironmentName = 'production';
+ state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
+ // testAction doesn't have access to getters. The state is passed in as getters
+ // instead of the actual getters inside the testAction method implementation.
+ // All methods downstream that needs access to getters will throw and error.
+ // For that reason, the result of the getter is set as a state variable.
+ state.fullDashboardPath = store.getters['monitoringDashboard/fullDashboardPath'];
+ });
+
+ it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
+ const mockMutate = jest.spyOn(gqClient, 'mutate');
+ const mutationVariables = {
+ mutation: getAnnotations,
+ variables: {
+ projectPath: state.projectPath,
+ environmentName: state.currentEnvironmentName,
+ dashboardPath: state.currentDashboard,
+ startingFrom: state.timeRange.start,
+ },
+ };
+ const parsedResponse = parseAnnotationsResponse(annotationsData);
+
+ mockMutate.mockResolvedValue({
+ data: {
+ project: {
+ environments: {
+ nodes: [
+ {
+ metricsDashboard: {
+ annotations: {
+ nodes: parsedResponse,
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ return testAction(
+ fetchAnnotations,
+ null,
+ state,
+ [],
+ [{ type: 'receiveAnnotationsSuccess', payload: parsedResponse }],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+
+ it('dispatches receiveAnnotationsFailure if the annotations API call fails', () => {
+ const mockMutate = jest.spyOn(gqClient, 'mutate');
+ const mutationVariables = {
+ mutation: getAnnotations,
+ variables: {
+ projectPath: state.projectPath,
+ environmentName: state.currentEnvironmentName,
+ dashboardPath: state.currentDashboard,
+ startingFrom: state.timeRange.start,
+ },
+ };
+
+ mockMutate.mockRejectedValue({});
+
+ return testAction(
+ fetchAnnotations,
+ null,
+ state,
+ [],
+ [{ type: 'receiveAnnotationsFailure' }],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+ });
+
+ describe('fetchDashboardValidationWarnings', () => {
+ let mockMutate;
+ let mutationVariables;
+
+ beforeEach(() => {
+ state.projectPath = 'gitlab-org/gitlab-test';
+ state.currentEnvironmentName = 'production';
+ state.currentDashboard = '.gitlab/dashboards/dashboard_with_warnings.yml';
+ // testAction doesn't have access to getters. The state is passed in as getters
+ // instead of the actual getters inside the testAction method implementation.
+ // All methods downstream that needs access to getters will throw and error.
+ // For that reason, the result of the getter is set as a state variable.
+ state.fullDashboardPath = store.getters['monitoringDashboard/fullDashboardPath'];
+
+ mockMutate = jest.spyOn(gqClient, 'mutate');
+ mutationVariables = {
+ mutation: getDashboardValidationWarnings,
+ variables: {
+ projectPath: state.projectPath,
+ environmentName: state.currentEnvironmentName,
+ dashboardPath: state.fullDashboardPath,
+ },
+ };
+ });
+
+ it('dispatches receiveDashboardValidationWarningsSuccess with true payload when there are warnings', () => {
+ mockMutate.mockResolvedValue({
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/29',
+ environments: {
+ nodes: [
+ {
+ name: 'production',
+ metricsDashboard: {
+ path: '.gitlab/dashboards/dashboard_errors_test.yml',
+ schemaValidationWarnings: ["unit: can't be blank"],
+ },
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ return testAction(
+ fetchDashboardValidationWarnings,
+ null,
+ state,
+ [],
+ [{ type: 'receiveDashboardValidationWarningsSuccess', payload: true }],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+
+ it('dispatches receiveDashboardValidationWarningsSuccess with false payload when there are no warnings', () => {
+ mockMutate.mockResolvedValue({
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/29',
+ environments: {
+ nodes: [
+ {
+ name: 'production',
+ metricsDashboard: {
+ path: '.gitlab/dashboards/dashboard_errors_test.yml',
+ schemaValidationWarnings: [],
+ },
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ return testAction(
+ fetchDashboardValidationWarnings,
+ null,
+ state,
+ [],
+ [{ type: 'receiveDashboardValidationWarningsSuccess', payload: false }],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+
+ it('dispatches receiveDashboardValidationWarningsSuccess with false payload when the response is empty ', () => {
+ mockMutate.mockResolvedValue({
+ data: {
+ project: null,
+ },
+ });
+
+ return testAction(
+ fetchDashboardValidationWarnings,
+ null,
+ state,
+ [],
+ [{ type: 'receiveDashboardValidationWarningsSuccess', payload: false }],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+
+ it('dispatches receiveDashboardValidationWarningsFailure if the warnings API call fails', () => {
+ mockMutate.mockRejectedValue({});
+
+ return testAction(
+ fetchDashboardValidationWarnings,
+ null,
+ state,
+ [],
+ [{ type: 'receiveDashboardValidationWarningsFailure' }],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+ });
+
+ // Dashboard manipulation
+
+ describe('toggleStarredValue', () => {
+ let unstarredDashboard;
+ let starredDashboard;
+
+ beforeEach(() => {
+ state.isUpdatingStarredValue = false;
+ [unstarredDashboard, starredDashboard] = dashboardGitResponse;
+ });
+
+ it('performs no changes if no dashboard is selected', () => {
+ return testAction(toggleStarredValue, null, state, [], []);
+ });
+
+ it('performs no changes if already changing starred value', () => {
+ state.selectedDashboard = unstarredDashboard;
+ state.isUpdatingStarredValue = true;
+ return testAction(toggleStarredValue, null, state, [], []);
+ });
+
+ it('stars dashboard if it is not starred', () => {
+ state.selectedDashboard = unstarredDashboard;
+ mock.onPost(unstarredDashboard.user_starred_path).reply(200);
+
+ return testAction(toggleStarredValue, null, state, [
+ { type: types.REQUEST_DASHBOARD_STARRING },
+ {
+ type: types.RECEIVE_DASHBOARD_STARRING_SUCCESS,
+ payload: {
+ newStarredValue: true,
+ selectedDashboard: unstarredDashboard,
+ },
+ },
+ ]);
+ });
+
+ it('unstars dashboard if it is starred', () => {
+ state.selectedDashboard = starredDashboard;
+ mock.onPost(starredDashboard.user_starred_path).reply(200);
+
+ return testAction(toggleStarredValue, null, state, [
+ { type: types.REQUEST_DASHBOARD_STARRING },
+ { type: types.RECEIVE_DASHBOARD_STARRING_FAILURE },
+ ]);
+ });
+ });
+
describe('duplicateSystemDashboard', () => {
beforeEach(() => {
state.dashboardsEndpoint = '/dashboards.json';
@@ -979,30 +1114,95 @@ describe('Monitoring store actions', () => {
});
});
- describe('setExpandedPanel', () => {
- it('Sets a panel as expanded', () => {
- const group = 'group_1';
- const panel = { title: 'A Panel' };
+ // Variables manipulation
- return testAction(
- setExpandedPanel,
- { group, panel },
+ describe('updateVariablesAndFetchData', () => {
+ it('should commit UPDATE_VARIABLE_VALUE mutation and fetch data', done => {
+ testAction(
+ updateVariablesAndFetchData,
+ { pod: 'POD' },
state,
- [{ type: types.SET_EXPANDED_PANEL, payload: { group, panel } }],
- [],
+ [
+ {
+ type: types.UPDATE_VARIABLE_VALUE,
+ payload: { pod: 'POD' },
+ },
+ ],
+ [
+ {
+ type: 'fetchDashboardData',
+ },
+ ],
+ done,
);
});
});
- describe('clearExpandedPanel', () => {
- it('Clears a panel as expanded', () => {
+ describe('fetchVariableMetricLabelValues', () => {
+ const variable = {
+ type: 'metric_label_values',
+ name: 'label1',
+ options: {
+ prometheusEndpointPath: '/series?match[]=metric_name',
+ label: 'job',
+ },
+ };
+
+ const defaultQueryParams = {
+ start_time: '2019-08-06T12:40:02.184Z',
+ end_time: '2019-08-06T20:40:02.184Z',
+ };
+
+ beforeEach(() => {
+ state = {
+ ...state,
+ timeRange: defaultTimeRange,
+ variables: [variable],
+ };
+ });
+
+ it('should commit UPDATE_VARIABLE_METRIC_LABEL_VALUES mutation and fetch data', () => {
+ const data = [
+ {
+ __name__: 'up',
+ job: 'prometheus',
+ },
+ {
+ __name__: 'up',
+ job: 'POD',
+ },
+ ];
+
+ mock.onGet('/series?match[]=metric_name').reply(200, {
+ status: 'success',
+ data,
+ });
+
return testAction(
- clearExpandedPanel,
- undefined,
+ fetchVariableMetricLabelValues,
+ { defaultQueryParams },
state,
- [{ type: types.SET_EXPANDED_PANEL, payload: { group: null, panel: null } }],
+ [
+ {
+ type: types.UPDATE_VARIABLE_METRIC_LABEL_VALUES,
+ payload: { variable, label: 'job', data },
+ },
+ ],
[],
);
});
+
+ it('should notify the user that dynamic options were not loaded', () => {
+ mock.onGet('/series?match[]=metric_name').reply(500);
+
+ return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
+ () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.stringContaining('error getting options for variable "label1"'),
+ );
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index 933ccb1e46c..a69f5265ea7 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -4,10 +4,11 @@ import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
import {
+ customDashboardBasePath,
environmentData,
metricsResult,
dashboardGitResponse,
- mockTemplatingDataResponses,
+ storeVariables,
mockLinks,
} from '../mock_data';
import {
@@ -27,7 +28,10 @@ describe('Monitoring store Getters', () => {
const { metricId } = state.dashboard.panelGroups[group].panels[panel].metrics[metric];
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, {
metricId,
- result,
+ data: {
+ resultType: 'matrix',
+ result,
+ },
});
};
@@ -340,19 +344,21 @@ describe('Monitoring store Getters', () => {
});
it('transforms the variables object to an array in the [variable, variable_value] format for all variable types', () => {
- mutations[types.SET_VARIABLES](state, mockTemplatingDataResponses.allVariableTypes);
+ state.variables = storeVariables;
const variablesArray = getters.getCustomVariablesParams(state);
expect(variablesArray).toEqual({
- 'variables[advCustomNormal]': 'value2',
- 'variables[advText]': 'default',
- 'variables[simpleCustom]': 'value1',
- 'variables[simpleText]': 'Simple text',
+ 'variables[textSimple]': 'My default value',
+ 'variables[textAdvanced]': 'A default value',
+ 'variables[customSimple]': 'value1',
+ 'variables[customAdvanced]': 'value2',
+ 'variables[customAdvancedWithoutLabel]': 'value2',
+ 'variables[customAdvancedWithoutOptText]': 'value2',
});
});
it('transforms the variables object to an empty array when no keys are present', () => {
- mutations[types.SET_VARIABLES](state, {});
+ state.variables = [];
const variablesArray = getters.getCustomVariablesParams(state);
expect(variablesArray).toEqual({});
@@ -361,45 +367,53 @@ describe('Monitoring store Getters', () => {
describe('selectedDashboard', () => {
const { selectedDashboard } = getters;
+ const localGetters = state => ({
+ fullDashboardPath: getters.fullDashboardPath(state),
+ });
it('returns a dashboard', () => {
const state = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[0].path,
+ customDashboardBasePath,
};
- expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
});
it('returns a non-default dashboard', () => {
const state = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[1].path,
+ customDashboardBasePath,
};
- expect(selectedDashboard(state)).toEqual(dashboardGitResponse[1]);
+ expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[1]);
});
it('returns a default dashboard when no dashboard is selected', () => {
const state = {
allDashboards: dashboardGitResponse,
currentDashboard: null,
+ customDashboardBasePath,
};
- expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
});
it('returns a default dashboard when dashboard cannot be found', () => {
const state = {
allDashboards: dashboardGitResponse,
currentDashboard: 'wrong_path',
+ customDashboardBasePath,
};
- expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
});
it('returns null when no dashboards are present', () => {
const state = {
allDashboards: [],
currentDashboard: dashboardGitResponse[0].path,
+ customDashboardBasePath,
};
- expect(selectedDashboard(state)).toEqual(null);
+ expect(selectedDashboard(state, localGetters(state))).toEqual(null);
});
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 0283f1a86a4..14b38d79aa2 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -3,9 +3,9 @@ import httpStatusCodes from '~/lib/utils/http_status';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state';
-import { metricStates } from '~/monitoring/constants';
+import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
-import { deploymentData, dashboardGitResponse } from '../mock_data';
+import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data';
import { metricsDashboardPayload } from '../fixture_data';
describe('Monitoring mutations', () => {
@@ -15,6 +15,14 @@ describe('Monitoring mutations', () => {
stateCopy = state();
});
+ describe('REQUEST_METRICS_DASHBOARD', () => {
+ it('sets an empty loading state', () => {
+ mutations[types.REQUEST_METRICS_DASHBOARD](stateCopy);
+
+ expect(stateCopy.emptyState).toBe(dashboardEmptyStates.LOADING);
+ });
+ });
+
describe('RECEIVE_METRICS_DASHBOARD_SUCCESS', () => {
let payload;
const getGroups = () => stateCopy.dashboard.panelGroups;
@@ -23,6 +31,18 @@ describe('Monitoring mutations', () => {
stateCopy.dashboard.panelGroups = [];
payload = metricsDashboardPayload;
});
+ it('sets an empty noData state when the dashboard is empty', () => {
+ const emptyDashboardPayload = {
+ ...payload,
+ panel_groups: [],
+ };
+
+ mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, emptyDashboardPayload);
+ const groups = getGroups();
+
+ expect(groups).toEqual([]);
+ expect(stateCopy.emptyState).toBe(dashboardEmptyStates.NO_DATA);
+ });
it('adds a key to the group', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload);
const groups = getGroups();
@@ -72,6 +92,20 @@ describe('Monitoring mutations', () => {
});
});
+ describe('RECEIVE_METRICS_DASHBOARD_FAILURE', () => {
+ it('sets an empty noData state when an empty error occurs', () => {
+ mutations[types.RECEIVE_METRICS_DASHBOARD_FAILURE](stateCopy);
+
+ expect(stateCopy.emptyState).toBe(dashboardEmptyStates.NO_DATA);
+ });
+
+ it('sets an empty unableToConnect state when an error occurs', () => {
+ mutations[types.RECEIVE_METRICS_DASHBOARD_FAILURE](stateCopy, 'myerror');
+
+ expect(stateCopy.emptyState).toBe(dashboardEmptyStates.UNABLE_TO_CONNECT);
+ });
+ });
+
describe('Dashboard starring mutations', () => {
it('REQUEST_DASHBOARD_STARRING', () => {
stateCopy = { isUpdatingStarredValue: false };
@@ -225,11 +259,28 @@ describe('Monitoring mutations', () => {
describe('Individual panel/metric results', () => {
const metricId = 'NO_DB_response_metrics_nginx_ingress_throughput_status_code';
- const result = [
- {
- values: [[0, 1], [1, 1], [1, 3]],
- },
- ];
+ 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];
@@ -238,13 +289,10 @@ describe('Monitoring mutations', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard);
});
it('stores a loading state on a metric', () => {
- expect(stateCopy.showEmptyState).toBe(true);
-
mutations[types.REQUEST_METRIC_RESULT](stateCopy, {
metricId,
});
- expect(stateCopy.showEmptyState).toBe(true);
expect(getMetric()).toEqual(
expect.objectContaining({
loading: true,
@@ -257,26 +305,16 @@ describe('Monitoring mutations', () => {
beforeEach(() => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard);
});
- it('clears empty state', () => {
- expect(stateCopy.showEmptyState).toBe(true);
-
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
- metricId,
- result,
- });
-
- expect(stateCopy.showEmptyState).toBe(false);
- });
it('adds results to the store', () => {
expect(getMetric().result).toBe(null);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
metricId,
- result,
+ data,
});
- expect(getMetric().result).toHaveLength(result.length);
+ expect(getMetric().result).toHaveLength(data.result.length);
expect(getMetric()).toEqual(
expect.objectContaining({
loading: false,
@@ -290,16 +328,6 @@ describe('Monitoring mutations', () => {
beforeEach(() => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard);
});
- it('maintains the loading state when a metric fails', () => {
- expect(stateCopy.showEmptyState).toBe(true);
-
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
- metricId,
- error: 'an error',
- });
-
- expect(stateCopy.showEmptyState).toBe(true);
- });
it('stores a timeout error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
@@ -369,6 +397,7 @@ describe('Monitoring mutations', () => {
});
});
});
+
describe('SET_ALL_DASHBOARDS', () => {
it('stores `undefined` dashboards as an empty array', () => {
mutations[types.SET_ALL_DASHBOARDS](stateCopy, undefined);
@@ -410,30 +439,53 @@ describe('Monitoring mutations', () => {
});
});
- describe('SET_VARIABLES', () => {
- it('stores an empty variables array when no custom variables are given', () => {
- mutations[types.SET_VARIABLES](stateCopy, {});
-
- expect(stateCopy.variables).toEqual({});
- });
-
- it('stores variables in the key key_value format in the array', () => {
- mutations[types.SET_VARIABLES](stateCopy, { pod: 'POD', stage: 'main ops' });
+ describe('UPDATE_VARIABLE_VALUE', () => {
+ it('updates only the value of the variable in variables', () => {
+ stateCopy.variables = storeTextVariables;
+ mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, { name: 'textSimple', value: 'New Value' });
- expect(stateCopy.variables).toEqual({ pod: 'POD', stage: 'main ops' });
+ expect(stateCopy.variables[0].value).toEqual('New Value');
});
});
- describe('UPDATE_VARIABLES', () => {
- afterEach(() => {
- mutations[types.SET_VARIABLES](stateCopy, {});
- });
-
- it('updates only the value of the variable in variables', () => {
- mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } });
- mutations[types.UPDATE_VARIABLES](stateCopy, { key: 'environment', value: 'new prod' });
+ describe('UPDATE_VARIABLE_METRIC_LABEL_VALUES', () => {
+ it('updates options in a variable', () => {
+ const data = [
+ {
+ __name__: 'up',
+ job: 'prometheus',
+ env: 'prd',
+ },
+ {
+ __name__: 'up',
+ job: 'prometheus',
+ env: 'stg',
+ },
+ {
+ __name__: 'up',
+ job: 'node',
+ env: 'prod',
+ },
+ {
+ __name__: 'up',
+ job: 'node',
+ env: 'stg',
+ },
+ ];
+
+ const variable = {
+ options: {},
+ };
+
+ mutations[types.UPDATE_VARIABLE_METRIC_LABEL_VALUES](stateCopy, {
+ variable,
+ label: 'job',
+ data,
+ });
- expect(stateCopy.variables).toEqual({ environment: { value: 'new prod', type: 'text' } });
+ expect(variable.options).toEqual({
+ values: [{ text: 'prometheus', value: 'prometheus' }, { text: 'node', value: 'node' }],
+ });
});
});
});
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 2dea40585f1..b97948fa1bf 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -5,9 +5,10 @@ import {
parseAnnotationsResponse,
removeLeadingSlash,
mapToDashboardViewModel,
- normalizeQueryResult,
+ normalizeQueryResponseData,
convertToGrafanaTimeRange,
addDashboardMetaDataToLink,
+ normalizeCustomDashboardPath,
} from '~/monitoring/stores/utils';
import * as urlUtils from '~/lib/utils/url_utility';
import { annotationsData } from '../mock_data';
@@ -21,7 +22,7 @@ describe('mapToDashboardViewModel', () => {
dashboard: '',
panelGroups: [],
links: [],
- variables: {},
+ variables: [],
});
});
@@ -51,7 +52,7 @@ describe('mapToDashboardViewModel', () => {
expect(mapToDashboardViewModel(response)).toEqual({
dashboard: 'Dashboard Name',
links: [],
- variables: {},
+ variables: [],
panelGroups: [
{
group: 'Group 1',
@@ -423,22 +424,20 @@ describe('mapToDashboardViewModel', () => {
urlUtils.queryToObject.mockReturnValueOnce();
- expect(mapToDashboardViewModel(response)).toMatchObject({
- dashboard: 'Dashboard Name',
- links: [],
- variables: {
- pod: {
- label: 'pod',
- type: 'text',
- value: 'kubernetes',
- },
- pod_2: {
- label: 'pod_2',
- type: 'text',
- value: 'kubernetes-2',
- },
+ expect(mapToDashboardViewModel(response).variables).toEqual([
+ {
+ name: 'pod',
+ label: 'pod',
+ type: 'text',
+ value: 'kubernetes',
},
- });
+ {
+ name: 'pod_2',
+ label: 'pod_2',
+ type: 'text',
+ value: 'kubernetes-2',
+ },
+ ]);
});
it('sets variables as-is from yml file if URL has no matching variables', () => {
@@ -457,22 +456,20 @@ describe('mapToDashboardViewModel', () => {
'var-environment': 'POD',
});
- expect(mapToDashboardViewModel(response)).toMatchObject({
- dashboard: 'Dashboard Name',
- links: [],
- variables: {
- pod: {
- label: 'pod',
- type: 'text',
- value: 'kubernetes',
- },
- pod_2: {
- label: 'pod_2',
- type: 'text',
- value: 'kubernetes-2',
- },
+ expect(mapToDashboardViewModel(response).variables).toEqual([
+ {
+ label: 'pod',
+ name: 'pod',
+ type: 'text',
+ value: 'kubernetes',
},
- });
+ {
+ label: 'pod_2',
+ name: 'pod_2',
+ type: 'text',
+ value: 'kubernetes-2',
+ },
+ ]);
});
it('merges variables from URL with the ones from yml file', () => {
@@ -493,44 +490,20 @@ describe('mapToDashboardViewModel', () => {
'var-pod_2': 'POD2',
});
- expect(mapToDashboardViewModel(response)).toMatchObject({
- dashboard: 'Dashboard Name',
- links: [],
- variables: {
- pod: {
- label: 'pod',
- type: 'text',
- value: 'POD1',
- },
- pod_2: {
- label: 'pod_2',
- type: 'text',
- value: 'POD2',
- },
+ expect(mapToDashboardViewModel(response).variables).toEqual([
+ {
+ label: 'pod',
+ name: 'pod',
+ type: 'text',
+ value: 'POD1',
},
- });
- });
- });
-});
-
-describe('normalizeQueryResult', () => {
- const testData = {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: [[1435781430.781, '1'], [1435781445.781, '1'], [1435781460.781, '1']],
- };
-
- it('processes a simple matrix result', () => {
- expect(normalizeQueryResult(testData)).toEqual({
- metric: { __name__: 'up', job: 'prometheus', instance: 'localhost:9090' },
- values: [
- ['2015-07-01T20:10:30.781Z', 1],
- ['2015-07-01T20:10:45.781Z', 1],
- ['2015-07-01T20:11:00.781Z', 1],
- ],
+ {
+ label: 'pod_2',
+ name: 'pod_2',
+ type: 'text',
+ value: 'POD2',
+ },
+ ]);
});
});
});
@@ -720,3 +693,187 @@ describe('user-defined links utils', () => {
});
});
});
+
+describe('normalizeQueryResponseData', () => {
+ // Data examples from
+ // https://prometheus.io/docs/prometheus/latest/querying/api/#expression-queries
+
+ it('processes a string result', () => {
+ const mockScalar = {
+ resultType: 'string',
+ result: [1435781451.781, '1'],
+ };
+
+ expect(normalizeQueryResponseData(mockScalar)).toEqual([
+ {
+ metric: {},
+ value: ['2015-07-01T20:10:51.781Z', '1'],
+ values: [['2015-07-01T20:10:51.781Z', '1']],
+ },
+ ]);
+ });
+
+ it('processes a scalar result', () => {
+ const mockScalar = {
+ resultType: 'scalar',
+ result: [1435781451.781, '1'],
+ };
+
+ expect(normalizeQueryResponseData(mockScalar)).toEqual([
+ {
+ metric: {},
+ value: ['2015-07-01T20:10:51.781Z', 1],
+ values: [['2015-07-01T20:10:51.781Z', 1]],
+ },
+ ]);
+ });
+
+ it('processes a vector result', () => {
+ const mockVector = {
+ resultType: 'vector',
+ result: [
+ {
+ metric: {
+ __name__: 'up',
+ job: 'prometheus',
+ instance: 'localhost:9090',
+ },
+ value: [1435781451.781, '1'],
+ },
+ {
+ metric: {
+ __name__: 'up',
+ job: 'node',
+ instance: 'localhost:9100',
+ },
+ value: [1435781451.781, '0'],
+ },
+ ],
+ };
+
+ expect(normalizeQueryResponseData(mockVector)).toEqual([
+ {
+ metric: { __name__: 'up', job: 'prometheus', instance: 'localhost:9090' },
+ value: ['2015-07-01T20:10:51.781Z', 1],
+ values: [['2015-07-01T20:10:51.781Z', 1]],
+ },
+ {
+ metric: { __name__: 'up', job: 'node', instance: 'localhost:9100' },
+ value: ['2015-07-01T20:10:51.781Z', 0],
+ values: [['2015-07-01T20:10:51.781Z', 0]],
+ },
+ ]);
+ });
+
+ it('processes a matrix result', () => {
+ const mockMatrix = {
+ resultType: 'matrix',
+ result: [
+ {
+ metric: {
+ __name__: 'up',
+ job: 'prometheus',
+ instance: 'localhost:9090',
+ },
+ values: [[1435781430.781, '1'], [1435781445.781, '2'], [1435781460.781, '3']],
+ },
+ {
+ metric: {
+ __name__: 'up',
+ job: 'node',
+ instance: 'localhost:9091',
+ },
+ values: [[1435781430.781, '4'], [1435781445.781, '5'], [1435781460.781, '6']],
+ },
+ ],
+ };
+
+ expect(normalizeQueryResponseData(mockMatrix)).toEqual([
+ {
+ metric: { __name__: 'up', instance: 'localhost:9090', job: 'prometheus' },
+ value: ['2015-07-01T20:11:00.781Z', 3],
+ values: [
+ ['2015-07-01T20:10:30.781Z', 1],
+ ['2015-07-01T20:10:45.781Z', 2],
+ ['2015-07-01T20:11:00.781Z', 3],
+ ],
+ },
+ {
+ metric: { __name__: 'up', instance: 'localhost:9091', job: 'node' },
+ value: ['2015-07-01T20:11:00.781Z', 6],
+ values: [
+ ['2015-07-01T20:10:30.781Z', 4],
+ ['2015-07-01T20:10:45.781Z', 5],
+ ['2015-07-01T20:11:00.781Z', 6],
+ ],
+ },
+ ]);
+ });
+
+ it('processes a scalar result with a NaN result', () => {
+ // Queries may return "NaN" string values.
+ // e.g. when Prometheus cannot find a metric the query
+ // `scalar(does_not_exist)` will return a "NaN" value.
+
+ const mockScalar = {
+ resultType: 'scalar',
+ result: [1435781451.781, 'NaN'],
+ };
+
+ expect(normalizeQueryResponseData(mockScalar)).toEqual([
+ {
+ metric: {},
+ value: ['2015-07-01T20:10:51.781Z', NaN],
+ values: [['2015-07-01T20:10:51.781Z', NaN]],
+ },
+ ]);
+ });
+
+ it('processes a matrix result with a "NaN" value', () => {
+ // Queries may return "NaN" string values.
+ const mockMatrix = {
+ resultType: 'matrix',
+ result: [
+ {
+ metric: {
+ __name__: 'up',
+ job: 'prometheus',
+ instance: 'localhost:9090',
+ },
+ values: [[1435781430.781, '1'], [1435781460.781, 'NaN']],
+ },
+ ],
+ };
+
+ expect(normalizeQueryResponseData(mockMatrix)).toEqual([
+ {
+ metric: { __name__: 'up', instance: 'localhost:9090', job: 'prometheus' },
+ value: ['2015-07-01T20:11:00.781Z', NaN],
+ values: [['2015-07-01T20:10:30.781Z', 1], ['2015-07-01T20:11:00.781Z', NaN]],
+ },
+ ]);
+ });
+});
+
+describe('normalizeCustomDashboardPath', () => {
+ it.each`
+ input | expected
+ ${[undefined]} | ${''}
+ ${[null]} | ${''}
+ ${[]} | ${''}
+ ${['links.yml']} | ${'links.yml'}
+ ${['links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/links.yml'}
+ ${['config/prometheus/common_metrics.yml']} | ${'config/prometheus/common_metrics.yml'}
+ ${['config/prometheus/common_metrics.yml', '.gitlab/dashboards']} | ${'config/prometheus/common_metrics.yml'}
+ ${['dir1/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/links.yml'}
+ ${['dir1/dir2/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/dir2/links.yml'}
+ ${['.gitlab/dashboards/links.yml']} | ${'.gitlab/dashboards/links.yml'}
+ ${['.gitlab/dashboards/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/links.yml'}
+ ${['.gitlab/dashboards/dir1/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/links.yml'}
+ ${['.gitlab/dashboards/dir1/dir2/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/dir2/links.yml'}
+ ${['config/prometheus/pod_metrics.yml', '.gitlab/dashboards']} | ${'config/prometheus/pod_metrics.yml'}
+ ${['config/prometheus/pod_metrics.yml']} | ${'config/prometheus/pod_metrics.yml'}
+ `(`normalizeCustomDashboardPath returns $expected for $input`, ({ input, expected }) => {
+ expect(normalizeCustomDashboardPath(...input)).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js
index 5164ed1b54b..de124b0313c 100644
--- a/spec/frontend/monitoring/store/variable_mapping_spec.js
+++ b/spec/frontend/monitoring/store/variable_mapping_spec.js
@@ -1,94 +1,209 @@
-import { parseTemplatingVariables, mergeURLVariables } from '~/monitoring/stores/variable_mapping';
+import {
+ parseTemplatingVariables,
+ mergeURLVariables,
+ optionsFromSeriesData,
+} from '~/monitoring/stores/variable_mapping';
+import {
+ templatingVariablesExamples,
+ storeTextVariables,
+ storeCustomVariables,
+ storeMetricLabelValuesVariables,
+} from '../mock_data';
import * as urlUtils from '~/lib/utils/url_utility';
-import { mockTemplatingData, mockTemplatingDataResponses } from '../mock_data';
-
-describe('parseTemplatingVariables', () => {
- it.each`
- case | input | expected
- ${'Returns empty object for no dashboard input'} | ${{}} | ${{}}
- ${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}}
- ${'Returns empty object for empty templating prop'} | ${mockTemplatingData.emptyTemplatingProp} | ${{}}
- ${'Returns empty object for empty variables prop'} | ${mockTemplatingData.emptyVariablesProp} | ${{}}
- ${'Returns parsed object for simple text variable'} | ${mockTemplatingData.simpleText} | ${mockTemplatingDataResponses.simpleText}
- ${'Returns parsed object for advanced text variable'} | ${mockTemplatingData.advText} | ${mockTemplatingDataResponses.advText}
- ${'Returns parsed object for simple custom variable'} | ${mockTemplatingData.simpleCustom} | ${mockTemplatingDataResponses.simpleCustom}
- ${'Returns parsed object for advanced custom variable without options'} | ${mockTemplatingData.advCustomWithoutOpts} | ${mockTemplatingDataResponses.advCustomWithoutOpts}
- ${'Returns parsed object for advanced custom variable for option without text'} | ${mockTemplatingData.advCustomWithoutOptText} | ${mockTemplatingDataResponses.advCustomWithoutOptText}
- ${'Returns parsed object for advanced custom variable without type'} | ${mockTemplatingData.advCustomWithoutType} | ${{}}
- ${'Returns parsed object for advanced custom variable without label'} | ${mockTemplatingData.advCustomWithoutLabel} | ${mockTemplatingDataResponses.advCustomWithoutLabel}
- ${'Returns parsed object for simple and advanced custom variables'} | ${mockTemplatingData.simpleAndAdv} | ${mockTemplatingDataResponses.simpleAndAdv}
- ${'Returns parsed object for all variable types'} | ${mockTemplatingData.allVariableTypes} | ${mockTemplatingDataResponses.allVariableTypes}
- `('$case', ({ input, expected }) => {
- expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected);
- });
-});
-describe('mergeURLVariables', () => {
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
+describe('Monitoring variable mapping', () => {
+ describe('parseTemplatingVariables', () => {
+ it.each`
+ case | input
+ ${'For undefined templating object'} | ${undefined}
+ ${'For empty templating object'} | ${{}}
+ `('$case, returns an empty array', ({ input }) => {
+ expect(parseTemplatingVariables(input)).toEqual([]);
+ });
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
+ it.each`
+ case | input | output
+ ${'Returns parsed object for text variables'} | ${templatingVariablesExamples.text} | ${storeTextVariables}
+ ${'Returns parsed object for custom variables'} | ${templatingVariablesExamples.custom} | ${storeCustomVariables}
+ ${'Returns parsed object for metric label value variables'} | ${templatingVariablesExamples.metricLabelValues} | ${storeMetricLabelValuesVariables}
+ `('$case, returns an empty array', ({ input, output }) => {
+ expect(parseTemplatingVariables(input)).toEqual(output);
+ });
});
- it('returns empty object if variables are not defined in yml or URL', () => {
- urlUtils.queryToObject.mockReturnValueOnce({});
+ describe('mergeURLVariables', () => {
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'queryToObject');
+ });
- expect(mergeURLVariables({})).toEqual({});
- });
+ afterEach(() => {
+ urlUtils.queryToObject.mockRestore();
+ });
- it('returns empty object if variables are defined in URL but not in yml', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- 'var-env': 'one',
- 'var-instance': 'localhost',
+ it('returns empty object if variables are not defined in yml or URL', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({});
+
+ expect(mergeURLVariables([])).toEqual([]);
});
- expect(mergeURLVariables({})).toEqual({});
- });
+ it('returns empty object if variables are defined in URL but not in yml', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({
+ 'var-env': 'one',
+ 'var-instance': 'localhost',
+ });
- it('returns yml variables if variables defined in yml but not in the URL', () => {
- urlUtils.queryToObject.mockReturnValueOnce({});
+ expect(mergeURLVariables([])).toEqual([]);
+ });
- const params = {
- env: 'one',
- instance: 'localhost',
- };
+ it('returns yml variables if variables defined in yml but not in the URL', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({});
+
+ const variables = [
+ {
+ name: 'env',
+ value: 'one',
+ },
+ {
+ name: 'instance',
+ value: 'localhost',
+ },
+ ];
+
+ expect(mergeURLVariables(variables)).toEqual(variables);
+ });
- expect(mergeURLVariables(params)).toEqual(params);
- });
+ it('returns yml variables if variables defined in URL do not match with yml variables', () => {
+ const urlParams = {
+ 'var-env': 'one',
+ 'var-instance': 'localhost',
+ };
+ const variables = [
+ {
+ name: 'env',
+ value: 'one',
+ },
+ {
+ name: 'service',
+ value: 'database',
+ },
+ ];
+ urlUtils.queryToObject.mockReturnValueOnce(urlParams);
+
+ expect(mergeURLVariables(variables)).toEqual(variables);
+ });
- it('returns yml variables if variables defined in URL do not match with yml variables', () => {
- const urlParams = {
- 'var-env': 'one',
- 'var-instance': 'localhost',
- };
- const ymlParams = {
- pod: { value: 'one' },
- service: { value: 'database' },
- };
- urlUtils.queryToObject.mockReturnValueOnce(urlParams);
-
- expect(mergeURLVariables(ymlParams)).toEqual(ymlParams);
+ it('returns merged yml and URL variables if there is some match', () => {
+ const urlParams = {
+ 'var-env': 'one',
+ 'var-instance': 'localhost:8080',
+ };
+ const variables = [
+ {
+ name: 'instance',
+ value: 'localhost',
+ },
+ {
+ name: 'service',
+ value: 'database',
+ },
+ ];
+
+ urlUtils.queryToObject.mockReturnValueOnce(urlParams);
+
+ expect(mergeURLVariables(variables)).toEqual([
+ {
+ name: 'instance',
+ value: 'localhost:8080',
+ },
+ {
+ name: 'service',
+ value: 'database',
+ },
+ ]);
+ });
});
- it('returns merged yml and URL variables if there is some match', () => {
- const urlParams = {
- 'var-env': 'one',
- 'var-instance': 'localhost:8080',
- };
- const ymlParams = {
- instance: { value: 'localhost' },
- service: { value: 'database' },
- };
+ describe('optionsFromSeriesData', () => {
+ it('fetches the label values from missing data', () => {
+ expect(optionsFromSeriesData({ label: 'job' })).toEqual([]);
+ });
- const merged = {
- instance: { value: 'localhost:8080' },
- service: { value: 'database' },
- };
+ it('fetches the label values from a simple series', () => {
+ const data = [
+ {
+ __name__: 'up',
+ job: 'job1',
+ },
+ {
+ __name__: 'up',
+ job: 'job2',
+ },
+ ];
+
+ expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
+ { text: 'job1', value: 'job1' },
+ { text: 'job2', value: 'job2' },
+ ]);
+ });
- urlUtils.queryToObject.mockReturnValueOnce(urlParams);
+ it('fetches the label values from multiple series', () => {
+ const data = [
+ {
+ __name__: 'up',
+ job: 'job1',
+ instance: 'host1',
+ },
+ {
+ __name__: 'up',
+ job: 'job2',
+ instance: 'host1',
+ },
+ {
+ __name__: 'up',
+ job: 'job1',
+ instance: 'host2',
+ },
+ {
+ __name__: 'up',
+ job: 'job2',
+ instance: 'host2',
+ },
+ ];
+
+ expect(optionsFromSeriesData({ label: '__name__', data })).toEqual([
+ { text: 'up', value: 'up' },
+ ]);
+
+ expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
+ { text: 'job1', value: 'job1' },
+ { text: 'job2', value: 'job2' },
+ ]);
+
+ expect(optionsFromSeriesData({ label: 'instance', data })).toEqual([
+ { text: 'host1', value: 'host1' },
+ { text: 'host2', value: 'host2' },
+ ]);
+ });
- expect(mergeURLVariables(ymlParams)).toEqual(merged);
+ it('fetches the label values from a series with missing values', () => {
+ const data = [
+ {
+ __name__: 'up',
+ job: 'job1',
+ },
+ {
+ __name__: 'up',
+ job: 'job2',
+ },
+ {
+ __name__: 'up',
+ },
+ ];
+
+ expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
+ { text: 'job1', value: 'job1' },
+ { text: 'job2', value: 'job2' },
+ ]);
+ });
});
});
diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js
index eb2578aa9db..6c8267e6a3c 100644
--- a/spec/frontend/monitoring/store_utils.js
+++ b/spec/frontend/monitoring/store_utils.js
@@ -8,7 +8,10 @@ export const setMetricResult = ({ store, result, group = 0, panel = 0, metric =
store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, {
metricId,
- result,
+ data: {
+ resultType: 'matrix',
+ result,
+ },
});
};
@@ -32,12 +35,6 @@ export const setupStoreWithDashboard = store => {
);
};
-export const setupStoreWithVariable = store => {
- store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, {
- label1: 'pod',
- });
-};
-
export const setupStoreWithLinks = store => {
store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, {
...metricsDashboardPayload,
@@ -60,3 +57,24 @@ export const setupStoreWithData = store => {
setEnvironmentData(store);
};
+
+export const setupStoreWithDataForPanelCount = (store, panelCount) => {
+ const payloadPanelGroup = metricsDashboardPayload.panel_groups[0];
+
+ const panelGroupCustom = {
+ ...payloadPanelGroup,
+ panels: payloadPanelGroup.panels.slice(0, panelCount),
+ };
+
+ const metricsDashboardPayloadCustom = {
+ ...metricsDashboardPayload,
+ panel_groups: [panelGroupCustom],
+ };
+
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
+ metricsDashboardPayloadCustom,
+ );
+
+ setMetricResult({ store, result: metricsResult, panel: 0 });
+};
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index 039cf275eea..35ca6ba9b52 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -1,12 +1,8 @@
import * as monitoringUtils from '~/monitoring/utils';
import * as urlUtils from '~/lib/utils/url_utility';
import { TEST_HOST } from 'jest/helpers/test_constants';
-import {
- mockProjectDir,
- singleStatMetricsResult,
- anomalyMockGraphData,
- barMockData,
-} from './mock_data';
+import { mockProjectDir, barMockData } from './mock_data';
+import { singleStatGraphData, anomalyGraphData } from './graph_data';
import { metricsDashboardViewModel, graphData } from './fixture_data';
const mockPath = `${TEST_HOST}${mockProjectDir}/-/environments/29/metrics`;
@@ -82,7 +78,7 @@ describe('monitoring/utils', () => {
it('validates data with the query format', () => {
const validGraphData = monitoringUtils.graphDataValidatorForValues(
true,
- singleStatMetricsResult,
+ singleStatGraphData(),
);
expect(validGraphData).toBe(true);
@@ -105,13 +101,13 @@ describe('monitoring/utils', () => {
let threeMetrics;
let fourMetrics;
beforeEach(() => {
- oneMetric = singleStatMetricsResult;
- threeMetrics = anomalyMockGraphData;
+ oneMetric = singleStatGraphData();
+ threeMetrics = anomalyGraphData();
const metrics = [...threeMetrics.metrics];
metrics.push(threeMetrics.metrics[0]);
fourMetrics = {
- ...anomalyMockGraphData,
+ ...anomalyGraphData(),
metrics,
};
});
@@ -429,14 +425,41 @@ describe('monitoring/utils', () => {
describe('convertVariablesForURL', () => {
it.each`
- input | expected
- ${undefined} | ${{}}
- ${null} | ${{}}
- ${{}} | ${{}}
- ${{ env: { value: 'prod' } }} | ${{ 'var-env': 'prod' }}
- ${{ 'var-env': { value: 'prod' } }} | ${{ 'var-var-env': 'prod' }}
+ input | expected
+ ${[]} | ${{}}
+ ${[{ name: 'env', value: 'prod' }]} | ${{ 'var-env': 'prod' }}
+ ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${{ 'var-env1': 'prod' }}
+ ${[{ name: 'var-env', value: 'prod' }]} | ${{ 'var-var-env': 'prod' }}
`('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => {
expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected);
});
});
+
+ describe('setCustomVariablesFromUrl', () => {
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'updateHistory');
+ });
+
+ afterEach(() => {
+ urlUtils.updateHistory.mockRestore();
+ });
+
+ it.each`
+ input | urlParams
+ ${[]} | ${''}
+ ${[{ name: 'env', value: 'prod' }]} | ${'?var-env=prod'}
+ ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${'?var-env=prod&var-env1=prod'}
+ `(
+ 'setCustomVariablesFromUrl updates history with query "$urlParams" with input $input',
+ ({ input, urlParams }) => {
+ monitoringUtils.setCustomVariablesFromUrl(input);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledTimes(1);
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/${urlParams}`,
+ title: '',
+ });
+ },
+ );
+ });
});