Commit b2229505 authored by Filipa Lacerda's avatar Filipa Lacerda

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

Generate shareable link for specific metric charts - EE

See merge request gitlab-org/gitlab-ee!14938
parents 10f4df58 e2e1ee8f
...@@ -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
...@@ -6645,6 +6645,9 @@ msgstr "" ...@@ -6645,6 +6645,9 @@ msgstr ""
msgid "Generate key" msgid "Generate key"
msgstr "" msgstr ""
msgid "Generate link to chart"
msgstr ""
msgid "Generate new export" msgid "Generate new export"
msgstr "" msgstr ""
...@@ -8823,6 +8826,9 @@ msgid_plural "Limited to showing %d events at most" ...@@ -8823,6 +8826,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(() => {
component.$destroy(); if (component) {
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