diff options
10 files changed, 83 insertions, 7 deletions
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index edf9423c74c..5b950f8c966 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -1,6 +1,7 @@ <script> import { __ } from '~/locale'; -import { GlLink } from '@gitlab/ui'; +import { mapState } from 'vuex'; +import { GlLink, GlButton } from '@gitlab/ui'; import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import dateFormat from 'dateformat'; import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils'; @@ -15,6 +16,7 @@ let debouncedResize; export default { components: { GlAreaChart, + GlButton, GlChartSeriesLabel, GlLink, Icon, @@ -67,6 +69,7 @@ export default { }; }, computed: { + ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']), chartData() { // Transforms & supplements query data to render appropriate labels & styles // Input: [{ queryAttributes1 }, { queryAttributes2 }] @@ -176,6 +179,18 @@ export default { yAxisLabel() { return `${this.graphData.y_label}`; }, + csvText() { + const chartData = this.chartData[0].data; + const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadLink() { + const data = new Blob([this.csvText], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, }, watch: { containerWidth: 'onResize', @@ -240,10 +255,20 @@ export default { </script> <template> - <div class="col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']"> - <div class="prometheus-graph" :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> + <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']"> + <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> + <gl-button + v-if="exportMetricsToCsvEnabled" + :href="downloadLink" + :title="__('Download CSV')" + :aria-label="__('Download CSV')" + style="margin-left: 200px;" + download="chart_metrics.csv" + > + {{ __('Download CSV') }} + </gl-button> <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> </div> <gl-area-chart diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index c0fee1ebb99..366034becd0 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -13,6 +13,7 @@ export default (props = {}) => { prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint, multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards, additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes, + exportMetricsToCsvEnabled: gon.features.exportMetricsToCsvEnabled, }); } diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 0cbad179f17..a9c491c7c6c 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -37,11 +37,17 @@ export const setEndpoints = ({ commit }, endpoints) => { export const setFeatureFlags = ( { commit }, - { prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled }, + { + prometheusEndpointEnabled, + multipleDashboardsEnabled, + additionalPanelTypesEnabled, + exportMetricsToCsvEnabled, + }, ) => { commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled); commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled); commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled); + commit(types.SET_EXPORT_METRICS_TO_CSV_ENABLED, exportMetricsToCsvEnabled); }; export const setShowErrorBanner = ({ commit }, enabled) => { diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 4b1aadbcf05..9ec8214b167 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -17,3 +17,4 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE'; export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; +export const SET_EXPORT_METRICS_TO_CSV_ENABLED = 'SET_EXPORT_METRICS_TO_CSV_ENABLED'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index b19520d6638..a2dceb21fc0 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -99,4 +99,7 @@ export default { [types.SET_SHOW_ERROR_BANNER](state, enabled) { state.showErrorBanner = enabled; }, + [types.SET_EXPORT_METRICS_TO_CSV_ENABLED](state, enabled) { + state.exportMetricsToCsvEnabled = enabled; + }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index 440bdc951e0..a14a25e3a20 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -10,6 +10,7 @@ export default () => ({ useDashboardEndpoint: false, multipleDashboardsEnabled: false, additionalPanelTypesEnabled: false, + exportMetricsToCsvEnabled: false, emptyState: 'gettingStarted', showEmptyState: true, showErrorBanner: true, diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index b709ac85e39..4ae79186143 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,6 +15,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards) push_frontend_feature_flag(:environment_metrics_additional_panel_types) push_frontend_feature_flag(:prometheus_computed_alerts) + push_frontend_feature_flag(:export_metrics_to_csv_enabled) end def index diff --git a/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml b/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml new file mode 100644 index 00000000000..59f12fca1f1 --- /dev/null +++ b/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Export and download CSV from metrics charts +merge_request: 30760 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9b41f32042a..50103c226d4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4007,6 +4007,9 @@ msgstr "" msgid "Download" msgstr "" +msgid "Download CSV" +msgstr "" + msgid "Download artifacts" msgstr "" diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index d3a76f33679..4541119dd2e 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -1,9 +1,9 @@ import { shallowMount } from '@vue/test-utils'; +import { createStore } from '~/monitoring/stores'; import { GlLink } from '@gitlab/ui'; import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper'; import Area from '~/monitoring/components/charts/area.vue'; -import { createStore } from '~/monitoring/stores'; import * as types from '~/monitoring/stores/mutation_types'; import { TEST_HOST } from 'spec/test_constants'; import MonitoringMock, { deploymentData } from '../mock_data'; @@ -17,13 +17,14 @@ describe('Area component', () => { let mockGraphData; let areaChart; let spriteSpy; + let store; beforeEach(() => { - const store = createStore(); - + store = createStore(); store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); + store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true }); [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics; areaChart = shallowMount(Area, { @@ -36,6 +37,7 @@ describe('Area component', () => { slots: { default: mockWidgets, }, + store, }); spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake( @@ -107,6 +109,16 @@ describe('Area component', () => { }); }); + describe('when exportMetricsToCsvEnabled is disabled', () => { + beforeEach(() => { + store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false }); + }); + + it('does not render the Download CSV button', () => { + expect(areaChart.contains('glbutton-stub')).toBe(false); + }); + }); + describe('methods', () => { describe('formatTooltipText', () => { const mockDate = deploymentData[0].created_at; @@ -252,5 +264,23 @@ describe('Area component', () => { expect(areaChart.vm.yAxisLabel).toBe('CPU'); }); }); + + describe('csvText', () => { + it('converts data from json to csv', () => { + const header = `timestamp,${mockGraphData.y_label}`; + const data = mockGraphData.queries[0].result[0].values; + const firstRow = `${data[0][0]},${data[0][1]}`; + + expect(areaChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`); + }); + }); + + describe('downloadLink', () => { + it('produces a link to download metrics as csv', () => { + const link = areaChart.vm.downloadLink; + + expect(link).toContain('blob:'); + }); + }); }); }); |