Merge branch 'lm-download-csv-of-charts-from-metrics-dashboard' into 'master'
Add ability to download chart CSV from metrics dashboard Closes #60733 See merge request gitlab-org/gitlab-ce!30760
This commit is contained in:
commit
25dd49cda2
10 changed files with 83 additions and 7 deletions
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { __ } from '~/locale';
|
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 { 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';
|
||||||
|
@ -15,6 +16,7 @@ let debouncedResize;
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
GlAreaChart,
|
GlAreaChart,
|
||||||
|
GlButton,
|
||||||
GlChartSeriesLabel,
|
GlChartSeriesLabel,
|
||||||
GlLink,
|
GlLink,
|
||||||
Icon,
|
Icon,
|
||||||
|
@ -67,6 +69,7 @@ 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 }]
|
||||||
|
@ -176,6 +179,18 @@ 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',
|
||||||
|
@ -240,10 +255,20 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
|
<div class="prometheus-graph 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-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
|
||||||
|
|
|
@ -13,6 +13,7 @@ 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,11 +37,17 @@ 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,3 +17,4 @@ 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,4 +99,7 @@ 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,6 +10,7 @@ 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,6 +15,7 @@ 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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Export and download CSV from metrics charts
|
||||||
|
merge_request: 30760
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -4007,6 +4007,9 @@ msgstr ""
|
||||||
msgid "Download"
|
msgid "Download"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download CSV"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Download artifacts"
|
msgid "Download artifacts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { createStore } from '~/monitoring/stores';
|
||||||
import { GlLink } from '@gitlab/ui';
|
import { GlLink } from '@gitlab/ui';
|
||||||
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
|
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
|
||||||
import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
|
import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
|
||||||
import Area from '~/monitoring/components/charts/area.vue';
|
import Area from '~/monitoring/components/charts/area.vue';
|
||||||
import { createStore } from '~/monitoring/stores';
|
|
||||||
import * as types from '~/monitoring/stores/mutation_types';
|
import * as types from '~/monitoring/stores/mutation_types';
|
||||||
import { TEST_HOST } from 'spec/test_constants';
|
import { TEST_HOST } from 'spec/test_constants';
|
||||||
import MonitoringMock, { deploymentData } from '../mock_data';
|
import MonitoringMock, { deploymentData } from '../mock_data';
|
||||||
|
@ -17,13 +17,14 @@ describe('Area component', () => {
|
||||||
let mockGraphData;
|
let mockGraphData;
|
||||||
let areaChart;
|
let areaChart;
|
||||||
let spriteSpy;
|
let spriteSpy;
|
||||||
|
let store;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const store = createStore();
|
store = createStore();
|
||||||
|
|
||||||
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, {
|
||||||
|
@ -36,6 +37,7 @@ describe('Area component', () => {
|
||||||
slots: {
|
slots: {
|
||||||
default: mockWidgets,
|
default: mockWidgets,
|
||||||
},
|
},
|
||||||
|
store,
|
||||||
});
|
});
|
||||||
|
|
||||||
spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake(
|
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('methods', () => {
|
||||||
describe('formatTooltipText', () => {
|
describe('formatTooltipText', () => {
|
||||||
const mockDate = deploymentData[0].created_at;
|
const mockDate = deploymentData[0].created_at;
|
||||||
|
@ -252,5 +264,23 @@ 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:');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue