summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel Rincon <mrincon@gitlab.com>2019-09-11 14:57:30 +0200
committerMiguel Rincon <mrincon@gitlab.com>2019-09-11 14:57:30 +0200
commitb7715c54b4c7f748d947fab4ed4a7b9ac1264131 (patch)
tree717c5128a68268fde3ac2008003e0eded0ff3d28
parent7eda144e3e6557b7f42d02a1eeff157d580ca9d1 (diff)
downloadgitlab-ce-5366-display-anomaly-deviation-boundaries-on-dashboard-ce-integration.tar.gz
- Add anomaly chart to panel types - Refactor dropdown into a single component - Add specs
-rw-r--r--app/assets/javascripts/monitoring/components/chart_dropdown.vue78
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue63
-rw-r--r--spec/frontend/monitoring/components/chart_dropdown_spec.js110
-rw-r--r--spec/javascripts/monitoring/panel_type_spec.js47
4 files changed, 255 insertions, 43 deletions
diff --git a/app/assets/javascripts/monitoring/components/chart_dropdown.vue b/app/assets/javascripts/monitoring/components/chart_dropdown.vue
new file mode 100644
index 00000000000..5b201b803c6
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/chart_dropdown.vue
@@ -0,0 +1,78 @@
+<script>
+import { __ } from '~/locale';
+import { GlDropdown, GlDropdownItem, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ csvText: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ chartLink: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ alertModalId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ csvHref() {
+ const data = new Blob([this.csvText], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
+ },
+ methods: {
+ showToast() {
+ this.$toast.show(__('Link copied to clipboard'));
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown
+ v-gl-tooltip
+ class="mx-2"
+ toggle-class="btn btn-transparent border-0"
+ :right="true"
+ :no-caret="true"
+ :title="__('More actions')"
+ >
+ <template slot="button-content">
+ <icon name="ellipsis_v" class="text-secondary" />
+ </template>
+ <gl-dropdown-item
+ v-if="csvText"
+ :href="csvHref"
+ download="chart_metrics.csv"
+ class="js-csv-dl-link"
+ >
+ {{ __('Download CSV') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="chartLink"
+ :data-clipboard-text="chartLink"
+ class="js-chart-link"
+ @click="showToast"
+ >
+ {{ __('Generate link to chart') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="alertModalId" v-gl-modal="alertModalId" class="js-alert-link">
+ {{ __('Alerts') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 73ff651d510..0ff7561c110 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -11,14 +11,18 @@ import {
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
+import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
+import MonitorChartDropdown from './chart_dropdown.vue';
export default {
components: {
MonitorSingleStatChart,
MonitorTimeSeriesChart,
+ MonitorAnomalyChart,
MonitorEmptyChart,
+ MonitorChartDropdown,
Icon,
GlDropdown,
GlDropdownItem,
@@ -64,10 +68,6 @@ export default {
return `${csv}${row}\r\n`;
}, header);
},
- downloadCsv() {
- const data = new Blob([this.csvText], { type: 'text/plain' });
- return window.URL.createObjectURL(data);
- },
},
methods: {
getGraphAlerts(queries) {
@@ -92,6 +92,31 @@ export default {
v-if="isPanelType('single-stat') && graphDataHasMetrics"
:graph-data="graphData"
/>
+ <monitor-anomaly-chart
+ v-else-if="isPanelType('anomaly') && graphDataHasMetrics"
+ :graph-data="graphData"
+ :deployment-data="deploymentData"
+ :project-path="projectPath"
+ :thresholds="getGraphAlertValues(graphData.queries)"
+ :container-width="dashboardWidth"
+ group-id="monitor-anomaly-chart"
+ >
+ <div class="d-flex align-items-center">
+ <alert-widget
+ v-if="alertWidgetAvailable && graphData"
+ :modal-id="`alert-modal-${index}`"
+ :alerts-endpoint="alertsEndpoint"
+ :relevant-queries="graphData.queries"
+ :alerts-to-manage="getGraphAlerts(graphData.queries)"
+ @setAlerts="setAlerts"
+ />
+ <monitor-chart-dropdown
+ :csv-text="csvText"
+ :chart-link="clipboardText"
+ :alert-modal-id="alertWidgetAvailable ? `alert-modal-${index}` : null"
+ />
+ </div>
+ </monitor-anomaly-chart>
<monitor-time-series-chart
v-else-if="graphDataHasMetrics"
:graph-data="graphData"
@@ -110,31 +135,11 @@ export default {
:alerts-to-manage="getGraphAlerts(graphData.queries)"
@setAlerts="setAlerts"
/>
- <gl-dropdown
- v-gl-tooltip
- class="mx-2"
- toggle-class="btn btn-transparent border-0"
- :right="true"
- :no-caret="true"
- :title="__('More actions')"
- >
- <template slot="button-content">
- <icon name="ellipsis_v" class="text-secondary" />
- </template>
- <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv">
- {{ __('Download CSV') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- class="js-chart-link"
- :data-clipboard-text="clipboardText"
- @click="showToast"
- >
- {{ __('Generate link to chart') }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`">
- {{ __('Alerts') }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <monitor-chart-dropdown
+ :csv-text="csvText"
+ :chart-link="clipboardText"
+ :alert-modal-id="alertWidgetAvailable ? `alert-modal-${index}` : null"
+ />
</div>
</monitor-time-series-chart>
<monitor-empty-chart v-else :graph-title="graphData.title" />
diff --git a/spec/frontend/monitoring/components/chart_dropdown_spec.js b/spec/frontend/monitoring/components/chart_dropdown_spec.js
new file mode 100644
index 00000000000..f45dbef94e2
--- /dev/null
+++ b/spec/frontend/monitoring/components/chart_dropdown_spec.js
@@ -0,0 +1,110 @@
+import MonitorChartDropdown from '~/monitoring/components/chart_dropdown.vue';
+import { shallowMount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
+
+describe('Chart Dropdown component', () => {
+ const glModal = jest.fn((el, binding) => binding.value);
+ let originalCreateObjectURL;
+ let dropdown;
+
+ beforeAll(() => {
+ // createObjectURL is not available yet in jsdom, but support is on the way
+ // see https://github.com/jsdom/jsdom/issues/1721
+ originalCreateObjectURL = window.URL.createObjectURL;
+ window.URL.createObjectURL = window.URL.createObjectURL || (() => {});
+ });
+
+ beforeEach(() => {
+ dropdown = shallowMount(MonitorChartDropdown, {
+ directives: {
+ 'gl-modal': glModal,
+ },
+ mocks: {
+ $toast: {
+ show: jest.fn(),
+ },
+ },
+ });
+ });
+
+ it('renders', () => {
+ expect(dropdown.exists()).toBe(true);
+ expect(dropdown.isVueInstance()).toBe(true);
+ });
+
+ describe('csv download link', () => {
+ const csvText = 'MOCK_CSV_TEXT';
+
+ beforeEach(() => {
+ jest.spyOn(window.URL, 'createObjectURL').mockReturnValue(`blob:${csvText}`);
+ dropdown.setProps({ csvText });
+ });
+
+ it('is displayed', () => {
+ const csvLinkComp = dropdown.find('.js-csv-dl-link');
+ expect(csvLinkComp.isVueInstance()).toBe(true);
+ expect(csvLinkComp.isEmpty()).toBe(false);
+ expect(csvLinkComp.attributes('href')).toEqual(`blob:${csvText}`);
+ });
+
+ afterEach(() => {
+ dropdown.setProps({ csvText: undefined });
+ window.URL.createObjectURL.mockRestore();
+ });
+ });
+
+ describe('chart link', () => {
+ const chartUrl = `${TEST_HOST}/chart`;
+ let chartLink;
+
+ beforeEach(() => {
+ dropdown.setProps({ chartLink: chartUrl });
+ chartLink = dropdown.find('.js-chart-link');
+ jest.spyOn(dropdown.vm.$toast, 'show');
+ });
+
+ it('is displayed', () => {
+ expect(chartLink.isVueInstance()).toBe(true);
+ expect(chartLink.isEmpty()).toBe(false);
+ expect(chartLink.attributes('data-clipboard-text')).toEqual(chartUrl);
+ });
+
+ it('shows a toast on click', () => {
+ chartLink.vm.$emit('click');
+ expect(dropdown.vm.$toast.show).toHaveBeenCalled();
+ });
+
+ afterEach(() => {
+ dropdown.vm.$toast.show.mockReset();
+ dropdown.setProps({ csvText: undefined });
+ });
+ });
+
+ describe('alert link', () => {
+ const alertModalId = `modal-1-2`;
+ let alertLink;
+
+ beforeEach(() => {
+ glModal.mockClear();
+ dropdown.setProps({ alertModalId });
+ alertLink = dropdown.find('.js-alert-link');
+ });
+
+ it('is displayed', () => {
+ expect(alertLink.isVueInstance()).toBe(true);
+ expect(alertLink.isEmpty()).toBe(false);
+ });
+
+ it('can open a modal with correct id', () => {
+ expect(glModal).toHaveReturnedWith(alertModalId);
+ });
+
+ afterEach(() => {
+ dropdown.setProps({ alertModalId: undefined });
+ });
+ });
+
+ afterAll(() => {
+ window.URL.createObjectURL = originalCreateObjectURL;
+ });
+});
diff --git a/spec/javascripts/monitoring/panel_type_spec.js b/spec/javascripts/monitoring/panel_type_spec.js
index a2366e74d43..19a237bd010 100644
--- a/spec/javascripts/monitoring/panel_type_spec.js
+++ b/spec/javascripts/monitoring/panel_type_spec.js
@@ -2,7 +2,9 @@ import { shallowMount } from '@vue/test-utils';
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 { graphDataPrometheusQueryRange } from './mock_data';
+import AnomalyChart from '~/monitoring/components/charts/anomaly.vue';
+import ChartDropdown from '~/monitoring/components/chart_dropdown.vue';
+import { graphDataPrometheusQueryRange, graphDataPrometheusQueryAnomalyResult } from './mock_data';
import { createStore } from '~/monitoring/stores';
describe('Panel Type component', () => {
@@ -52,27 +54,44 @@ describe('Panel Type component', () => {
beforeEach(() => {
store = createStore();
- panelType = shallowMount(PanelType, {
- propsData: {
- clipboardText: exampleText,
- dashboardWidth,
- graphData: graphDataPrometheusQueryRange,
- },
- store,
- });
+ panelType = type =>
+ shallowMount(PanelType, {
+ propsData: {
+ clipboardText: exampleText,
+ dashboardWidth,
+ graphData: { ...graphDataPrometheusQueryAnomalyResult, type },
+ },
+ store,
+ });
});
describe('Time Series Chart panel type', () => {
it('is rendered', () => {
- expect(panelType.find(TimeSeriesChart).isVueInstance()).toBe(true);
- expect(panelType.find(TimeSeriesChart).exists()).toBe(true);
+ const areaPanelType = panelType('area');
+
+ expect(areaPanelType.find(TimeSeriesChart).isVueInstance()).toBe(true);
+ expect(areaPanelType.find(TimeSeriesChart).exists()).toBe(true);
+ });
+
+ it('sets clipboard text on the dropdown', () => {
+ const dropdown = () => panelType('area').find(ChartDropdown);
+
+ expect(dropdown().props('chartLink')).toEqual(exampleText);
+ });
+ });
+
+ describe('Anomaly Chart panel type', () => {
+ it('is rendered', () => {
+ const anomalyChart = panelType('anomaly');
+
+ expect(anomalyChart.find(AnomalyChart).isVueInstance()).toBe(true);
+ expect(anomalyChart.find(AnomalyChart).exists()).toBe(true);
});
it('sets clipboard text on the dropdown', () => {
- const link = () => panelType.find('.js-chart-link');
- const clipboardText = () => link().element.dataset.clipboardText;
+ const dropdown = () => panelType('anomaly').find(ChartDropdown);
- expect(clipboardText()).toBe(exampleText);
+ expect(dropdown().props('chartLink')).toEqual(exampleText);
});
});
});