Commit e2e1ee8f authored by Tristan Read's avatar Tristan Read Committed by Filipa Lacerda

Add clipboard button to metric chart dropdown

Adds a clipboard button to the metrics dashboard, that allows
copying a link to an individual chart.
parent 10f4df58
......@@ -10,9 +10,9 @@ import {
} from '@gitlab/ui';
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues } from '~/lib/utils/url_utility';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import MonitorAreaChart from './charts/area.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
......@@ -168,8 +168,11 @@ export default {
'multipleDashboardsEnabled',
'additionalPanelTypesEnabled',
]),
firstDashboard() {
return this.allDashboards[0] || {};
},
selectedDashboardText() {
return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name);
return this.currentDashboard || this.firstDashboard.display_name;
},
addingMetricsAvailable() {
return IS_EE && this.canAddMetrics && !this.showEmptyState;
......@@ -258,6 +261,14 @@ export default {
getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(queries));
},
showToast() {
this.$toast.show(__('Link copied to clipboard'));
},
generateLink(group, title, yLabel) {
const dashboard = this.currentDashboard || this.firstDashboard.path;
const params = { dashboard, group, title, y_label: yLabel };
return mergeUrlParams(params, window.location.href);
},
// TODO: END
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
......@@ -435,6 +446,7 @@ export default {
<panel-type
v-for="(graphData, graphIndex) in groupData.metrics"
:key="`panel-type-${graphIndex}`"
:clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
:graph-data="graphData"
:dashboard-width="elWidth"
:index="`${index}-${graphIndex}`"
......@@ -474,6 +486,15 @@ export default {
<gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv">
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item
class="js-chart-link"
:data-clipboard-text="
generateLink(groupData.group, graphData.title, graphData.y_label)
"
@click="showToast"
>
{{ __('Generate link to chart') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${index}-${graphIndex}`"
......
<script>
import { mapState } from 'vuex';
import _ from 'underscore';
import { __ } from '~/locale';
import {
GlDropdown,
GlDropdownItem,
......@@ -28,6 +29,10 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
clipboardText: {
type: String,
required: true,
},
graphData: {
type: Object,
required: true,
......@@ -76,6 +81,9 @@ export default {
isPanelType(type) {
return this.graphData.type && this.graphData.type === type;
},
showToast() {
this.$toast.show(__('Link copied to clipboard'));
},
},
};
</script>
......@@ -116,6 +124,13 @@ export default {
<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>
......
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
import store from './stores';
Vue.use(GlToast);
export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
......
---
title: Generate shareable link for specific metric charts
merge_request: 31339
author:
type: added
......@@ -6645,6 +6645,9 @@ msgstr ""
msgid "Generate key"
msgstr ""
msgid "Generate link to chart"
msgstr ""
msgid "Generate new export"
msgstr ""
......@@ -8823,6 +8826,9 @@ msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
msgstr[1] ""
msgid "Link copied to clipboard"
msgstr ""
msgid "Linked emails (%{email_count})"
msgstr ""
......
import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlToast } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
......@@ -13,6 +15,7 @@ import MonitoringMock, {
dashboardGitResponse,
} from './mock_data';
const localVue = createLocalVue();
const propsData = {
hasMetrics: false,
documentationPath: '/path/to/docs',
......@@ -59,7 +62,9 @@ describe('Dashboard', () => {
});
afterEach(() => {
component.$destroy();
if (component) {
component.$destroy();
}
mock.restore();
});
......@@ -373,6 +378,51 @@ describe('Dashboard', () => {
});
});
describe('link to chart', () => {
let wrapper;
const currentDashboard = 'TEST_DASHBOARD';
localVue.use(GlToast);
const link = () => wrapper.find('.js-chart-link');
const clipboardText = () => link().element.dataset.clipboardText;
beforeEach(done => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
wrapper = shallowMount(DashboardComponent, {
localVue,
sync: false,
attachToDocument: true,
propsData: { ...propsData, hasMetrics: true, currentDashboard },
store,
});
setTimeout(done);
});
afterEach(() => {
wrapper.destroy();
});
it('adds a copy button to the dropdown', () => {
expect(link().text()).toContain('Generate link to chart');
});
it('contains a link to the dashboard', () => {
expect(clipboardText()).toContain(`dashboard=${currentDashboard}`);
expect(clipboardText()).toContain(`group=`);
expect(clipboardText()).toContain(`title=`);
expect(clipboardText()).toContain(`y_label=`);
});
it('creates a toast when clicked', () => {
spyOn(wrapper.vm.$toast, 'show').and.stub();
link().vm.$emit('click');
expect(wrapper.vm.$toast.show).toHaveBeenCalled();
});
});
describe('when the window resizes', () => {
beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
......
import { shallowMount } from '@vue/test-utils';
import PanelType from '~/monitoring/components/panel_type.vue';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
import AreaChart from '~/monitoring/components/charts/area.vue';
import { graphDataPrometheusQueryRange } from './mock_data';
import { createStore } from '~/monitoring/stores';
describe('Panel Type component', () => {
let store;
let panelType;
const dashboardWidth = 100;
describe('When no graphData is available', () => {
let glEmptyChart;
const graphDataNoResult = graphDataPrometheusQueryRange;
// Deep clone object before modifying
const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange));
graphDataNoResult.queries[0].result = [];
beforeEach(() => {
panelType = shallowMount(PanelType, {
propsData: {
clipboardText: 'dashboard_link',
dashboardWidth,
graphData: graphDataNoResult,
},
......@@ -41,4 +46,33 @@ describe('Panel Type component', () => {
});
});
});
describe('when Graph data is available', () => {
const exampleText = 'example_text';
beforeEach(() => {
store = createStore();
panelType = shallowMount(PanelType, {
propsData: {
clipboardText: exampleText,
dashboardWidth,
graphData: graphDataPrometheusQueryRange,
},
store,
});
});
describe('Area Chart panel type', () => {
it('is rendered', () => {
expect(panelType.find(AreaChart).exists()).toBe(true);
});
it('sets clipboard text on the dropdown', () => {
const link = () => panelType.find('.js-chart-link');
const clipboardText = () => link().element.dataset.clipboardText;
expect(clipboardText()).toBe(exampleText);
});
});
});
});
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