Commit 9e75711a authored by Laura Montemayor's avatar Laura Montemayor Committed by Clement Ho

Adds download CSV functionality to dropdown in metrics

This MR adds the functionality to download metrics data
as CSV. It also removes the exportMetricsToCsvEnabled feature
flag which was used before the dropdown was implemented to
display a button.
parent 0b43c102
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import { mapState } from 'vuex'; import { GlLink } from '@gitlab/ui';
import { GlLink, GlButton } from '@gitlab/ui';
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils'; import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
...@@ -16,7 +15,6 @@ let debouncedResize; ...@@ -16,7 +15,6 @@ let debouncedResize;
export default { export default {
components: { components: {
GlAreaChart, GlAreaChart,
GlButton,
GlChartSeriesLabel, GlChartSeriesLabel,
GlLink, GlLink,
Icon, Icon,
...@@ -69,7 +67,6 @@ export default { ...@@ -69,7 +67,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']),
chartData() { chartData() {
// Transforms & supplements query data to render appropriate labels & styles // Transforms & supplements query data to render appropriate labels & styles
// Input: [{ queryAttributes1 }, { queryAttributes2 }] // Input: [{ queryAttributes1 }, { queryAttributes2 }]
...@@ -179,18 +176,6 @@ export default { ...@@ -179,18 +176,6 @@ export default {
yAxisLabel() { yAxisLabel() {
return `${this.graphData.y_label}`; 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: { watch: {
containerWidth: 'onResize', containerWidth: 'onResize',
...@@ -259,16 +244,6 @@ export default { ...@@ -259,16 +244,6 @@ export default {
<div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
<div class="prometheus-graph-header"> <div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> <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 ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div> </div>
<gl-area-chart <gl-area-chart
......
...@@ -235,6 +235,19 @@ export default { ...@@ -235,6 +235,19 @@ export default {
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
); );
}, },
csvText(graphData) {
const chartData = graphData.queries[0].result[0].values;
const yLabel = graphData.y_label;
const header = `timestamp,${yLabel}\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);
},
downloadCsv(graphData) {
const data = new Blob([this.csvText(graphData)], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
// TODO: BEGIN, Duplicated code with panel_type until feature flag is removed // TODO: BEGIN, Duplicated code with panel_type until feature flag is removed
// Issue number: https://gitlab.com/gitlab-org/gitlab-ce/issues/63845 // Issue number: https://gitlab.com/gitlab-org/gitlab-ce/issues/63845
getGraphAlerts(queries) { getGraphAlerts(queries) {
...@@ -448,7 +461,6 @@ export default { ...@@ -448,7 +461,6 @@ export default {
@setAlerts="setAlerts" @setAlerts="setAlerts"
/> />
<gl-dropdown <gl-dropdown
v-if="alertWidgetAvailable"
v-gl-tooltip v-gl-tooltip
class="mx-2" class="mx-2"
toggle-class="btn btn-transparent border-0" toggle-class="btn btn-transparent border-0"
...@@ -459,6 +471,9 @@ export default { ...@@ -459,6 +471,9 @@ export default {
<template slot="button-content"> <template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" /> <icon name="ellipsis_v" class="text-secondary" />
</template> </template>
<gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv">
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item <gl-dropdown-item
v-if="alertWidgetAvailable" v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${index}-${graphIndex}`" v-gl-modal="`alert-modal-${index}-${graphIndex}`"
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui'; import {
GlDropdown,
GlDropdownItem,
GlModal,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import MonitorAreaChart from './charts/area.vue'; import MonitorAreaChart from './charts/area.vue';
import MonitorSingleStatChart from './charts/single_stat.vue'; import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorEmptyChart from './charts/empty_chart.vue'; import MonitorEmptyChart from './charts/empty_chart.vue';
...@@ -11,12 +18,14 @@ export default { ...@@ -11,12 +18,14 @@ export default {
MonitorAreaChart, MonitorAreaChart,
MonitorSingleStatChart, MonitorSingleStatChart,
MonitorEmptyChart, MonitorEmptyChart,
Icon,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlModal, GlModal,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
}, },
props: { props: {
graphData: { graphData: {
...@@ -41,6 +50,19 @@ export default { ...@@ -41,6 +50,19 @@ export default {
graphDataHasMetrics() { graphDataHasMetrics() {
return this.graphData.queries[0].result.length > 0; return this.graphData.queries[0].result.length > 0;
}, },
csvText() {
const chartData = this.graphData.queries[0].result[0].values;
const yLabel = this.graphData.y_label;
const header = `timestamp,${yLabel}\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);
},
downloadCsv() {
const data = new Blob([this.csvText], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
}, },
methods: { methods: {
getGraphAlerts(queries) { getGraphAlerts(queries) {
...@@ -81,7 +103,6 @@ export default { ...@@ -81,7 +103,6 @@ export default {
@setAlerts="setAlerts" @setAlerts="setAlerts"
/> />
<gl-dropdown <gl-dropdown
v-if="alertWidgetAvailable"
v-gl-tooltip v-gl-tooltip
class="mx-2" class="mx-2"
toggle-class="btn btn-transparent border-0" toggle-class="btn btn-transparent border-0"
...@@ -92,6 +113,9 @@ export default { ...@@ -92,6 +113,9 @@ export default {
<template slot="button-content"> <template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" /> <icon name="ellipsis_v" class="text-secondary" />
</template> </template>
<gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv">
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`"> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`">
{{ __('Alerts') }} {{ __('Alerts') }}
</gl-dropdown-item> </gl-dropdown-item>
......
...@@ -13,7 +13,6 @@ export default (props = {}) => { ...@@ -13,7 +13,6 @@ export default (props = {}) => {
prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint, prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards, multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards,
additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes, additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes,
exportMetricsToCsvEnabled: gon.features.exportMetricsToCsvEnabled,
}); });
} }
......
...@@ -37,17 +37,11 @@ export const setEndpoints = ({ commit }, endpoints) => { ...@@ -37,17 +37,11 @@ export const setEndpoints = ({ commit }, endpoints) => {
export const setFeatureFlags = ( export const setFeatureFlags = (
{ commit }, { commit },
{ { prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled },
prometheusEndpointEnabled,
multipleDashboardsEnabled,
additionalPanelTypesEnabled,
exportMetricsToCsvEnabled,
},
) => { ) => {
commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled); commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled); commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled);
commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled); commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
commit(types.SET_EXPORT_METRICS_TO_CSV_ENABLED, exportMetricsToCsvEnabled);
}; };
export const setShowErrorBanner = ({ commit }, enabled) => { export const setShowErrorBanner = ({ commit }, enabled) => {
......
...@@ -17,4 +17,3 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS'; ...@@ -17,4 +17,3 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; 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_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
export const SET_EXPORT_METRICS_TO_CSV_ENABLED = 'SET_EXPORT_METRICS_TO_CSV_ENABLED';
...@@ -99,7 +99,4 @@ export default { ...@@ -99,7 +99,4 @@ export default {
[types.SET_SHOW_ERROR_BANNER](state, enabled) { [types.SET_SHOW_ERROR_BANNER](state, enabled) {
state.showErrorBanner = enabled; state.showErrorBanner = enabled;
}, },
[types.SET_EXPORT_METRICS_TO_CSV_ENABLED](state, enabled) {
state.exportMetricsToCsvEnabled = enabled;
},
}; };
...@@ -10,7 +10,6 @@ export default () => ({ ...@@ -10,7 +10,6 @@ export default () => ({
useDashboardEndpoint: false, useDashboardEndpoint: false,
multipleDashboardsEnabled: false, multipleDashboardsEnabled: false,
additionalPanelTypesEnabled: false, additionalPanelTypesEnabled: false,
exportMetricsToCsvEnabled: false,
emptyState: 'gettingStarted', emptyState: 'gettingStarted',
showEmptyState: true, showEmptyState: true,
showErrorBanner: true, showErrorBanner: true,
......
...@@ -15,7 +15,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -15,7 +15,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards) push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
push_frontend_feature_flag(:environment_metrics_additional_panel_types) push_frontend_feature_flag(:environment_metrics_additional_panel_types)
push_frontend_feature_flag(:prometheus_computed_alerts) push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:export_metrics_to_csv_enabled)
end end
def index def index
......
---
title: 'feat: adds a download to csv functionality to the dropdown in prometheus metrics'
merge_request: 31679
author:
type: changed
...@@ -24,7 +24,6 @@ describe('Area component', () => { ...@@ -24,7 +24,6 @@ describe('Area component', () => {
store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true });
[mockGraphData] = store.state.monitoringDashboard.groups[0].metrics; [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
areaChart = shallowMount(Area, { areaChart = shallowMount(Area, {
...@@ -109,16 +108,6 @@ describe('Area component', () => { ...@@ -109,16 +108,6 @@ 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('methods', () => {
describe('formatTooltipText', () => { describe('formatTooltipText', () => {
const mockDate = deploymentData[0].created_at; const mockDate = deploymentData[0].created_at;
...@@ -264,23 +253,5 @@ describe('Area component', () => { ...@@ -264,23 +253,5 @@ describe('Area component', () => {
expect(areaChart.vm.yAxisLabel).toBe('CPU'); 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:');
});
});
}); });
}); });
...@@ -5,7 +5,7 @@ import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants'; ...@@ -5,7 +5,7 @@ import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { import MonitoringMock, {
metricsGroupsAPIResponse, metricsGroupsAPIResponse,
mockApiEndpoint, mockApiEndpoint,
environmentData, environmentData,
...@@ -40,6 +40,7 @@ describe('Dashboard', () => { ...@@ -40,6 +40,7 @@ describe('Dashboard', () => {
let mock; let mock;
let store; let store;
let component; let component;
let mockGraphData;
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
...@@ -482,4 +483,36 @@ describe('Dashboard', () => { ...@@ -482,4 +483,36 @@ describe('Dashboard', () => {
}); });
}); });
}); });
describe('when downloading metrics data as CSV', () => {
beforeEach(() => {
component = new DashboardComponent({
propsData: {
...propsData,
},
store,
});
store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
MonitoringMock.data,
);
[mockGraphData] = component.$store.state.monitoringDashboard.groups[0].metrics;
});
describe('csvText', () => {
it('converts metrics 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(component.csvText(mockGraphData)).toMatch(`^${header}\r\n${firstRow}`);
});
});
describe('downloadCsv', () => {
it('produces a link with a Blob', () => {
expect(component.downloadCsv(mockGraphData)).toContain(`blob:`);
});
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment