Commit 804232d2 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'tr-embed-metric-links' into 'master'

Generate shareable link for specific metric charts

See merge request gitlab-org/gitlab-ce!31339
parents 535c2d3c 9cba187a
...@@ -10,9 +10,9 @@ import { ...@@ -10,9 +10,9 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import _ from 'underscore'; import _ from 'underscore';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale'; import { __, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; 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 invalidUrl from '~/lib/utils/invalid_url';
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';
...@@ -168,8 +168,11 @@ export default { ...@@ -168,8 +168,11 @@ export default {
'multipleDashboardsEnabled', 'multipleDashboardsEnabled',
'additionalPanelTypesEnabled', 'additionalPanelTypesEnabled',
]), ]),
firstDashboard() {
return this.allDashboards[0] || {};
},
selectedDashboardText() { selectedDashboardText() {
return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name); return this.currentDashboard || this.firstDashboard.display_name;
}, },
addingMetricsAvailable() { addingMetricsAvailable() {
return IS_EE && this.canAddMetrics && !this.showEmptyState; return IS_EE && this.canAddMetrics && !this.showEmptyState;
...@@ -258,6 +261,14 @@ export default { ...@@ -258,6 +261,14 @@ export default {
getGraphAlertValues(queries) { getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(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 // TODO: END
hideAddMetricModal() { hideAddMetricModal() {
this.$refs.addMetricModal.hide(); this.$refs.addMetricModal.hide();
...@@ -435,6 +446,7 @@ export default { ...@@ -435,6 +446,7 @@ export default {
<panel-type <panel-type
v-for="(graphData, graphIndex) in groupData.metrics" v-for="(graphData, graphIndex) in groupData.metrics"
:key="`panel-type-${graphIndex}`" :key="`panel-type-${graphIndex}`"
:clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
:graph-data="graphData" :graph-data="graphData"
:dashboard-width="elWidth" :dashboard-width="elWidth"
:index="`${index}-${graphIndex}`" :index="`${index}-${graphIndex}`"
...@@ -474,6 +486,15 @@ export default { ...@@ -474,6 +486,15 @@ export default {
<gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv"> <gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv">
{{ __('Download CSV') }} {{ __('Download CSV') }}
</gl-dropdown-item> </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 <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 { __ } from '~/locale';
import { import {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
...@@ -28,6 +29,10 @@ export default { ...@@ -28,6 +29,10 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
clipboardText: {
type: String,
required: true,
},
graphData: { graphData: {
type: Object, type: Object,
required: true, required: true,
...@@ -76,6 +81,9 @@ export default { ...@@ -76,6 +81,9 @@ export default {
isPanelType(type) { isPanelType(type) {
return this.graphData.type && this.graphData.type === type; return this.graphData.type && this.graphData.type === type;
}, },
showToast() {
this.$toast.show(__('Link copied to clipboard'));
},
}, },
}; };
</script> </script>
...@@ -116,6 +124,13 @@ export default { ...@@ -116,6 +124,13 @@ export default {
<gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv"> <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv">
{{ __('Download CSV') }} {{ __('Download CSV') }}
</gl-dropdown-item> </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}`"> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`">
{{ __('Alerts') }} {{ __('Alerts') }}
</gl-dropdown-item> </gl-dropdown-item>
......
import Vue from 'vue'; import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility'; import { getParameterValues } from '~/lib/utils/url_utility';
import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue'; import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
import store from './stores'; import store from './stores';
Vue.use(GlToast);
export default (props = {}) => { export default (props = {}) => {
const el = document.getElementById('prometheus-graphs'); const el = document.getElementById('prometheus-graphs');
......
---
title: Generate shareable link for specific metric charts
merge_request: 31339
author:
type: added
...@@ -5181,6 +5181,9 @@ msgstr "" ...@@ -5181,6 +5181,9 @@ msgstr ""
msgid "Generate a default set of labels" msgid "Generate a default set of labels"
msgstr "" msgstr ""
msgid "Generate link to chart"
msgstr ""
msgid "Generate new export" msgid "Generate new export"
msgstr "" msgstr ""
...@@ -6534,6 +6537,9 @@ msgid_plural "Limited to showing %d events at most" ...@@ -6534,6 +6537,9 @@ msgid_plural "Limited to showing %d events at most"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Link copied to clipboard"
msgstr ""
msgid "Linked emails (%{email_count})" msgid "Linked emails (%{email_count})"
msgstr "" msgstr ""
......
import Vue from 'vue'; import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlToast } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue'; import Dashboard from '~/monitoring/components/dashboard.vue';
import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants'; import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
...@@ -13,6 +15,7 @@ import MonitoringMock, { ...@@ -13,6 +15,7 @@ import MonitoringMock, {
dashboardGitResponse, dashboardGitResponse,
} from './mock_data'; } from './mock_data';
const localVue = createLocalVue();
const propsData = { const propsData = {
hasMetrics: false, hasMetrics: false,
documentationPath: '/path/to/docs', documentationPath: '/path/to/docs',
...@@ -59,7 +62,9 @@ describe('Dashboard', () => { ...@@ -59,7 +62,9 @@ describe('Dashboard', () => {
}); });
afterEach(() => { afterEach(() => {
if (component) {
component.$destroy(); component.$destroy();
}
mock.restore(); mock.restore();
}); });
...@@ -373,6 +378,51 @@ describe('Dashboard', () => { ...@@ -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', () => { describe('when the window resizes', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import PanelType from '~/monitoring/components/panel_type.vue'; import PanelType from '~/monitoring/components/panel_type.vue';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
import AreaChart from '~/monitoring/components/charts/area.vue';
import { graphDataPrometheusQueryRange } from './mock_data'; import { graphDataPrometheusQueryRange } from './mock_data';
import { createStore } from '~/monitoring/stores';
describe('Panel Type component', () => { describe('Panel Type component', () => {
let store;
let panelType; let panelType;
const dashboardWidth = 100; const dashboardWidth = 100;
describe('When no graphData is available', () => { describe('When no graphData is available', () => {
let glEmptyChart; let glEmptyChart;
const graphDataNoResult = graphDataPrometheusQueryRange; // Deep clone object before modifying
const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange));
graphDataNoResult.queries[0].result = []; graphDataNoResult.queries[0].result = [];
beforeEach(() => { beforeEach(() => {
panelType = shallowMount(PanelType, { panelType = shallowMount(PanelType, {
propsData: { propsData: {
clipboardText: 'dashboard_link',
dashboardWidth, dashboardWidth,
graphData: graphDataNoResult, graphData: graphDataNoResult,
}, },
...@@ -41,4 +46,33 @@ describe('Panel Type component', () => { ...@@ -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