Commit c8dfb11b authored by Clement Ho's avatar Clement Ho

Merge branch...

Merge branch '64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-into-the-dropdown' into 'master'

Move download CSV functionality to dropdown in metrics

Closes #64950

See merge request gitlab-org/gitlab-ce!31679
parents 0b43c102 9e75711a
<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