summaryrefslogtreecommitdiff
path: root/spec/frontend/monitoring
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/monitoring')
-rw-r--r--spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap43
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js422
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap25
-rw-r--r--spec/frontend/monitoring/components/alert_widget_form_spec.js220
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js14
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js43
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js (renamed from spec/frontend/monitoring/components/panel_type_spec.js)266
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js549
-rw-r--r--spec/frontend/monitoring/components/dashboard_template_spec.js15
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js127
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js26
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js10
-rw-r--r--spec/frontend/monitoring/components/variables/custom_variable_spec.js52
-rw-r--r--spec/frontend/monitoring/components/variables/text_variable_spec.js59
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js126
-rw-r--r--spec/frontend/monitoring/mock_data.js231
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js180
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js84
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js92
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js6
-rw-r--r--spec/frontend/monitoring/store/variable_mapping_spec.js22
-rw-r--r--spec/frontend/monitoring/store_utils.js43
-rw-r--r--spec/frontend/monitoring/stubs/modal_stub.js11
-rw-r--r--spec/frontend/monitoring/utils_spec.js302
-rw-r--r--spec/frontend/monitoring/validators_spec.js80
26 files changed, 2767 insertions, 283 deletions
diff --git a/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap b/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
new file mode 100644
index 00000000000..2179e7b4ab5
--- /dev/null
+++ b/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AlertWidget Alert firing displays a warning icon and matches snapshot 1`] = `
+<gl-badge-stub
+ class="d-flex-center text-truncate"
+ pill=""
+ variant="danger"
+>
+ <gl-icon-stub
+ class="flex-shrink-0"
+ name="warning"
+ size="16"
+ />
+
+ <span
+ class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me"
+ >
+ Firing:
+ alert-label &gt; 42
+
+ </span>
+</gl-badge-stub>
+`;
+
+exports[`AlertWidget Alert not firing displays a warning icon and matches snapshot 1`] = `
+<gl-badge-stub
+ class="d-flex-center text-truncate"
+ pill=""
+ variant="secondary"
+>
+ <gl-icon-stub
+ class="flex-shrink-0"
+ name="warning"
+ size="16"
+ />
+
+ <span
+ class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me"
+ >
+ alert-label &gt; 42
+ </span>
+</gl-badge-stub>
+`;
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
new file mode 100644
index 00000000000..f0355dfa01b
--- /dev/null
+++ b/spec/frontend/monitoring/alert_widget_spec.js
@@ -0,0 +1,422 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
+import AlertWidget from '~/monitoring/components/alert_widget.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+
+const mockReadAlert = jest.fn();
+const mockCreateAlert = jest.fn();
+const mockUpdateAlert = jest.fn();
+const mockDeleteAlert = jest.fn();
+
+jest.mock('~/flash');
+jest.mock(
+ '~/monitoring/services/alerts_service',
+ () =>
+ function AlertsServiceMock() {
+ return {
+ readAlert: mockReadAlert,
+ createAlert: mockCreateAlert,
+ updateAlert: mockUpdateAlert,
+ deleteAlert: mockDeleteAlert,
+ };
+ },
+);
+
+describe('AlertWidget', () => {
+ let wrapper;
+
+ const nonFiringAlertResult = [
+ {
+ values: [[0, 1], [1, 42], [2, 41]],
+ },
+ ];
+ const firingAlertResult = [
+ {
+ values: [[0, 42], [1, 43], [2, 44]],
+ },
+ ];
+ const metricId = '5';
+ const alertPath = 'my/alert.json';
+
+ const relevantQueries = [
+ {
+ metricId,
+ label: 'alert-label',
+ alert_path: alertPath,
+ result: nonFiringAlertResult,
+ },
+ ];
+
+ const firingRelevantQueries = [
+ {
+ metricId,
+ label: 'alert-label',
+ alert_path: alertPath,
+ result: firingAlertResult,
+ },
+ ];
+
+ const defaultProps = {
+ alertsEndpoint: '',
+ relevantQueries,
+ alertsToManage: {},
+ modalId: 'alert-modal-1',
+ };
+
+ const propsWithAlert = {
+ relevantQueries,
+ };
+
+ const propsWithAlertData = {
+ relevantQueries,
+ alertsToManage: {
+ [alertPath]: { operator: '>', threshold: 42, alert_path: alertPath, metricId },
+ },
+ };
+
+ const createComponent = propsData => {
+ wrapper = shallowMount(AlertWidget, {
+ stubs: { GlTooltip, GlSprintf },
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ });
+ };
+ const hasLoadingIcon = () => wrapper.contains(GlLoadingIcon);
+ const findWidgetForm = () => wrapper.find({ ref: 'widgetForm' });
+ const findAlertErrorMessage = () => wrapper.find({ ref: 'alertErrorMessage' });
+ const findCurrentSettingsText = () =>
+ wrapper
+ .find({ ref: 'alertCurrentSetting' })
+ .text()
+ .replace(/\s\s+/g, ' ');
+ const findBadge = () => wrapper.find(GlBadge);
+ const findTooltip = () => wrapper.find(GlTooltip);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('displays a loading spinner and disables form when fetching alerts', () => {
+ let resolveReadAlert;
+ mockReadAlert.mockReturnValue(
+ new Promise(resolve => {
+ resolveReadAlert = resolve;
+ }),
+ );
+ createComponent(defaultProps);
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(hasLoadingIcon()).toBe(true);
+ expect(findWidgetForm().props('disabled')).toBe(true);
+
+ resolveReadAlert({ operator: '==', threshold: 42 });
+ })
+ .then(() => waitForPromises())
+ .then(() => {
+ expect(hasLoadingIcon()).toBe(false);
+ expect(findWidgetForm().props('disabled')).toBe(false);
+ });
+ });
+
+ it('does not render loading spinner if showLoadingState is false', () => {
+ let resolveReadAlert;
+ mockReadAlert.mockReturnValue(
+ new Promise(resolve => {
+ resolveReadAlert = resolve;
+ }),
+ );
+ createComponent({
+ ...defaultProps,
+ showLoadingState: false,
+ });
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+
+ resolveReadAlert({ operator: '==', threshold: 42 });
+ })
+ .then(() => waitForPromises())
+ .then(() => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+ });
+
+ it('displays an error message when fetch fails', () => {
+ mockReadAlert.mockRejectedValue();
+ createComponent(propsWithAlert);
+ expect(hasLoadingIcon()).toBe(true);
+
+ return waitForPromises().then(() => {
+ expect(createFlash).toHaveBeenCalled();
+ expect(hasLoadingIcon()).toBe(false);
+ });
+ });
+
+ describe('Alert not firing', () => {
+ it('displays a warning icon and matches snapshot', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ createComponent(propsWithAlertData);
+
+ return waitForPromises().then(() => {
+ expect(findBadge().element).toMatchSnapshot();
+ });
+ });
+
+ it('displays an alert summary when there is a single alert', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ createComponent(propsWithAlertData);
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toEqual('alert-label > 42');
+ });
+ });
+
+ it('displays a combined alert summary when there are multiple alerts', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const propsWithManyAlerts = {
+ relevantQueries: [
+ ...relevantQueries,
+ ...[
+ {
+ metricId: '6',
+ alert_path: 'my/alert2.json',
+ label: 'alert-label2',
+ result: [{ values: [] }],
+ },
+ ],
+ ],
+ alertsToManage: {
+ 'my/alert.json': {
+ operator: '>',
+ threshold: 42,
+ alert_path: alertPath,
+ metricId,
+ },
+ 'my/alert2.json': {
+ operator: '==',
+ threshold: 900,
+ alert_path: 'my/alert2.json',
+ metricId: '6',
+ },
+ },
+ };
+ createComponent(propsWithManyAlerts);
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toContain('2 alerts applied');
+ });
+ });
+ });
+
+ describe('Alert firing', () => {
+ it('displays a warning icon and matches snapshot', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ propsWithAlertData.relevantQueries = firingRelevantQueries;
+ createComponent(propsWithAlertData);
+
+ return waitForPromises().then(() => {
+ expect(findBadge().element).toMatchSnapshot();
+ });
+ });
+
+ it('displays an alert summary when there is a single alert', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ propsWithAlertData.relevantQueries = firingRelevantQueries;
+ createComponent(propsWithAlertData);
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toEqual('Firing: alert-label > 42');
+ });
+ });
+
+ it('displays a combined alert summary when there are multiple alerts', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const propsWithManyAlerts = {
+ relevantQueries: [
+ ...firingRelevantQueries,
+ ...[
+ {
+ metricId: '6',
+ alert_path: 'my/alert2.json',
+ label: 'alert-label2',
+ result: [{ values: [] }],
+ },
+ ],
+ ],
+ alertsToManage: {
+ 'my/alert.json': {
+ operator: '>',
+ threshold: 42,
+ alert_path: alertPath,
+ metricId,
+ },
+ 'my/alert2.json': {
+ operator: '==',
+ threshold: 900,
+ alert_path: 'my/alert2.json',
+ metricId: '6',
+ },
+ },
+ };
+ createComponent(propsWithManyAlerts);
+
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toContain('2 alerts applied, 1 firing');
+ });
+ });
+
+ it('should display tooltip with thresholds summary', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const propsWithManyAlerts = {
+ relevantQueries: [
+ ...firingRelevantQueries,
+ ...[
+ {
+ metricId: '6',
+ alert_path: 'my/alert2.json',
+ label: 'alert-label2',
+ result: [{ values: [] }],
+ },
+ ],
+ ],
+ alertsToManage: {
+ 'my/alert.json': {
+ operator: '>',
+ threshold: 42,
+ alert_path: alertPath,
+ metricId,
+ },
+ 'my/alert2.json': {
+ operator: '==',
+ threshold: 900,
+ alert_path: 'my/alert2.json',
+ metricId: '6',
+ },
+ },
+ };
+ createComponent(propsWithManyAlerts);
+
+ return waitForPromises().then(() => {
+ expect(
+ findTooltip()
+ .text()
+ .replace(/\s\s+/g, ' '),
+ ).toEqual('Firing: alert-label > 42');
+ });
+ });
+ });
+
+ it('creates an alert with an appropriate handler', () => {
+ const alertParams = {
+ operator: '<',
+ threshold: 4,
+ prometheus_metric_id: '5',
+ };
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const fakeAlertPath = 'foo/bar';
+ mockCreateAlert.mockResolvedValue({ alert_path: fakeAlertPath, ...alertParams });
+ createComponent({
+ alertsToManage: {
+ [fakeAlertPath]: {
+ alert_path: fakeAlertPath,
+ operator: '<',
+ threshold: 4,
+ prometheus_metric_id: '5',
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('create', alertParams);
+
+ expect(mockCreateAlert).toHaveBeenCalledWith(alertParams);
+ });
+
+ it('updates an alert with an appropriate handler', () => {
+ const alertParams = { operator: '<', threshold: 4, alert_path: alertPath };
+ const newAlertParams = { operator: '==', threshold: 12 };
+ mockReadAlert.mockResolvedValue(alertParams);
+ mockUpdateAlert.mockResolvedValue({ ...alertParams, ...newAlertParams });
+ createComponent({
+ ...propsWithAlertData,
+ alertsToManage: {
+ [alertPath]: {
+ alert_path: alertPath,
+ operator: '==',
+ threshold: 12,
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('update', {
+ alert: alertPath,
+ ...newAlertParams,
+ prometheus_metric_id: '5',
+ });
+
+ expect(mockUpdateAlert).toHaveBeenCalledWith(alertPath, newAlertParams);
+ });
+
+ it('deletes an alert with an appropriate handler', () => {
+ const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
+ mockReadAlert.mockResolvedValue(alertParams);
+ mockDeleteAlert.mockResolvedValue({});
+ createComponent({
+ ...propsWithAlert,
+ alertsToManage: {
+ [alertPath]: {
+ alert_path: alertPath,
+ operator: '>',
+ threshold: 42,
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('delete', { alert: alertPath });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(mockDeleteAlert).toHaveBeenCalledWith(alertPath);
+ expect(findAlertErrorMessage().exists()).toBe(false);
+ });
+ });
+
+ describe('when delete fails', () => {
+ beforeEach(() => {
+ const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
+ mockReadAlert.mockResolvedValue(alertParams);
+ mockDeleteAlert.mockRejectedValue();
+
+ createComponent({
+ ...propsWithAlert,
+ alertsToManage: {
+ [alertPath]: {
+ alert_path: alertPath,
+ operator: '>',
+ threshold: 42,
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('delete', { alert: alertPath });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows error message', () => {
+ expect(findAlertErrorMessage().text()).toEqual('Error deleting alert');
+ });
+
+ it('dismisses error message on cancel', () => {
+ findWidgetForm().vm.$emit('cancel');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findAlertErrorMessage().exists()).toBe(false);
+ });
+ });
+ });
+});
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 1906ad7c6ed..9be5fa72110 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -16,7 +16,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master"
id="monitor-dashboards-dropdown"
- selecteddashboard="[object Object]"
toggle-class="dropdown-menu-toggle"
/>
</div>
@@ -72,7 +71,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<date-time-picker-stub
class="flex-grow-1 show-last-dropdown"
customenabled="true"
- data-qa-selector="show_last_dropdown"
+ data-qa-selector="range_picker_dropdown"
options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
value="[object Object]"
/>
@@ -101,6 +100,26 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="d-sm-flex"
>
+ <div
+ class="mb-2 mr-2 d-flex"
+ >
+ <div
+ class="flex-grow-1"
+ title="Star dashboard"
+ >
+ <gl-deprecated-button-stub
+ class="w-100"
+ size="md"
+ variant="default"
+ >
+ <gl-icon-stub
+ name="star-o"
+ size="16"
+ />
+ </gl-deprecated-button-stub>
+ </div>
+ </div>
+
<!---->
<!---->
@@ -111,6 +130,8 @@ exports[`Dashboard template matches the default snapshot 1`] = `
</div>
</div>
+ <!---->
+
<empty-state-stub
clusterspath="/path/to/clusters"
documentationpath="/path/to/docs"
diff --git a/spec/frontend/monitoring/components/alert_widget_form_spec.js b/spec/frontend/monitoring/components/alert_widget_form_spec.js
new file mode 100644
index 00000000000..a8416216a94
--- /dev/null
+++ b/spec/frontend/monitoring/components/alert_widget_form_spec.js
@@ -0,0 +1,220 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import AlertWidgetForm from '~/monitoring/components/alert_widget_form.vue';
+import ModalStub from '../stubs/modal_stub';
+
+describe('AlertWidgetForm', () => {
+ let wrapper;
+
+ const metricId = '8';
+ const alertPath = 'alert';
+ const relevantQueries = [{ metricId, alert_path: alertPath, label: 'alert-label' }];
+ const dataTrackingOptions = {
+ create: { action: 'click_button', label: 'create_alert' },
+ delete: { action: 'click_button', label: 'delete_alert' },
+ update: { action: 'click_button', label: 'update_alert' },
+ };
+
+ const defaultProps = {
+ disabled: false,
+ relevantQueries,
+ modalId: 'alert-modal-1',
+ };
+
+ const propsWithAlertData = {
+ ...defaultProps,
+ alertsToManage: {
+ alert: { alert_path: alertPath, operator: '<', threshold: 5, metricId },
+ },
+ configuredAlert: metricId,
+ };
+
+ function createComponent(props = {}) {
+ const propsData = {
+ ...defaultProps,
+ ...props,
+ };
+
+ wrapper = shallowMount(AlertWidgetForm, {
+ propsData,
+ stubs: {
+ GlModal: ModalStub,
+ },
+ });
+ }
+
+ const modal = () => wrapper.find(ModalStub);
+ const modalTitle = () => modal().attributes('title');
+ const submitButton = () => modal().find(GlLink);
+ const submitButtonTrackingOpts = () =>
+ JSON.parse(submitButton().attributes('data-tracking-options'));
+ const e = {
+ preventDefault: jest.fn(),
+ };
+
+ beforeEach(() => {
+ e.preventDefault.mockReset();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ it('disables the form when disabled prop is set', () => {
+ createComponent({ disabled: true });
+
+ expect(modal().attributes('ok-disabled')).toBe('true');
+ });
+
+ it('disables the form if no query is selected', () => {
+ createComponent();
+
+ expect(modal().attributes('ok-disabled')).toBe('true');
+ });
+
+ it('shows correct title and button text', () => {
+ expect(modalTitle()).toBe('Add alert');
+ expect(submitButton().text()).toBe('Add');
+ });
+
+ it('sets tracking options for create alert', () => {
+ expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.create);
+ });
+
+ it('emits a "create" event when form submitted without existing alert', () => {
+ createComponent();
+
+ wrapper.vm.selectQuery('9');
+ wrapper.setData({
+ threshold: 900,
+ });
+
+ wrapper.vm.handleSubmit(e);
+
+ expect(wrapper.emitted().create[0]).toEqual([
+ {
+ alert: undefined,
+ operator: '>',
+ threshold: 900,
+ prometheus_metric_id: '9',
+ },
+ ]);
+ expect(e.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('resets form when modal is dismissed (hidden)', () => {
+ createComponent();
+
+ wrapper.vm.selectQuery('9');
+ wrapper.vm.selectQuery('>');
+ wrapper.setData({
+ threshold: 800,
+ });
+
+ modal().vm.$emit('hidden');
+
+ expect(wrapper.vm.selectedAlert).toEqual({});
+ expect(wrapper.vm.operator).toBe(null);
+ expect(wrapper.vm.threshold).toBe(null);
+ expect(wrapper.vm.prometheusMetricId).toBe(null);
+ });
+
+ it('sets selectedAlert to the provided configuredAlert on modal show', () => {
+ createComponent(propsWithAlertData);
+
+ modal().vm.$emit('shown');
+
+ expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
+ });
+
+ it('sets selectedAlert to the first relevantQueries if there is only one option on modal show', () => {
+ createComponent({
+ ...propsWithAlertData,
+ configuredAlert: '',
+ });
+
+ modal().vm.$emit('shown');
+
+ expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
+ });
+
+ it('does not set selectedAlert to the first relevantQueries if there is more than one option on modal show', () => {
+ createComponent({
+ relevantQueries: [
+ {
+ metricId: '8',
+ alertPath: 'alert',
+ label: 'alert-label',
+ },
+ {
+ metricId: '9',
+ alertPath: 'alert',
+ label: 'alert-label',
+ },
+ ],
+ });
+
+ modal().vm.$emit('shown');
+
+ expect(wrapper.vm.selectedAlert).toEqual({});
+ });
+
+ describe('with existing alert', () => {
+ beforeEach(() => {
+ createComponent(propsWithAlertData);
+
+ wrapper.vm.selectQuery(metricId);
+ });
+
+ it('sets tracking options for delete alert', () => {
+ expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.delete);
+ });
+
+ it('updates button text', () => {
+ expect(modalTitle()).toBe('Edit alert');
+ expect(submitButton().text()).toBe('Delete');
+ });
+
+ it('emits "delete" event when form values unchanged', () => {
+ wrapper.vm.handleSubmit(e);
+
+ expect(wrapper.emitted().delete[0]).toEqual([
+ {
+ alert: 'alert',
+ operator: '<',
+ threshold: 5,
+ prometheus_metric_id: '8',
+ },
+ ]);
+ expect(e.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('emits "update" event when form changed', () => {
+ wrapper.setData({
+ threshold: 11,
+ });
+
+ wrapper.vm.handleSubmit(e);
+
+ expect(wrapper.emitted().update[0]).toEqual([
+ {
+ alert: 'alert',
+ operator: '<',
+ threshold: 11,
+ prometheus_metric_id: '8',
+ },
+ ]);
+ expect(e.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('sets tracking options for update alert', () => {
+ wrapper.setData({
+ threshold: 11,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
index fb0682d0338..9cc5970da82 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 { graphDataPrometheusQuery } from '../../mock_data';
+import { singleStatMetricsResult } from '../../mock_data';
describe('Single Stat Chart component', () => {
let singleStatChart;
@@ -8,7 +8,7 @@ describe('Single Stat Chart component', () => {
beforeEach(() => {
singleStatChart = shallowMount(SingleStatChart, {
propsData: {
- graphData: graphDataPrometheusQuery,
+ graphData: singleStatMetricsResult,
},
});
});
@@ -26,7 +26,7 @@ describe('Single Stat Chart component', () => {
it('should change the value representation to a percentile one', () => {
singleStatChart.setProps({
graphData: {
- ...graphDataPrometheusQuery,
+ ...singleStatMetricsResult,
maxValue: 120,
},
});
@@ -37,7 +37,7 @@ describe('Single Stat Chart component', () => {
it('should display NaN for non numeric maxValue values', () => {
singleStatChart.setProps({
graphData: {
- ...graphDataPrometheusQuery,
+ ...singleStatMetricsResult,
maxValue: 'not a number',
},
});
@@ -48,13 +48,13 @@ describe('Single Stat Chart component', () => {
it('should display NaN for missing query values', () => {
singleStatChart.setProps({
graphData: {
- ...graphDataPrometheusQuery,
+ ...singleStatMetricsResult,
metrics: [
{
- ...graphDataPrometheusQuery.metrics[0],
+ ...singleStatMetricsResult.metrics[0],
result: [
{
- ...graphDataPrometheusQuery.metrics[0].result[0],
+ ...singleStatMetricsResult.metrics[0].result[0],
value: [''],
},
],
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 5ac716b0c63..7d5a08bc4a1 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
import { setTestTimeout } from 'helpers/timeout';
import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'jest/helpers/test_constants';
@@ -11,6 +11,7 @@ import {
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';
@@ -39,10 +40,10 @@ describe('Time series component', () => {
let mockGraphData;
let store;
- const makeTimeSeriesChart = (graphData, type) =>
- mount(TimeSeries, {
+ const createWrapper = (graphData = mockGraphData, mountingMethod = shallowMount) =>
+ mountingMethod(TimeSeries, {
propsData: {
- graphData: { ...graphData, type },
+ graphData,
deploymentData: store.state.monitoringDashboard.deploymentData,
annotations: store.state.monitoringDashboard.annotations,
projectPath: `${TEST_HOST}${mockProjectDir}`,
@@ -79,9 +80,9 @@ describe('Time series component', () => {
const findChart = () => timeSeriesChart.find({ ref: 'chart' });
- beforeEach(done => {
- timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
- timeSeriesChart.vm.$nextTick(done);
+ beforeEach(() => {
+ timeSeriesChart = createWrapper(mockGraphData, mount);
+ return timeSeriesChart.vm.$nextTick();
});
it('allows user to override max value label text using prop', () => {
@@ -100,6 +101,21 @@ describe('Time series component', () => {
});
});
+ it('chart sets a default height', () => {
+ const wrapper = createWrapper();
+ expect(wrapper.props('height')).toBe(chartHeight);
+ });
+
+ it('chart has a configurable height', () => {
+ const mockHeight = 599;
+ const wrapper = createWrapper();
+
+ wrapper.setProps({ height: mockHeight });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.props('height')).toBe(mockHeight);
+ });
+ });
+
describe('events', () => {
describe('datazoom', () => {
let eChartMock;
@@ -125,7 +141,7 @@ describe('Time series component', () => {
}),
};
- timeSeriesChart = makeTimeSeriesChart(mockGraphData);
+ timeSeriesChart = createWrapper(mockGraphData, mount);
timeSeriesChart.vm.$nextTick(() => {
findChart().vm.$emit('created', eChartMock);
done();
@@ -535,11 +551,11 @@ describe('Time series component', () => {
describe('wrapped components', () => {
const glChartComponents = [
{
- chartType: 'area-chart',
+ chartType: panelTypes.AREA_CHART,
component: GlAreaChart,
},
{
- chartType: 'line-chart',
+ chartType: panelTypes.LINE_CHART,
component: GlLineChart,
},
];
@@ -550,7 +566,10 @@ describe('Time series component', () => {
const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component);
beforeEach(done => {
- timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
+ timeSeriesAreaChart = createWrapper(
+ { ...mockGraphData, type: dynamicComponent.chartType },
+ mount,
+ );
timeSeriesAreaChart.vm.$nextTick(done);
});
@@ -632,7 +651,7 @@ describe('Time series component', () => {
Object.assign(metric, { result: metricResultStatus.result }),
);
- timeSeriesChart = makeTimeSeriesChart(graphData, 'area-chart');
+ timeSeriesChart = createWrapper({ ...graphData, type: 'area-chart' }, mount);
timeSeriesChart.vm.$nextTick(done);
});
diff --git a/spec/frontend/monitoring/components/panel_type_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index 819b5235284..f8c9bd56721 100644
--- a/spec/frontend/monitoring/components/panel_type_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -1,13 +1,13 @@
+import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { setTestTimeout } from 'helpers/timeout';
import invalidUrl from '~/lib/utils/invalid_url';
import axios from '~/lib/utils/axios_utils';
+import { GlDropdownItem } from '@gitlab/ui';
+import AlertWidget from '~/monitoring/components/alert_widget.vue';
-import PanelType from '~/monitoring/components/panel_type.vue';
-import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
-import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
-import AnomalyChart from '~/monitoring/components/charts/anomaly.vue';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import {
anomalyMockGraphData,
mockLogsHref,
@@ -15,8 +15,23 @@ import {
mockNamespace,
mockNamespacedData,
mockTimeRange,
+ singleStatMetricsResult,
+ graphDataPrometheusQueryRangeMultiTrack,
+ barMockData,
+ propsData,
} from '../mock_data';
+import { panelTypes } from '~/monitoring/constants';
+
+import MonitorEmptyChart from '~/monitoring/components/charts/empty_chart.vue';
+import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
+import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
+import MonitorSingleStatChart from '~/monitoring/components/charts/single_stat.vue';
+import MonitorHeatmapChart from '~/monitoring/components/charts/heatmap.vue';
+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';
@@ -29,7 +44,7 @@ const mocks = {
},
};
-describe('Panel Type component', () => {
+describe('Dashboard Panel', () => {
let axiosMock;
let store;
let state;
@@ -38,18 +53,20 @@ describe('Panel Type component', () => {
const exampleText = 'example_text';
const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
- const findTimeChart = () => wrapper.find({ ref: 'timeChart' });
+ const findTimeChart = () => wrapper.find({ ref: 'timeSeriesChart' });
const findTitle = () => wrapper.find({ ref: 'graphTitle' });
const findContextualMenu = () => wrapper.find({ ref: 'contextualMenu' });
- const createWrapper = props => {
- wrapper = shallowMount(PanelType, {
+ const createWrapper = (props, options) => {
+ wrapper = shallowMount(DashboardPanel, {
propsData: {
graphData,
+ settingsPath: propsData.settingsPath,
...props,
},
store,
mocks,
+ ...options,
});
};
@@ -66,6 +83,22 @@ describe('Panel Type component', () => {
axiosMock.reset();
});
+ describe('Renders slots', () => {
+ it('renders "topLeft" slot', () => {
+ createWrapper(
+ {},
+ {
+ slots: {
+ topLeft: `<div class="top-left-content">OK</div>`,
+ },
+ },
+ );
+
+ expect(wrapper.find('.top-left-content').exists()).toBe(true);
+ expect(wrapper.find('.top-left-content').text()).toBe('OK');
+ });
+ });
+
describe('When no graphData is available', () => {
beforeEach(() => {
createWrapper({
@@ -77,27 +110,54 @@ describe('Panel Type component', () => {
wrapper.destroy();
});
- describe('Empty Chart component', () => {
- it('renders the chart title', () => {
- expect(findTitle().text()).toBe(graphDataEmpty.title);
- });
+ it('renders the chart title', () => {
+ expect(findTitle().text()).toBe(graphDataEmpty.title);
+ });
- it('renders the no download csv link', () => {
- expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
- });
+ it('renders no download csv link', () => {
+ expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
+ });
- it('does not contain graph widgets', () => {
- expect(findContextualMenu().exists()).toBe(false);
- });
+ it('does not contain graph widgets', () => {
+ expect(findContextualMenu().exists()).toBe(false);
+ });
- it('is a Vue instance', () => {
- expect(wrapper.find(EmptyChart).exists()).toBe(true);
- expect(wrapper.find(EmptyChart).isVueInstance()).toBe(true);
+ it('The Empty Chart component is rendered and is a Vue instance', () => {
+ expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
+ });
+ });
+
+ describe('When graphData is null', () => {
+ beforeEach(() => {
+ createWrapper({
+ graphData: null,
});
});
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders no chart title', () => {
+ expect(findTitle().text()).toBe('');
+ });
+
+ it('renders no download csv link', () => {
+ expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
+ });
+
+ it('does not contain graph widgets', () => {
+ expect(findContextualMenu().exists()).toBe(false);
+ });
+
+ it('The Empty Chart component is rendered and is a Vue instance', () => {
+ expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
+ });
});
- describe('when graph data is available', () => {
+ describe('When graphData is available', () => {
beforeEach(() => {
createWrapper();
});
@@ -134,34 +194,54 @@ describe('Panel Type component', () => {
});
});
- describe('Time Series Chart panel type', () => {
- it('is rendered', () => {
- expect(wrapper.find(TimeSeriesChart).isVueInstance()).toBe(true);
- expect(wrapper.find(TimeSeriesChart).exists()).toBe(true);
- });
+ it('includes a default group id', () => {
+ expect(wrapper.vm.groupId).toBe('dashboard-panel');
+ });
+
+ describe('Supports different panel types', () => {
+ const dataWithType = type => {
+ return {
+ ...graphData,
+ type,
+ };
+ };
- it('includes a default group id', () => {
- expect(wrapper.vm.groupId).toBe('panel-type-chart');
+ it('empty chart is rendered for empty results', () => {
+ createWrapper({ graphData: graphDataEmpty });
+ expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
});
- });
- describe('Anomaly Chart panel type', () => {
- beforeEach(() => {
- wrapper.setProps({
- graphData: anomalyMockGraphData,
- });
- return wrapper.vm.$nextTick();
+ it('area chart is rendered by default', () => {
+ createWrapper();
+ expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
});
- it('is rendered with an anomaly chart', () => {
- expect(wrapper.find(AnomalyChart).isVueInstance()).toBe(true);
- expect(wrapper.find(AnomalyChart).exists()).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 }) => {
+ 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);
});
});
});
describe('Edit custom metric dropdown item', () => {
const findEditCustomMetricLink = () => wrapper.find({ ref: 'editMetricLink' });
+ const mockEditPath = '/root/kubernetes-gke-project/prometheus/metrics/23/edit';
beforeEach(() => {
createWrapper();
@@ -180,7 +260,7 @@ describe('Panel Type component', () => {
metrics: [
{
...graphData.metrics[0],
- edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
+ edit_path: mockEditPath,
},
],
},
@@ -189,10 +269,11 @@ describe('Panel Type component', () => {
return wrapper.vm.$nextTick(() => {
expect(findEditCustomMetricLink().exists()).toBe(true);
expect(findEditCustomMetricLink().text()).toBe('Edit metric');
+ expect(findEditCustomMetricLink().attributes('href')).toBe(mockEditPath);
});
});
- it('shows an "Edit metrics" link for a panel with multiple metrics', () => {
+ it('shows an "Edit metrics" link pointing to settingsPath for a panel with multiple metrics', () => {
wrapper.setProps({
graphData: {
...graphData,
@@ -211,6 +292,7 @@ describe('Panel Type component', () => {
return wrapper.vm.$nextTick(() => {
expect(findEditCustomMetricLink().text()).toBe('Edit metrics');
+ expect(findEditCustomMetricLink().attributes('href')).toBe(propsData.settingsPath);
});
});
});
@@ -294,10 +376,6 @@ describe('Panel Type component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('sets clipboard text on the dropdown', () => {
expect(findCopyLink().exists()).toBe(true);
expect(findCopyLink().element.dataset.clipboardText).toBe(clipboardText);
@@ -314,11 +392,24 @@ describe('Panel Type component', () => {
});
});
+ describe('when cliboard data is not available', () => {
+ it('there is no "copy to clipboard" link for a null value', () => {
+ createWrapper({ clipboardText: null });
+ expect(findCopyLink().exists()).toBe(false);
+ });
+
+ it('there is no "copy to clipboard" link for an empty value', () => {
+ createWrapper({ clipboardText: '' });
+ expect(findCopyLink().exists()).toBe(false);
+ });
+ });
+
describe('when downloading metrics data as CSV', () => {
beforeEach(() => {
- wrapper = shallowMount(PanelType, {
+ wrapper = shallowMount(DashboardPanel, {
propsData: {
clipboardText: exampleText,
+ settingsPath: propsData.settingsPath,
graphData: {
y_label: 'metric',
...graphData,
@@ -365,9 +456,10 @@ describe('Panel Type component', () => {
store.registerModule(mockNamespace, monitoringDashboard);
store.state.embedGroup.modules.push(mockNamespace);
- wrapper = shallowMount(PanelType, {
+ wrapper = shallowMount(DashboardPanel, {
propsData: {
graphData,
+ settingsPath: propsData.settingsPath,
namespace: mockNamespace,
},
store,
@@ -401,8 +493,84 @@ describe('Panel Type component', () => {
});
it('it renders a time series chart with no errors', () => {
- expect(wrapper.find(TimeSeriesChart).isVueInstance()).toBe(true);
- expect(wrapper.find(TimeSeriesChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
+ expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
+ });
+ });
+
+ describe('Expand to full screen', () => {
+ const findExpandBtn = () => wrapper.find({ ref: 'expandBtn' });
+
+ describe('when there is no @expand listener', () => {
+ it('does not show `View full screen` option', () => {
+ createWrapper();
+ expect(findExpandBtn().exists()).toBe(false);
+ });
+ });
+
+ describe('when there is an @expand listener', () => {
+ beforeEach(() => {
+ createWrapper({}, { listeners: { expand: () => {} } });
+ });
+
+ it('shows the `expand` option', () => {
+ expect(findExpandBtn().exists()).toBe(true);
+ });
+
+ it('emits the `expand` event', () => {
+ const preventDefault = jest.fn();
+ findExpandBtn().vm.$emit('click', { preventDefault });
+ expect(wrapper.emitted('expand')).toHaveLength(1);
+ expect(preventDefault).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('panel alerts', () => {
+ const setMetricsSavedToDb = val =>
+ monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
+ const findAlertsWidget = () => wrapper.find(AlertWidget);
+ const findMenuItemAlert = () =>
+ wrapper.findAll(GlDropdownItem).filter(i => i.text() === 'Alerts');
+
+ beforeEach(() => {
+ jest.spyOn(monitoringDashboard.getters, 'metricsSavedToDb').mockReturnValue([]);
+
+ store = new Vuex.Store({
+ modules: {
+ monitoringDashboard,
+ },
+ });
+
+ createWrapper();
+ });
+
+ describe.each`
+ desc | metricsSavedToDb | props | isShown
+ ${'with permission and no metrics in db'} | ${[]} | ${{}} | ${false}
+ ${'with permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{}} | ${true}
+ ${'without permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{ prometheusAlertsAvailable: false }} | ${false}
+ ${'with permission and unrelated metrics in db'} | ${['another_metric_id']} | ${{}} | ${false}
+ `('$desc', ({ metricsSavedToDb, isShown, props }) => {
+ const showsDesc = isShown ? 'shows' : 'does not show';
+
+ beforeEach(() => {
+ setMetricsSavedToDb(metricsSavedToDb);
+ createWrapper({
+ alertsEndpoint: '/endpoint',
+ prometheusAlertsAvailable: true,
+ ...props,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it(`${showsDesc} alert widget`, () => {
+ expect(findAlertsWidget().exists()).toBe(isShown);
+ });
+
+ it(`${showsDesc} alert configuration`, () => {
+ expect(findMenuItemAlert().exists()).toBe(isShown);
+ });
});
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 8b6ee9b3bf6..b2c9fe93cde 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -1,6 +1,8 @@
import { shallowMount, mount } from '@vue/test-utils';
import Tracking from '~/tracking';
-import { GlModal, GlDropdownItem, GlDeprecatedButton } from '@gitlab/ui';
+import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
+import { GlModal, GlDropdownItem, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
+import { objectToQuery } from '~/lib/utils/url_utility';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
@@ -11,13 +13,23 @@ import Dashboard from '~/monitoring/components/dashboard.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 PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
-import { setupStoreWithDashboard, setMetricResult, setupStoreWithData } from '../store_utils';
+import {
+ setupAllDashboards,
+ setupStoreWithDashboard,
+ setMetricResult,
+ setupStoreWithData,
+ setupStoreWithVariable,
+} from '../store_utils';
import { environmentData, dashboardGitResponse, propsData } from '../mock_data';
import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
describe('Dashboard', () => {
let store;
@@ -27,15 +39,12 @@ describe('Dashboard', () => {
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
const setSearchTerm = searchTerm => {
- wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
+ store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
};
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(Dashboard, {
propsData: { ...propsData, ...props },
- methods: {
- fetchData: jest.fn(),
- },
store,
...options,
});
@@ -44,10 +53,8 @@ describe('Dashboard', () => {
const createMountedWrapper = (props = {}, options = {}) => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
- methods: {
- fetchData: jest.fn(),
- },
store,
+ stubs: ['graph-group', 'dashboard-panel'],
...options,
});
};
@@ -55,19 +62,18 @@ describe('Dashboard', () => {
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
mock.restore();
+ if (store.dispatch.mockReset) {
+ store.dispatch.mockReset();
+ }
});
describe('no metrics are available yet', () => {
beforeEach(() => {
- jest.spyOn(store, 'dispatch');
createShallowWrapper();
});
@@ -103,9 +109,7 @@ describe('Dashboard', () => {
describe('request information to the server', () => {
it('calls to set time range and fetch data', () => {
- jest.spyOn(store, 'dispatch');
-
- createShallowWrapper({ hasMetrics: true }, { methods: {} });
+ createShallowWrapper({ hasMetrics: true });
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
@@ -118,20 +122,20 @@ describe('Dashboard', () => {
});
it('shows up a loading state', () => {
- createShallowWrapper({ hasMetrics: true }, { methods: {} });
+ store.state.monitoringDashboard.emptyState = 'loading';
+
+ createShallowWrapper({ hasMetrics: true });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.emptyState).toEqual('loading');
+ expect(wrapper.find(EmptyState).exists()).toBe(true);
+ expect(wrapper.find(EmptyState).props('selectedState')).toBe('loading');
});
});
it('hides the group panels when showPanels is false', () => {
- createMountedWrapper(
- { hasMetrics: true, showPanels: false },
- { stubs: ['graph-group', 'panel-type'] },
- );
+ createMountedWrapper({ hasMetrics: true, showPanels: false });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.showEmptyState).toEqual(false);
@@ -142,9 +146,9 @@ describe('Dashboard', () => {
it('fetches the metrics data with proper time window', () => {
jest.spyOn(store, 'dispatch');
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- wrapper.vm.$store.commit(
+ store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
@@ -155,11 +159,176 @@ describe('Dashboard', () => {
});
});
+ describe('when the URL contains a reference to a panel', () => {
+ let location;
+
+ const setSearch = search => {
+ window.location = { ...location, search };
+ };
+
+ beforeEach(() => {
+ location = window.location;
+ delete window.location;
+ });
+
+ afterEach(() => {
+ window.location = location;
+ });
+
+ it('when the URL points to a panel it expands', () => {
+ const panelGroup = metricsDashboardViewModel.panelGroups[0];
+ const panel = panelGroup.panels[0];
+
+ setSearch(
+ objectToQuery({
+ group: panelGroup.group,
+ title: panel.title,
+ y_label: panel.y_label,
+ }),
+ );
+
+ createMountedWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
+ group: panelGroup.group,
+ panel: expect.objectContaining({
+ title: panel.title,
+ y_label: panel.y_label,
+ }),
+ });
+ });
+ });
+
+ it('when the URL does not link to any panel, no panel is expanded', () => {
+ setSearch('');
+
+ createMountedWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'monitoringDashboard/setExpandedPanel',
+ expect.anything(),
+ );
+ });
+ });
+
+ it('when the URL points to an incorrect panel it shows an error', () => {
+ const panelGroup = metricsDashboardViewModel.panelGroups[0];
+ const panel = panelGroup.panels[0];
+
+ setSearch(
+ objectToQuery({
+ group: panelGroup.group,
+ title: 'incorrect',
+ y_label: panel.y_label,
+ }),
+ );
+
+ createMountedWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(createFlash).toHaveBeenCalled();
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'monitoringDashboard/setExpandedPanel',
+ expect.anything(),
+ );
+ });
+ });
+ });
+
+ describe('when the panel is expanded', () => {
+ let group;
+ let panel;
+
+ const expandPanel = (mockGroup, mockPanel) => {
+ store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
+ group: mockGroup,
+ panel: mockPanel,
+ });
+ };
+
+ beforeEach(() => {
+ setupStoreWithData(store);
+
+ const { panelGroups } = store.state.monitoringDashboard.dashboard;
+ group = panelGroups[0].group;
+ [panel] = panelGroups[0].panels;
+
+ jest.spyOn(window.history, 'pushState').mockImplementation();
+ });
+
+ afterEach(() => {
+ window.history.pushState.mockRestore();
+ });
+
+ it('URL is updated with panel parameters', () => {
+ createMountedWrapper({ hasMetrics: true });
+ expandPanel(group, panel);
+
+ const expectedSearch = objectToQuery({
+ group,
+ title: panel.title,
+ y_label: panel.y_label,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(), // state
+ expect.any(String), // document title
+ expect.stringContaining(`${expectedSearch}`),
+ );
+ });
+ });
+
+ it('URL is updated with panel parameters and custom dashboard', () => {
+ const dashboard = 'dashboard.yml';
+
+ createMountedWrapper({ hasMetrics: true, currentDashboard: dashboard });
+ expandPanel(group, panel);
+
+ const expectedSearch = objectToQuery({
+ dashboard,
+ group,
+ title: panel.title,
+ y_label: panel.y_label,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(), // state
+ expect.any(String), // document title
+ expect.stringContaining(`${expectedSearch}`),
+ );
+ });
+ });
+
+ it('URL is updated with no parameters', () => {
+ expandPanel(group, panel);
+ createMountedWrapper({ hasMetrics: true });
+ expandPanel(null, null);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(), // state
+ expect.any(String), // document title
+ expect.not.stringMatching(/group|title|y_label/), // no panel params
+ );
+ });
+ });
+ });
+
describe('when all requests have been commited by the store', () => {
beforeEach(() => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick();
});
@@ -185,10 +354,89 @@ describe('Dashboard', () => {
});
});
+ describe('star dashboards', () => {
+ const findToggleStar = () => wrapper.find({ ref: 'toggleStarBtn' });
+ const findToggleStarIcon = () => findToggleStar().find(GlIcon);
+
+ beforeEach(() => {
+ createShallowWrapper();
+ setupAllDashboards(store);
+ });
+
+ it('toggle star button is shown', () => {
+ expect(findToggleStar().exists()).toBe(true);
+ expect(findToggleStar().props('disabled')).toBe(false);
+ });
+
+ it('toggle star button is disabled when starring is taking place', () => {
+ store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findToggleStar().exists()).toBe(true);
+ expect(findToggleStar().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('when the dashboard list is loaded', () => {
+ // Tooltip element should wrap directly
+ const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title');
+
+ beforeEach(() => {
+ setupAllDashboards(store);
+ jest.spyOn(store, 'dispatch');
+ });
+
+ it('dispatches a toggle star action', () => {
+ findToggleStar().vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/toggleStarredValue',
+ undefined,
+ );
+ });
+ });
+
+ describe('when dashboard is not starred', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[0].path,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('toggle star button shows "Star dashboard"', () => {
+ expect(getToggleTooltip()).toBe('Star dashboard');
+ });
+
+ it('toggle star button shows an unstarred state', () => {
+ expect(findToggleStarIcon().attributes('name')).toBe('star-o');
+ });
+ });
+
+ describe('when dashboard is starred', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[1].path,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('toggle star button shows "Star dashboard"', () => {
+ expect(getToggleTooltip()).toBe('Unstar dashboard');
+ });
+
+ it('toggle star button shows a starred state', () => {
+ expect(findToggleStarIcon().attributes('name')).toBe('star');
+ });
+ });
+ });
+ });
+
it('hides the environments dropdown list when there is no environments', () => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- setupStoreWithDashboard(wrapper.vm.$store);
+ setupStoreWithDashboard(store);
return wrapper.vm.$nextTick().then(() => {
expect(findAllEnvironmentsDropdownItems()).toHaveLength(0);
@@ -196,9 +444,9 @@ describe('Dashboard', () => {
});
it('renders the datetimepicker dropdown', () => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DateTimePicker).exists()).toBe(true);
@@ -206,9 +454,9 @@ describe('Dashboard', () => {
});
it('renders the refresh dashboard button', () => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
const refreshBtn = wrapper.findAll({ ref: 'refreshDashboardBtn' });
@@ -218,14 +466,135 @@ describe('Dashboard', () => {
});
});
- describe('when one of the metrics is missing', () => {
+ describe('variables section', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+ setupStoreWithVariable(store);
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows the variables section', () => {
+ expect(wrapper.vm.shouldShowVariablesSection).toBe(true);
+ });
+ });
+
+ describe('single panel expands to "full screen" mode', () => {
+ const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' });
- const { $store } = wrapper.vm;
+ describe('when the panel is not expanded', () => {
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+ return wrapper.vm.$nextTick();
+ });
+
+ it('expanded panel is not visible', () => {
+ expect(findExpandedPanel().isVisible()).toBe(false);
+ });
+
+ it('can set a panel as expanded', () => {
+ const panel = wrapper.findAll(DashboardPanel).at(1);
+
+ jest.spyOn(store, 'dispatch');
+
+ panel.vm.$emit('expand');
+
+ const groupData = metricsDashboardViewModel.panelGroups[0];
+
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
+ group: groupData.group,
+ panel: expect.objectContaining({
+ id: groupData.panels[0].id,
+ }),
+ });
+ });
+ });
+
+ describe('when the panel is expanded', () => {
+ let group;
+ let panel;
+
+ const mockKeyup = key => window.dispatchEvent(new KeyboardEvent('keyup', { key }));
+
+ const MockPanel = {
+ template: `<div><slot name="topLeft"/></div>`,
+ };
+
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true }, { stubs: { DashboardPanel: MockPanel } });
+ setupStoreWithData(store);
+
+ const { panelGroups } = store.state.monitoringDashboard.dashboard;
+
+ group = panelGroups[0].group;
+ [panel] = panelGroups[0].panels;
+
+ store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
+ group,
+ panel,
+ });
+
+ jest.spyOn(store, 'dispatch');
+
+ return wrapper.vm.$nextTick();
+ });
- setupStoreWithDashboard($store);
- setMetricResult({ $store, result: [], panel: 2 });
+ it('displays a single panel and others are hidden', () => {
+ const panels = wrapper.findAll(MockPanel);
+ const visiblePanels = panels.filter(w => w.isVisible());
+
+ expect(findExpandedPanel().isVisible()).toBe(true);
+ // v-show for hiding panels is more performant than v-if
+ // check for panels to be hidden.
+ expect(panels.length).toBe(metricsDashboardPanelCount + 1);
+ expect(visiblePanels.length).toBe(1);
+ });
+
+ it('sets a link to the expanded panel', () => {
+ const searchQuery =
+ '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=System%20metrics%20(Kubernetes)&title=Memory%20Usage%20(Total)&y_label=Total%20Memory%20Used%20(GB)';
+
+ expect(findExpandedPanel().attributes('clipboard-text')).toEqual(
+ expect.stringContaining(searchQuery),
+ );
+ });
+
+ it('restores full dashboard by clicking `back`', () => {
+ wrapper.find({ ref: 'goBackBtn' }).vm.$emit('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/clearExpandedPanel',
+ undefined,
+ );
+ });
+
+ it('restores dashboard from full screen by typing the Escape key', () => {
+ mockKeyup(ESC_KEY);
+ expect(store.dispatch).toHaveBeenCalledWith(
+ `monitoringDashboard/clearExpandedPanel`,
+ undefined,
+ );
+ });
+
+ it('restores dashboard from full screen by typing the Escape key on IE11', () => {
+ mockKeyup(ESC_KEY_IE11);
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ `monitoringDashboard/clearExpandedPanel`,
+ undefined,
+ );
+ });
+ });
+ });
+
+ describe('when one of the metrics is missing', () => {
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true });
+
+ setupStoreWithDashboard(store);
+ setMetricResult({ store, result: [], panel: 2 });
return wrapper.vm.$nextTick();
});
@@ -249,19 +618,17 @@ describe('Dashboard', () => {
describe('searchable environments dropdown', () => {
beforeEach(() => {
- createMountedWrapper(
- { hasMetrics: true },
- {
- attachToDocument: true,
- stubs: ['graph-group', 'panel-type'],
- },
- );
+ createMountedWrapper({ hasMetrics: true }, { attachToDocument: true });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick();
});
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
it('renders a search input', () => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownSearch' }).exists()).toBe(true);
});
@@ -304,7 +671,7 @@ describe('Dashboard', () => {
});
it('shows loading element when environments fetch is still loading', () => {
- wrapper.vm.$store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
+ store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
return wrapper.vm
.$nextTick()
@@ -312,7 +679,7 @@ describe('Dashboard', () => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownLoading' }).exists()).toBe(true);
})
.then(() => {
- wrapper.vm.$store.commit(
+ store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
@@ -330,9 +697,11 @@ describe('Dashboard', () => {
const findRearrangeButton = () => wrapper.find('.js-rearrange-button');
beforeEach(() => {
- createShallowWrapper({ hasMetrics: true });
+ // call original dispatch
+ store.dispatch.mockRestore();
- setupStoreWithData(wrapper.vm.$store);
+ createShallowWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
return wrapper.vm.$nextTick();
});
@@ -420,7 +789,7 @@ describe('Dashboard', () => {
createShallowWrapper({ hasMetrics: true, showHeader: false });
// all_dashboards is not defined in health dashboards
- wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
+ store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
return wrapper.vm.$nextTick();
});
@@ -440,10 +809,7 @@ describe('Dashboard', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
- wrapper.vm.$store.commit(
- `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
- dashboardGitResponse,
- );
+ setupAllDashboards(store);
return wrapper.vm.$nextTick();
});
@@ -452,10 +818,11 @@ describe('Dashboard', () => {
});
it('is present for a custom dashboard, and links to its edit_path', () => {
- const dashboard = dashboardGitResponse[1]; // non-default dashboard
- const currentDashboard = dashboard.path;
+ const dashboard = dashboardGitResponse[1];
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboard.path,
+ });
- wrapper.setProps({ currentDashboard });
return wrapper.vm.$nextTick().then(() => {
expect(findEditLink().exists()).toBe(true);
expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path);
@@ -465,13 +832,8 @@ describe('Dashboard', () => {
describe('Dashboard dropdown', () => {
beforeEach(() => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
-
- wrapper.vm.$store.commit(
- `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
- dashboardGitResponse,
- );
-
+ createMountedWrapper({ hasMetrics: true });
+ setupAllDashboards(store);
return wrapper.vm.$nextTick();
});
@@ -484,15 +846,12 @@ describe('Dashboard', () => {
describe('external dashboard link', () => {
beforeEach(() => {
- createMountedWrapper(
- {
- hasMetrics: true,
- showPanels: false,
- showTimeWindowDropdown: false,
- externalDashboardUrl: '/mockUrl',
- },
- { stubs: ['graph-group', 'panel-type'] },
- );
+ createMountedWrapper({
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ externalDashboardUrl: '/mockUrl',
+ });
return wrapper.vm.$nextTick();
});
@@ -507,45 +866,29 @@ describe('Dashboard', () => {
});
describe('Clipboard text in panels', () => {
- const currentDashboard = 'TEST_DASHBOARD';
+ const currentDashboard = dashboardGitResponse[1].path;
+ const panelIndex = 1; // skip expanded panel
- const getClipboardTextAt = i =>
+ const getClipboardTextFirstPanel = () =>
wrapper
- .findAll(PanelType)
- .at(i)
+ .findAll(DashboardPanel)
+ .at(panelIndex)
.props('clipboardText');
beforeEach(() => {
+ setupStoreWithData(store);
createShallowWrapper({ hasMetrics: true, currentDashboard });
- setupStoreWithData(wrapper.vm.$store);
-
return wrapper.vm.$nextTick();
});
it('contains a link to the dashboard', () => {
- expect(getClipboardTextAt(0)).toContain(`dashboard=${currentDashboard}`);
- expect(getClipboardTextAt(0)).toContain(`group=`);
- expect(getClipboardTextAt(0)).toContain(`title=`);
- expect(getClipboardTextAt(0)).toContain(`y_label=`);
- });
-
- it('strips the undefined parameter', () => {
- wrapper.setProps({ currentDashboard: undefined });
-
- return wrapper.vm.$nextTick(() => {
- expect(getClipboardTextAt(0)).not.toContain(`dashboard=`);
- expect(getClipboardTextAt(0)).toContain(`y_label=`);
- });
- });
+ const dashboardParam = `dashboard=${encodeURIComponent(currentDashboard)}`;
- it('null parameter is stripped', () => {
- wrapper.setProps({ currentDashboard: null });
-
- return wrapper.vm.$nextTick(() => {
- expect(getClipboardTextAt(0)).not.toContain(`dashboard=`);
- expect(getClipboardTextAt(0)).toContain(`y_label=`);
- });
+ expect(getClipboardTextFirstPanel()).toContain(dashboardParam);
+ expect(getClipboardTextFirstPanel()).toContain(`group=`);
+ expect(getClipboardTextFirstPanel()).toContain(`title=`);
+ expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
});
});
@@ -572,7 +915,7 @@ describe('Dashboard', () => {
customMetricsPath: '/endpoint',
customMetricsAvailable: true,
});
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
origPage = document.body.dataset.page;
document.body.dataset.page = 'projects:environments:metrics';
diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js
index d1790df4189..cc0ac348b11 100644
--- a/spec/frontend/monitoring/components/dashboard_template_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_template_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
+import { setupAllDashboards } from '../store_utils';
import { propsData } from '../mock_data';
jest.mock('~/lib/utils/url_utility');
@@ -15,24 +16,16 @@ describe('Dashboard template', () => {
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
+
+ setupAllDashboards(store);
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
mock.restore();
});
it('matches the default snapshot', () => {
- wrapper = shallowMount(Dashboard, {
- propsData: { ...propsData },
- methods: {
- fetchData: jest.fn(),
- },
- store,
- });
+ wrapper = shallowMount(Dashboard, { propsData: { ...propsData }, store });
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index 65e9d036d1a..9bba5280007 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -27,7 +27,7 @@ describe('dashboard invalid url parameters', () => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
store,
- stubs: ['graph-group', 'panel-type'],
+ stubs: ['graph-group', 'dashboard-panel'],
...options,
});
};
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 0bcfabe6415..b29d86cbc5b 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert, GlIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
@@ -9,36 +9,48 @@ import { dashboardGitResponse } from '../mock_data';
const defaultBranch = 'master';
-function createComponent(props, opts = {}) {
- const storeOpts = {
- methods: {
- duplicateSystemDashboard: jest.fn(),
- },
- computed: {
- allDashboards: () => dashboardGitResponse,
- },
- };
-
- return shallowMount(DashboardsDropdown, {
- propsData: {
- ...props,
- defaultBranch,
- },
- sync: false,
- ...storeOpts,
- ...opts,
- });
-}
+const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred);
+const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred);
describe('DashboardsDropdown', () => {
let wrapper;
+ let mockDashboards;
+ let mockSelectedDashboard;
+
+ function createComponent(props, opts = {}) {
+ const storeOpts = {
+ methods: {
+ duplicateSystemDashboard: jest.fn(),
+ },
+ computed: {
+ allDashboards: () => mockDashboards,
+ selectedDashboard: () => mockSelectedDashboard,
+ },
+ };
+
+ return shallowMount(DashboardsDropdown, {
+ propsData: {
+ ...props,
+ defaultBranch,
+ },
+ sync: false,
+ ...storeOpts,
+ ...opts,
+ });
+ }
const findItems = () => wrapper.findAll(GlDropdownItem);
const findItemAt = i => wrapper.findAll(GlDropdownItem).at(i);
const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' });
const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' });
+ const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' });
const setSearchTerm = searchTerm => wrapper.setData({ searchTerm });
+ beforeEach(() => {
+ mockDashboards = dashboardGitResponse;
+ mockSelectedDashboard = null;
+ });
+
describe('when it receives dashboards data', () => {
beforeEach(() => {
wrapper = createComponent();
@@ -48,10 +60,14 @@ describe('DashboardsDropdown', () => {
expect(findItems().length).toEqual(dashboardGitResponse.length);
});
- it('displays items with the dashboard display name', () => {
- expect(findItemAt(0).text()).toBe(dashboardGitResponse[0].display_name);
- expect(findItemAt(1).text()).toBe(dashboardGitResponse[1].display_name);
- expect(findItemAt(2).text()).toBe(dashboardGitResponse[2].display_name);
+ it('displays items with the dashboard display name, with starred dashboards first', () => {
+ expect(findItemAt(0).text()).toBe(starredDashboards[0].display_name);
+ expect(findItemAt(1).text()).toBe(notStarredDashboards[0].display_name);
+ expect(findItemAt(2).text()).toBe(notStarredDashboards[1].display_name);
+ });
+
+ it('displays separator between starred and not starred dashboards', () => {
+ expect(findStarredListDivider().exists()).toBe(true);
});
it('displays a search input', () => {
@@ -81,18 +97,71 @@ describe('DashboardsDropdown', () => {
});
});
+ describe('when the dashboard is missing a display name', () => {
+ beforeEach(() => {
+ mockDashboards = dashboardGitResponse.map(d => ({ ...d, display_name: undefined }));
+ wrapper = createComponent();
+ });
+
+ it('displays items with the dashboard path, with starred dashboards first', () => {
+ expect(findItemAt(0).text()).toBe(starredDashboards[0].path);
+ expect(findItemAt(1).text()).toBe(notStarredDashboards[0].path);
+ expect(findItemAt(2).text()).toBe(notStarredDashboards[1].path);
+ });
+ });
+
+ describe('when it receives starred dashboards', () => {
+ beforeEach(() => {
+ mockDashboards = starredDashboards;
+ wrapper = createComponent();
+ });
+
+ it('displays an item for each dashboard', () => {
+ expect(findItems().length).toEqual(starredDashboards.length);
+ });
+
+ it('displays a star icon', () => {
+ const star = findItemAt(0).find(GlIcon);
+ expect(star.exists()).toBe(true);
+ expect(star.attributes('name')).toBe('star');
+ });
+
+ it('displays no separator between starred and not starred dashboards', () => {
+ expect(findStarredListDivider().exists()).toBe(false);
+ });
+ });
+
+ describe('when it receives only not-starred dashboards', () => {
+ beforeEach(() => {
+ mockDashboards = notStarredDashboards;
+ wrapper = createComponent();
+ });
+
+ it('displays an item for each dashboard', () => {
+ expect(findItems().length).toEqual(notStarredDashboards.length);
+ });
+
+ it('displays no star icon', () => {
+ const star = findItemAt(0).find(GlIcon);
+ expect(star.exists()).toBe(false);
+ });
+
+ it('displays no separator between starred and not starred dashboards', () => {
+ expect(findStarredListDivider().exists()).toBe(false);
+ });
+ });
+
describe('when a system dashboard is selected', () => {
let duplicateDashboardAction;
let modalDirective;
beforeEach(() => {
+ [mockSelectedDashboard] = dashboardGitResponse;
modalDirective = jest.fn();
duplicateDashboardAction = jest.fn().mockResolvedValue();
wrapper = createComponent(
- {
- selectedDashboard: dashboardGitResponse[0],
- },
+ {},
{
directives: {
GlModal: modalDirective,
@@ -260,7 +329,7 @@ describe('DashboardsDropdown', () => {
expect(wrapper.emitted().selectDashboard).toBeTruthy();
});
it('emits a "selectDashboard" event with dashboard information', () => {
- expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[1]]);
+ expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[0]]);
});
});
});
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
index 10fd58f749d..216ec345552 100644
--- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
@@ -81,7 +81,8 @@ describe('DuplicateDashboardForm', () => {
it('with the inital form values', () => {
expect(wrapper.emitted().change).toHaveLength(1);
- expect(lastChange()).resolves.toEqual({
+
+ return expect(lastChange()).resolves.toEqual({
branch: '',
commitMessage: expect.any(String),
dashboard: dashboardGitResponse[0].path,
@@ -92,7 +93,7 @@ describe('DuplicateDashboardForm', () => {
it('containing an inputted file name', () => {
setValue('fileName', 'my_dashboard.yml');
- expect(lastChange()).resolves.toMatchObject({
+ return expect(lastChange()).resolves.toMatchObject({
fileName: 'my_dashboard.yml',
});
});
@@ -100,7 +101,7 @@ describe('DuplicateDashboardForm', () => {
it('containing a default commit message when no message is set', () => {
setValue('commitMessage', '');
- expect(lastChange()).resolves.toMatchObject({
+ return expect(lastChange()).resolves.toMatchObject({
commitMessage: expect.stringContaining('Create custom dashboard'),
});
});
@@ -108,7 +109,7 @@ describe('DuplicateDashboardForm', () => {
it('containing an inputted commit message', () => {
setValue('commitMessage', 'My commit message');
- expect(lastChange()).resolves.toMatchObject({
+ return expect(lastChange()).resolves.toMatchObject({
commitMessage: expect.stringContaining('My commit message'),
});
});
@@ -116,7 +117,7 @@ describe('DuplicateDashboardForm', () => {
it('containing an inputted branch name', () => {
setValue('branchName', 'a-new-branch');
- expect(lastChange()).resolves.toMatchObject({
+ return expect(lastChange()).resolves.toMatchObject({
branch: 'a-new-branch',
});
});
@@ -125,13 +126,14 @@ describe('DuplicateDashboardForm', () => {
setChecked(wrapper.vm.$options.radioVals.DEFAULT);
setValue('branchName', 'a-new-branch');
- expect(lastChange()).resolves.toMatchObject({
- branch: defaultBranch,
- });
-
- return wrapper.vm.$nextTick(() => {
- expect(findByRef('branchName').isVisible()).toBe(false);
- });
+ return Promise.all([
+ expect(lastChange()).resolves.toMatchObject({
+ branch: defaultBranch,
+ }),
+ wrapper.vm.$nextTick(() => {
+ expect(findByRef('branchName').isVisible()).toBe(false);
+ }),
+ ]);
});
it('when `new` branch option is chosen, focuses on the branch name input', () => {
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
index b829cd53479..f23823ccad6 100644
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
@@ -1,6 +1,6 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { TEST_HOST } from 'helpers/test_constants';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
import { groups, initialState, metricsData, metricsWithData } from './mock_data';
@@ -62,7 +62,7 @@ describe('MetricEmbed', () => {
it('shows an empty state when no metrics are present', () => {
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.find(PanelType).exists()).toBe(false);
+ expect(wrapper.find(DashboardPanel).exists()).toBe(false);
});
});
@@ -90,12 +90,12 @@ describe('MetricEmbed', () => {
it('shows a chart when metrics are present', () => {
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.find(PanelType).exists()).toBe(true);
- expect(wrapper.findAll(PanelType).length).toBe(2);
+ expect(wrapper.find(DashboardPanel).exists()).toBe(true);
+ expect(wrapper.findAll(DashboardPanel).length).toBe(2);
});
it('includes groupId with dashboardUrl', () => {
- expect(wrapper.find(PanelType).props('groupId')).toBe(TEST_HOST);
+ expect(wrapper.find(DashboardPanel).props('groupId')).toBe(TEST_HOST);
});
});
});
diff --git a/spec/frontend/monitoring/components/variables/custom_variable_spec.js b/spec/frontend/monitoring/components/variables/custom_variable_spec.js
new file mode 100644
index 00000000000..5a2b26219b6
--- /dev/null
+++ b/spec/frontend/monitoring/components/variables/custom_variable_spec.js
@@ -0,0 +1,52 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import CustomVariable from '~/monitoring/components/variables/custom_variable.vue';
+
+describe('Custom variable component', () => {
+ let wrapper;
+ const propsData = {
+ name: 'env',
+ label: 'Select environment',
+ value: 'Production',
+ options: [{ text: 'Production', value: 'prod' }, { text: 'Canary', value: 'canary' }],
+ };
+ const createShallowWrapper = () => {
+ wrapper = shallowMount(CustomVariable, {
+ propsData,
+ });
+ };
+
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+
+ it('renders dropdown element when all necessary props are passed', () => {
+ createShallowWrapper();
+
+ expect(findDropdown()).toExist();
+ });
+
+ it('renders dropdown element with a text', () => {
+ createShallowWrapper();
+
+ expect(findDropdown().attributes('text')).toBe(propsData.value);
+ });
+
+ it('renders all the dropdown items', () => {
+ createShallowWrapper();
+
+ expect(findDropdownItems()).toHaveLength(propsData.options.length);
+ });
+
+ it('changing dropdown items triggers update', () => {
+ createShallowWrapper();
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findDropdownItems()
+ .at(1)
+ .vm.$emit('click');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'env', 'canary');
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/variables/text_variable_spec.js b/spec/frontend/monitoring/components/variables/text_variable_spec.js
new file mode 100644
index 00000000000..f01584ae8bc
--- /dev/null
+++ b/spec/frontend/monitoring/components/variables/text_variable_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormInput } from '@gitlab/ui';
+import TextVariable from '~/monitoring/components/variables/text_variable.vue';
+
+describe('Text variable component', () => {
+ let wrapper;
+ const propsData = {
+ name: 'pod',
+ label: 'Select pod',
+ value: 'test-pod',
+ };
+ const createShallowWrapper = () => {
+ wrapper = shallowMount(TextVariable, {
+ propsData,
+ });
+ };
+
+ const findInput = () => wrapper.find(GlFormInput);
+
+ it('renders a text input when all props are passed', () => {
+ createShallowWrapper();
+
+ expect(findInput()).toExist();
+ });
+
+ it('always has a default value', () => {
+ createShallowWrapper();
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findInput().attributes('value')).toBe(propsData.value);
+ });
+ });
+
+ it('triggers keyup enter', () => {
+ createShallowWrapper();
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findInput().element.value = 'prod-pod';
+ findInput().trigger('input');
+ findInput().trigger('keyup.enter');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'prod-pod');
+ });
+ });
+
+ it('triggers blur enter', () => {
+ createShallowWrapper();
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findInput().element.value = 'canary-pod';
+ findInput().trigger('input');
+ findInput().trigger('blur');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'canary-pod');
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
new file mode 100644
index 00000000000..095d89c9231
--- /dev/null
+++ b/spec/frontend/monitoring/components/variables_section_spec.js
@@ -0,0 +1,126 @@
+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 { 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';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ updateHistory: jest.fn(),
+ mergeUrlParams: jest.fn(),
+}));
+
+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, {
+ store,
+ });
+ };
+
+ const findTextInput = () => wrapper.findAll(TextVariable);
+ const findCustomInput = () => wrapper.findAll(CustomVariable);
+
+ beforeEach(() => {
+ store = createStore();
+
+ store.state.monitoringDashboard.showEmptyState = false;
+ });
+
+ it('does not show the variables section', () => {
+ createShallowWrapper();
+ const allInputs = findTextInput().length + findCustomInput().length;
+
+ expect(allInputs).toBe(0);
+ });
+
+ it('shows the variables section', () => {
+ createShallowWrapper();
+ store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
+
+ return wrapper.vm.$nextTick(() => {
+ const allInputs = findTextInput().length + findCustomInput().length;
+
+ expect(allInputs).toBe(Object.keys(sampleVariables).length);
+ });
+ });
+
+ describe('when changing the variable inputs', () => {
+ const fetchDashboardData = jest.fn();
+ const updateVariableValues = jest.fn();
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ modules: {
+ monitoringDashboard: {
+ namespaced: true,
+ state: {
+ showEmptyState: false,
+ promVariables: sampleVariables,
+ },
+ actions: {
+ fetchDashboardData,
+ updateVariableValues,
+ },
+ },
+ },
+ });
+
+ createShallowWrapper();
+ });
+
+ it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => {
+ const firstInput = findTextInput().at(0);
+
+ firstInput.vm.$emit('onUpdate', 'label1', 'test');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(updateVariableValues).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(
+ convertVariablesForURL(sampleVariables),
+ window.location.href,
+ );
+ expect(updateHistory).toHaveBeenCalled();
+ expect(fetchDashboardData).toHaveBeenCalled();
+ });
+ });
+
+ it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => {
+ const firstInput = findCustomInput().at(0);
+
+ firstInput.vm.$emit('onUpdate', 'label1', 'test');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(updateVariableValues).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(
+ convertVariablesForURL(sampleVariables),
+ window.location.href,
+ );
+ expect(updateHistory).toHaveBeenCalled();
+ expect(fetchDashboardData).toHaveBeenCalled();
+ });
+ });
+
+ 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);
+
+ firstInput.vm.$emit('onUpdate', 'label1', 'Simple text');
+
+ expect(updateVariableValues).not.toHaveBeenCalled();
+ expect(mergeUrlParams).not.toHaveBeenCalled();
+ expect(updateHistory).not.toHaveBeenCalled();
+ expect(fetchDashboardData).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 56236918c68..4611e6f1b18 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -34,6 +34,7 @@ const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({
system_dashboard: false,
project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`,
path: `.gitlab/dashboards/dashboard_${idx}.yml`,
+ starred: false,
}));
export const mockDashboardsErrorResponse = {
@@ -323,6 +324,18 @@ export const dashboardGitResponse = [
system_dashboard: true,
project_blob_path: null,
path: 'config/prometheus/common_metrics.yml',
+ starred: false,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/common_metrics.yml`,
+ },
+ {
+ default: false,
+ display_name: 'dashboard.yml',
+ can_edit: true,
+ system_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`,
},
...customDashboardsData,
];
@@ -341,7 +354,7 @@ export const metricsResult = [
},
];
-export const graphDataPrometheusQuery = {
+export const singleStatMetricsResult = {
title: 'Super Chart A2',
type: 'single-stat',
weight: 2,
@@ -489,7 +502,7 @@ export const stackedColumnMockedData = {
export const barMockData = {
title: 'SLA Trends - Primary Services',
- type: 'bar-chart',
+ type: 'bar',
xLabel: 'service',
y_label: 'percentile',
metrics: [
@@ -549,3 +562,217 @@ export const mockNamespacedData = {
export const mockLogsPath = '/mockLogsPath';
export const mockLogsHref = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`;
+
+const templatingVariableTypes = {
+ text: {
+ simple: 'Simple text',
+ advanced: {
+ label: 'Variable 4',
+ type: 'text',
+ options: {
+ default_value: 'default',
+ },
+ },
+ },
+ 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: {},
+ },
+ withoutLabel: {
+ 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,
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+const generateMockTemplatingData = data => {
+ const vars = data
+ ? {
+ variables: {
+ ...data,
+ },
+ }
+ : {};
+ return {
+ dashboard: {
+ templating: vars,
+ },
+ };
+};
+
+const responseForSimpleTextVariable = {
+ simpleText: {
+ label: 'simpleText',
+ type: 'text',
+ value: 'Simple text',
+ },
+};
+
+const responseForAdvTextVariable = {
+ advText: {
+ label: 'Variable 4',
+ type: 'text',
+ value: 'default',
+ },
+};
+
+const responseForSimpleCustomVariable = {
+ simpleCustom: {
+ label: 'simpleCustom',
+ value: 'value1',
+ options: [
+ {
+ default: false,
+ text: 'value1',
+ value: 'value1',
+ },
+ {
+ default: false,
+ text: 'value2',
+ value: 'value2',
+ },
+ {
+ default: false,
+ text: 'value3',
+ value: 'value3',
+ },
+ ],
+ type: 'custom',
+ },
+};
+
+const responseForAdvancedCustomVariableWithoutOptions = {
+ advCustomWithoutOpts: {
+ label: 'advCustomWithoutOpts',
+ options: [],
+ type: 'custom',
+ },
+};
+
+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',
+ },
+};
+
+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',
+ },
+};
+
+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,
+ }),
+ 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 mockTemplatingDataResponses = {
+ emptyTemplatingProp: {},
+ emptyVariablesProp: {},
+ simpleText: responseForSimpleTextVariable,
+ advText: responseForAdvTextVariable,
+ simpleCustom: responseForSimpleCustomVariable,
+ advCustomWithoutOpts: responseForAdvancedCustomVariableWithoutOptions,
+ advCustomWithoutType: {},
+ advCustomWithoutLabel: responseForAdvancedCustomVariableWithoutLabel,
+ simpleAndAdv: responseForAdvancedCustomVariable,
+ allVariableTypes: responsesForAllVariableTypes,
+};
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index f312aa1fd34..8914f2e66ea 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -11,17 +11,22 @@ import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import store from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import {
+ fetchData,
fetchDashboard,
receiveMetricsDashboardSuccess,
fetchDeploymentsData,
fetchEnvironmentsData,
fetchDashboardData,
fetchAnnotations,
+ toggleStarredValue,
fetchPrometheusMetric,
setInitialState,
filterEnvironments,
+ setExpandedPanel,
+ clearExpandedPanel,
setGettingStartedEmptyState,
duplicateSystemDashboard,
+ updateVariableValues,
} from '~/monitoring/stores/actions';
import {
gqClient,
@@ -35,6 +40,7 @@ import {
deploymentData,
environmentData,
annotationsData,
+ mockTemplatingData,
dashboardGitResponse,
mockDashboardsErrorResponse,
} from '../mock_data';
@@ -62,9 +68,6 @@ describe('Monitoring store actions', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- // Mock `backOff` function to remove exponential algorithm delay.
- jest.useFakeTimers();
-
jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
const q = new Promise((resolve, reject) => {
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
@@ -87,6 +90,45 @@ describe('Monitoring store actions', () => {
createFlash.mockReset();
});
+ describe('fetchData', () => {
+ it('dispatches fetchEnvironmentsData and fetchEnvironmentsData', () => {
+ const { state } = store;
+
+ return testAction(
+ fetchData,
+ null,
+ state,
+ [],
+ [
+ { type: 'fetchEnvironmentsData' },
+ { type: 'fetchDashboard' },
+ { type: 'fetchAnnotations' },
+ ],
+ );
+ });
+
+ it('dispatches when feature metricsDashboardAnnotations is on', () => {
+ const origGon = window.gon;
+ window.gon = { features: { metricsDashboardAnnotations: true } };
+
+ const { state } = store;
+
+ return testAction(
+ fetchData,
+ null,
+ state,
+ [],
+ [
+ { type: 'fetchEnvironmentsData' },
+ { type: 'fetchDashboard' },
+ { type: 'fetchAnnotations' },
+ ],
+ ).then(() => {
+ window.gon = origGon;
+ });
+ });
+ });
+
describe('fetchDeploymentsData', () => {
it('dispatches receiveDeploymentsDataSuccess on success', () => {
const { state } = store;
@@ -310,6 +352,49 @@ describe('Monitoring store actions', () => {
});
});
+ describe('Toggles starred value of current dashboard', () => {
+ const { state } = store;
+ 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: true },
+ ]);
+ });
+
+ 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', () => {
let mockedState;
beforeEach(() => {
@@ -357,6 +442,29 @@ describe('Monitoring store actions', () => {
);
});
});
+
+ describe('updateVariableValues', () => {
+ let mockedState;
+ beforeEach(() => {
+ mockedState = storeState();
+ });
+ it('should commit UPDATE_VARIABLE_VALUES mutation', done => {
+ testAction(
+ updateVariableValues,
+ { pod: 'POD' },
+ mockedState,
+ [
+ {
+ type: types.UPDATE_VARIABLE_VALUES,
+ payload: { pod: 'POD' },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
describe('fetchDashboard', () => {
let dispatch;
let state;
@@ -467,6 +575,33 @@ 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;
@@ -873,4 +1008,43 @@ describe('Monitoring store actions', () => {
});
});
});
+
+ describe('setExpandedPanel', () => {
+ let state;
+
+ beforeEach(() => {
+ state = storeState();
+ });
+
+ it('Sets a panel as expanded', () => {
+ const group = 'group_1';
+ const panel = { title: 'A Panel' };
+
+ return testAction(
+ setExpandedPanel,
+ { group, panel },
+ state,
+ [{ type: types.SET_EXPANDED_PANEL, payload: { group, panel } }],
+ [],
+ );
+ });
+ });
+
+ describe('clearExpandedPanel', () => {
+ let state;
+
+ beforeEach(() => {
+ state = storeState();
+ });
+
+ it('Clears a panel as expanded', () => {
+ return testAction(
+ clearExpandedPanel,
+ undefined,
+ state,
+ [{ type: types.SET_EXPANDED_PANEL, payload: { group: null, panel: null } }],
+ [],
+ );
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index f040876b832..365052e68e3 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -3,7 +3,12 @@ import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
-import { environmentData, metricsResult } from '../mock_data';
+import {
+ environmentData,
+ metricsResult,
+ dashboardGitResponse,
+ mockTemplatingDataResponses,
+} from '../mock_data';
import {
metricsDashboardPayload,
metricResultStatus,
@@ -323,4 +328,81 @@ describe('Monitoring store Getters', () => {
expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]);
});
});
+
+ describe('getCustomVariablesArray', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ promVariables: {},
+ };
+ });
+
+ it('transforms the promVariables object to an array in the [variable, variable_value] format for all variable types', () => {
+ mutations[types.SET_VARIABLES](state, mockTemplatingDataResponses.allVariableTypes);
+ const variablesArray = getters.getCustomVariablesArray(state);
+
+ expect(variablesArray).toEqual([
+ 'simpleText',
+ 'Simple text',
+ 'advText',
+ 'default',
+ 'simpleCustom',
+ 'value1',
+ 'advCustomNormal',
+ 'value2',
+ ]);
+ });
+
+ it('transforms the promVariables object to an empty array when no keys are present', () => {
+ mutations[types.SET_VARIABLES](state, {});
+ const variablesArray = getters.getCustomVariablesArray(state);
+
+ expect(variablesArray).toEqual([]);
+ });
+ });
+
+ describe('selectedDashboard', () => {
+ const { selectedDashboard } = getters;
+
+ it('returns a dashboard', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: dashboardGitResponse[0].path,
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ });
+
+ it('returns a non-default dashboard', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: dashboardGitResponse[1].path,
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[1]);
+ });
+
+ it('returns a default dashboard when no dashboard is selected', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: null,
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ });
+
+ it('returns a default dashboard when dashboard cannot be found', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: 'wrong_path',
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ });
+
+ it('returns null when no dashboards are present', () => {
+ const state = {
+ allDashboards: [],
+ currentDashboard: dashboardGitResponse[0].path,
+ };
+ expect(selectedDashboard(state)).toEqual(null);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 1452e9bc491..4306243689a 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -72,6 +72,49 @@ describe('Monitoring mutations', () => {
});
});
+ describe('Dashboard starring mutations', () => {
+ it('REQUEST_DASHBOARD_STARRING', () => {
+ stateCopy = { isUpdatingStarredValue: false };
+ mutations[types.REQUEST_DASHBOARD_STARRING](stateCopy);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(true);
+ });
+
+ describe('RECEIVE_DASHBOARD_STARRING_SUCCESS', () => {
+ let allDashboards;
+
+ beforeEach(() => {
+ allDashboards = [...dashboardGitResponse];
+ stateCopy = {
+ allDashboards,
+ currentDashboard: allDashboards[1].path,
+ isUpdatingStarredValue: true,
+ };
+ });
+
+ it('sets a dashboard as starred', () => {
+ mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, true);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(false);
+ expect(stateCopy.allDashboards[1].starred).toBe(true);
+ });
+
+ it('sets a dashboard as unstarred', () => {
+ mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, false);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(false);
+ expect(stateCopy.allDashboards[1].starred).toBe(false);
+ });
+ });
+
+ it('RECEIVE_DASHBOARD_STARRING_FAILURE', () => {
+ stateCopy = { isUpdatingStarredValue: true };
+ mutations[types.RECEIVE_DASHBOARD_STARRING_FAILURE](stateCopy);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(false);
+ });
+ });
+
describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => {
it('stores the deployment data', () => {
stateCopy.deploymentData = [];
@@ -342,4 +385,53 @@ describe('Monitoring mutations', () => {
expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
});
});
+
+ describe('SET_EXPANDED_PANEL', () => {
+ it('no expanded panel is set initally', () => {
+ expect(stateCopy.expandedPanel.panel).toEqual(null);
+ expect(stateCopy.expandedPanel.group).toEqual(null);
+ });
+
+ it('sets a panel id as the expanded panel', () => {
+ const group = 'group_1';
+ const panel = { title: 'A Panel' };
+ mutations[types.SET_EXPANDED_PANEL](stateCopy, { group, panel });
+
+ expect(stateCopy.expandedPanel).toEqual({ group, panel });
+ });
+
+ it('clears panel as the expanded panel', () => {
+ mutations[types.SET_EXPANDED_PANEL](stateCopy, { group: null, panel: null });
+
+ expect(stateCopy.expandedPanel.group).toEqual(null);
+ expect(stateCopy.expandedPanel.panel).toEqual(null);
+ });
+ });
+
+ describe('SET_VARIABLES', () => {
+ it('stores an empty variables array when no custom variables are given', () => {
+ mutations[types.SET_VARIABLES](stateCopy, {});
+
+ expect(stateCopy.promVariables).toEqual({});
+ });
+
+ it('stores variables in the key key_value format in the array', () => {
+ mutations[types.SET_VARIABLES](stateCopy, { pod: 'POD', stage: 'main ops' });
+
+ expect(stateCopy.promVariables).toEqual({ pod: 'POD', stage: 'main ops' });
+ });
+ });
+
+ describe('UPDATE_VARIABLE_VALUES', () => {
+ afterEach(() => {
+ mutations[types.SET_VARIABLES](stateCopy, {});
+ });
+
+ it('updates only the value of the variable in promVariables', () => {
+ mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } });
+ mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { key: 'environment', value: 'new prod' });
+
+ expect(stateCopy.promVariables).toEqual({ environment: { value: 'new prod', type: 'text' } });
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 7ee2a16b4bd..fe5754e1216 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -27,6 +27,7 @@ describe('mapToDashboardViewModel', () => {
group: 'Group 1',
panels: [
{
+ id: 'ID_ABC',
title: 'Title A',
xLabel: '',
xAxis: {
@@ -49,6 +50,7 @@ describe('mapToDashboardViewModel', () => {
key: 'group-1-0',
panels: [
{
+ id: 'ID_ABC',
title: 'Title A',
type: 'chart-type',
xLabel: '',
@@ -127,11 +129,13 @@ describe('mapToDashboardViewModel', () => {
it('panel with x_label', () => {
setupWithPanel({
+ id: 'ID_123',
title: panelTitle,
x_label: 'x label',
});
expect(getMappedPanel()).toEqual({
+ id: 'ID_123',
title: panelTitle,
xLabel: 'x label',
xAxis: {
@@ -149,10 +153,12 @@ describe('mapToDashboardViewModel', () => {
it('group y_axis defaults', () => {
setupWithPanel({
+ id: 'ID_456',
title: panelTitle,
});
expect(getMappedPanel()).toEqual({
+ id: 'ID_456',
title: panelTitle,
xLabel: '',
y_label: '',
diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js
new file mode 100644
index 00000000000..47681ac7c65
--- /dev/null
+++ b/spec/frontend/monitoring/store/variable_mapping_spec.js
@@ -0,0 +1,22 @@
+import { parseTemplatingVariables } from '~/monitoring/stores/variable_mapping';
+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 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);
+ });
+});
diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js
index d764a79ccc3..338af79dbbe 100644
--- a/spec/frontend/monitoring/store_utils.js
+++ b/spec/frontend/monitoring/store_utils.js
@@ -1,34 +1,49 @@
import * as types from '~/monitoring/stores/mutation_types';
-import { metricsResult, environmentData } from './mock_data';
+import { metricsResult, environmentData, dashboardGitResponse } from './mock_data';
import { metricsDashboardPayload } from './fixture_data';
-export const setMetricResult = ({ $store, result, group = 0, panel = 0, metric = 0 }) => {
- const { dashboard } = $store.state.monitoringDashboard;
+export const setMetricResult = ({ store, result, group = 0, panel = 0, metric = 0 }) => {
+ const { dashboard } = store.state.monitoringDashboard;
const { metricId } = dashboard.panelGroups[group].panels[panel].metrics[metric];
- $store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, {
+ store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, {
metricId,
result,
});
};
-const setEnvironmentData = $store => {
- $store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData);
+const setEnvironmentData = store => {
+ store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData);
};
-export const setupStoreWithDashboard = $store => {
- $store.commit(
+export const setupAllDashboards = store => {
+ store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, dashboardGitResponse);
+};
+
+export const setupStoreWithDashboard = store => {
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
+ metricsDashboardPayload,
+ );
+ store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
metricsDashboardPayload,
);
};
-export const setupStoreWithData = $store => {
- setupStoreWithDashboard($store);
+export const setupStoreWithVariable = store => {
+ store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, {
+ label1: 'pod',
+ });
+};
+
+export const setupStoreWithData = store => {
+ setupAllDashboards(store);
+ setupStoreWithDashboard(store);
- setMetricResult({ $store, result: [], panel: 0 });
- setMetricResult({ $store, result: metricsResult, panel: 1 });
- setMetricResult({ $store, result: metricsResult, panel: 2 });
+ setMetricResult({ store, result: [], panel: 0 });
+ setMetricResult({ store, result: metricsResult, panel: 1 });
+ setMetricResult({ store, result: metricsResult, panel: 2 });
- setEnvironmentData($store);
+ setEnvironmentData(store);
};
diff --git a/spec/frontend/monitoring/stubs/modal_stub.js b/spec/frontend/monitoring/stubs/modal_stub.js
new file mode 100644
index 00000000000..4cd0362096e
--- /dev/null
+++ b/spec/frontend/monitoring/stubs/modal_stub.js
@@ -0,0 +1,11 @@
+const ModalStub = {
+ name: 'glmodal-stub',
+ template: `
+ <div>
+ <slot></slot>
+ <slot name="modal-ok"></slot>
+ </div>
+ `,
+};
+
+export default ModalStub;
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index 0bb1b987b2e..aa5a4459a72 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -1,15 +1,13 @@
import * as monitoringUtils from '~/monitoring/utils';
-import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
+import * as urlUtils from '~/lib/utils/url_utility';
import { TEST_HOST } from 'jest/helpers/test_constants';
import {
mockProjectDir,
- graphDataPrometheusQuery,
+ singleStatMetricsResult,
anomalyMockGraphData,
barMockData,
} from './mock_data';
-import { graphData } from './fixture_data';
-
-jest.mock('~/lib/utils/url_utility');
+import { metricsDashboardViewModel, graphData } from './fixture_data';
const mockPath = `${TEST_HOST}${mockProjectDir}/-/environments/29/metrics`;
@@ -27,11 +25,6 @@ const rollingRange = {
};
describe('monitoring/utils', () => {
- afterEach(() => {
- mergeUrlParams.mockReset();
- queryToObject.mockReset();
- });
-
describe('trackGenerateLinkToChartEventOptions', () => {
it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
document.body.dataset.page = 'groups:clusters:show';
@@ -89,7 +82,7 @@ describe('monitoring/utils', () => {
it('validates data with the query format', () => {
const validGraphData = monitoringUtils.graphDataValidatorForValues(
true,
- graphDataPrometheusQuery,
+ singleStatMetricsResult,
);
expect(validGraphData).toBe(true);
@@ -112,7 +105,7 @@ describe('monitoring/utils', () => {
let threeMetrics;
let fourMetrics;
beforeEach(() => {
- oneMetric = graphDataPrometheusQuery;
+ oneMetric = singleStatMetricsResult;
threeMetrics = anomalyMockGraphData;
const metrics = [...threeMetrics.metrics];
@@ -139,18 +132,25 @@ describe('monitoring/utils', () => {
});
describe('timeRangeFromUrl', () => {
- const { timeRangeFromUrl } = monitoringUtils;
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'queryToObject');
+ });
+
+ afterEach(() => {
+ urlUtils.queryToObject.mockRestore();
+ });
- it('returns a fixed range when query contains `start` and `end` paramters are given', () => {
- queryToObject.mockReturnValueOnce(range);
+ const { timeRangeFromUrl } = monitoringUtils;
+ it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
+ urlUtils.queryToObject.mockReturnValueOnce(range);
expect(timeRangeFromUrl()).toEqual(range);
});
- it('returns a rolling range when query contains `duration_seconds` paramters are given', () => {
+ it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
const { seconds } = rollingRange.duration;
- queryToObject.mockReturnValueOnce({
+ urlUtils.queryToObject.mockReturnValueOnce({
dashboard: '.gitlab/dashboard/my_dashboard.yml',
duration_seconds: `${seconds}`,
});
@@ -158,23 +158,59 @@ describe('monitoring/utils', () => {
expect(timeRangeFromUrl()).toEqual(rollingRange);
});
- it('returns null when no time range paramters are given', () => {
- const params = {
+ it('returns null when no time range parameters are given', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({
dashboard: '.gitlab/dashboards/custom_dashboard.yml',
param1: 'value1',
param2: 'value2',
- };
+ });
- expect(timeRangeFromUrl(params, mockPath)).toBe(null);
+ expect(timeRangeFromUrl()).toBe(null);
+ });
+ });
+
+ describe('getPromCustomVariablesFromUrl', () => {
+ const { getPromCustomVariablesFromUrl } = monitoringUtils;
+
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'queryToObject');
+ });
+
+ afterEach(() => {
+ urlUtils.queryToObject.mockRestore();
+ });
+
+ it('returns an object with only the custom variables', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({
+ dashboard: '.gitlab/dashboards/custom_dashboard.yml',
+ y_label: 'memory usage',
+ group: 'kubernetes',
+ title: 'Kubernetes memory total',
+ start: '2020-05-06',
+ end: '2020-05-07',
+ duration_seconds: '86400',
+ direction: 'left',
+ anchor: 'top',
+ pod: 'POD',
+ 'var-pod': 'POD',
+ });
+
+ expect(getPromCustomVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' }));
+ });
+
+ it('returns an empty object when no custom variables are present', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({
+ dashboard: '.gitlab/dashboards/custom_dashboard.yml',
+ });
+
+ expect(getPromCustomVariablesFromUrl()).toStrictEqual({});
});
});
describe('removeTimeRangeParams', () => {
const { removeTimeRangeParams } = monitoringUtils;
- it('returns when query contains `start` and `end` paramters are given', () => {
- removeParams.mockReturnValueOnce(mockPath);
-
+ it('returns when query contains `start` and `end` parameters are given', () => {
expect(removeTimeRangeParams(`${mockPath}?start=${range.start}&end=${range.end}`)).toEqual(
mockPath,
);
@@ -184,28 +220,126 @@ describe('monitoring/utils', () => {
describe('timeRangeToUrl', () => {
const { timeRangeToUrl } = monitoringUtils;
- it('returns a fixed range when query contains `start` and `end` paramters are given', () => {
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'mergeUrlParams');
+ jest.spyOn(urlUtils, 'removeParams');
+ });
+
+ afterEach(() => {
+ urlUtils.mergeUrlParams.mockRestore();
+ urlUtils.removeParams.mockRestore();
+ });
+
+ it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
const toUrl = `${mockPath}?start=${range.start}&end=${range.end}`;
const fromUrl = mockPath;
- removeParams.mockReturnValueOnce(fromUrl);
- mergeUrlParams.mockReturnValueOnce(toUrl);
+ urlUtils.removeParams.mockReturnValueOnce(fromUrl);
+ urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
expect(timeRangeToUrl(range)).toEqual(toUrl);
- expect(mergeUrlParams).toHaveBeenCalledWith(range, fromUrl);
+ expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(range, fromUrl);
});
- it('returns a rolling range when query contains `duration_seconds` paramters are given', () => {
+ it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
const { seconds } = rollingRange.duration;
const toUrl = `${mockPath}?duration_seconds=${seconds}`;
const fromUrl = mockPath;
- removeParams.mockReturnValueOnce(fromUrl);
- mergeUrlParams.mockReturnValueOnce(toUrl);
+ urlUtils.removeParams.mockReturnValueOnce(fromUrl);
+ urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
expect(timeRangeToUrl(rollingRange)).toEqual(toUrl);
- expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: `${seconds}` }, fromUrl);
+ expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(
+ { duration_seconds: `${seconds}` },
+ fromUrl,
+ );
+ });
+ });
+
+ describe('expandedPanelPayloadFromUrl', () => {
+ const { expandedPanelPayloadFromUrl } = monitoringUtils;
+ const [panelGroup] = metricsDashboardViewModel.panelGroups;
+ const [panel] = panelGroup.panels;
+
+ const { group } = panelGroup;
+ const { title, y_label: yLabel } = panel;
+
+ it('returns payload for a panel when query parameters are given', () => {
+ const search = `?group=${group}&title=${title}&y_label=${yLabel}`;
+
+ expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toEqual({
+ group: panelGroup.group,
+ panel,
+ });
+ });
+
+ it('returns null when no parameters are given', () => {
+ expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, '')).toBe(null);
+ });
+
+ it('throws an error when no group is provided', () => {
+ const search = `?title=${panel.title}&y_label=${yLabel}`;
+ expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
+ });
+
+ it('throws an error when no title is provided', () => {
+ const search = `?title=${title}&y_label=${yLabel}`;
+ expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
+ });
+
+ it('throws an error when no y_label group is provided', () => {
+ const search = `?group=${group}&title=${title}`;
+ expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
+ });
+
+ test.each`
+ group | title | yLabel | missingField
+ ${'NOT_A_GROUP'} | ${title} | ${yLabel} | ${'group'}
+ ${group} | ${'NOT_A_TITLE'} | ${yLabel} | ${'title'}
+ ${group} | ${title} | ${'NOT_A_Y_LABEL'} | ${'y_label'}
+ `('throws an error when $missingField is incorrect', params => {
+ const search = `?group=${params.group}&title=${params.title}&y_label=${params.yLabel}`;
+ expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
+ });
+ });
+
+ describe('panelToUrl', () => {
+ const { panelToUrl } = monitoringUtils;
+
+ const dashboard = 'metrics.yml';
+ const [panelGroup] = metricsDashboardViewModel.panelGroups;
+ const [panel] = panelGroup.panels;
+
+ const getUrlParams = url => urlUtils.queryToObject(url.split('?')[1]);
+
+ it('returns URL for a panel when query parameters are given', () => {
+ const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, panel));
+
+ expect(params).toEqual(
+ expect.objectContaining({
+ dashboard,
+ group: panelGroup.group,
+ title: panel.title,
+ y_label: panel.y_label,
+ }),
+ );
+ });
+
+ it('returns a dashboard only URL if group is missing', () => {
+ const params = getUrlParams(panelToUrl(dashboard, {}, null, panel));
+ expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
+ });
+
+ it('returns a dashboard only URL if panel is missing', () => {
+ const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, null));
+ expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
+ });
+
+ it('returns URL for a panel when query paramters are given including custom variables', () => {
+ const params = getUrlParams(panelToUrl(dashboard, { pod: 'pod' }, panelGroup.group, null));
+ expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml', pod: 'pod' }));
});
});
@@ -271,4 +405,108 @@ describe('monitoring/utils', () => {
});
});
});
+
+ describe('removePrefixFromLabel', () => {
+ it.each`
+ input | expected
+ ${undefined} | ${''}
+ ${null} | ${''}
+ ${''} | ${''}
+ ${' '} | ${' '}
+ ${'pod-1'} | ${'pod-1'}
+ ${'pod-var-1'} | ${'pod-var-1'}
+ ${'pod-1-var'} | ${'pod-1-var'}
+ ${'podvar--1'} | ${'podvar--1'}
+ ${'povar-d-1'} | ${'povar-d-1'}
+ ${'var-pod-1'} | ${'pod-1'}
+ ${'var-var-pod-1'} | ${'var-pod-1'}
+ ${'varvar-pod-1'} | ${'varvar-pod-1'}
+ ${'var-pod-1-var-'} | ${'pod-1-var-'}
+ `('removePrefixFromLabel returns $expected with input $input', ({ input, expected }) => {
+ expect(monitoringUtils.removePrefixFromLabel(input)).toEqual(expected);
+ });
+ });
+
+ describe('mergeURLVariables', () => {
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'queryToObject');
+ });
+
+ afterEach(() => {
+ urlUtils.queryToObject.mockRestore();
+ });
+
+ it('returns empty object if variables are not defined in yml or URL', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({});
+
+ expect(monitoringUtils.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',
+ });
+
+ expect(monitoringUtils.mergeURLVariables({})).toEqual({});
+ });
+
+ it('returns yml variables if variables defined in yml but not in the URL', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({});
+
+ const params = {
+ env: 'one',
+ instance: 'localhost',
+ };
+
+ expect(monitoringUtils.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 ymlParams = {
+ pod: { value: 'one' },
+ service: { value: 'database' },
+ };
+ urlUtils.queryToObject.mockReturnValueOnce(urlParams);
+
+ expect(monitoringUtils.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 ymlParams = {
+ instance: { value: 'localhost' },
+ service: { value: 'database' },
+ };
+
+ const merged = {
+ instance: { value: 'localhost:8080' },
+ service: { value: 'database' },
+ };
+
+ urlUtils.queryToObject.mockReturnValueOnce(urlParams);
+
+ expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(merged);
+ });
+ });
+
+ describe('convertVariablesForURL', () => {
+ it.each`
+ input | expected
+ ${undefined} | ${{}}
+ ${null} | ${{}}
+ ${{}} | ${{}}
+ ${{ env: { value: 'prod' } }} | ${{ 'var-env': 'prod' }}
+ ${{ 'var-env': { value: 'prod' } }} | ${{ 'var-var-env': 'prod' }}
+ `('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => {
+ expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/validators_spec.js b/spec/frontend/monitoring/validators_spec.js
new file mode 100644
index 00000000000..0c3d77a7d98
--- /dev/null
+++ b/spec/frontend/monitoring/validators_spec.js
@@ -0,0 +1,80 @@
+import { alertsValidator, queriesValidator } from '~/monitoring/validators';
+
+describe('alertsValidator', () => {
+ const validAlert = {
+ alert_path: 'my/alert.json',
+ operator: '<',
+ threshold: 5,
+ metricId: '8',
+ };
+ it('requires all alerts to have an alert path', () => {
+ const { operator, threshold, metricId } = validAlert;
+ const input = {
+ [validAlert.alert_path]: {
+ operator,
+ threshold,
+ metricId,
+ },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires that the object key matches the alert path', () => {
+ const input = {
+ undefined: validAlert,
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires all alerts to have a metric id', () => {
+ const input = {
+ [validAlert.alert_path]: { ...validAlert, metricId: undefined },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires the metricId to be a string', () => {
+ const input = {
+ [validAlert.alert_path]: { ...validAlert, metricId: 8 },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires all alerts to have an operator', () => {
+ const input = {
+ [validAlert.alert_path]: { ...validAlert, operator: '' },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires all alerts to have an numeric threshold', () => {
+ const input = {
+ [validAlert.alert_path]: { ...validAlert, threshold: '60' },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('correctly identifies a valid alerts object', () => {
+ const input = {
+ [validAlert.alert_path]: validAlert,
+ };
+ expect(alertsValidator(input)).toEqual(true);
+ });
+});
+describe('queriesValidator', () => {
+ const validQuery = {
+ metricId: '8',
+ alert_path: 'alert',
+ label: 'alert-label',
+ };
+ it('requires all alerts to have a metric id', () => {
+ const input = [{ ...validQuery, metricId: undefined }];
+ expect(queriesValidator(input)).toEqual(false);
+ });
+ it('requires the metricId to be a string', () => {
+ const input = [{ ...validQuery, metricId: 8 }];
+ expect(queriesValidator(input)).toEqual(false);
+ });
+ it('requires all queries to have a label', () => {
+ const input = [{ ...validQuery, label: undefined }];
+ expect(queriesValidator(input)).toEqual(false);
+ });
+ it('correctly identifies a valid queries array', () => {
+ const input = [validQuery];
+ expect(queriesValidator(input)).toEqual(true);
+ });
+});