diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 587392adbc3208d13f6bea3b2250c13173ef9702..dfeeba238cadea3bdd99e8aab9970d47042e34f8 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -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 { {{ __('Download CSV') }} + + {{ __('Generate link to chart') }} + 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')); + }, }, }; @@ -116,6 +124,13 @@ export default { {{ __('Download CSV') }} + + {{ __('Generate link to chart') }} + {{ __('Alerts') }} diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index c0fee1ebb992568b5e32ad242169e2de9d1aea5f..51cef20455c38a229da28ffb8764701c69e5670b 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,9 +1,12 @@ 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'); diff --git a/changelogs/unreleased/tr-embed-metric-links.yml b/changelogs/unreleased/tr-embed-metric-links.yml new file mode 100644 index 0000000000000000000000000000000000000000..6918114a4ae273e233b4b1e4b35260ca05450c42 --- /dev/null +++ b/changelogs/unreleased/tr-embed-metric-links.yml @@ -0,0 +1,5 @@ +--- +title: Generate shareable link for specific metric charts +merge_request: 31339 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d85b514ecfb10df1087c107d56e82375d69258e2..afdfd620ca262d7d2df4e1ad1f8436f39366d022 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5181,6 +5181,9 @@ msgstr "" msgid "Generate a default set of labels" msgstr "" +msgid "Generate link to chart" +msgstr "" + msgid "Generate new export" msgstr "" @@ -6534,6 +6537,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 "" diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index b78896c45fc2700a970f28affc739fa0d4fd1771..624d8b14c8f21b12b38a24e77c9d8582491697fd 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -1,4 +1,6 @@ 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); diff --git a/spec/javascripts/monitoring/panel_type_spec.js b/spec/javascripts/monitoring/panel_type_spec.js index 8ce24041e97fd1e136aa5eeed0e6b88da7ece7b1..086be62809310f3b6bd09653b1de319f290841fb 100644 --- a/spec/javascripts/monitoring/panel_type_spec.js +++ b/spec/javascripts/monitoring/panel_type_spec.js @@ -1,20 +1,25 @@ 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); + }); + }); + }); });