diff options
author | Fatih Acet <acetfatih@gmail.com> | 2019-07-22 12:01:43 +0000 |
---|---|---|
committer | 🤖 GitLab Bot 🤖 <gitlab-bot@gitlab.com> | 2019-07-22 13:21:49 +0000 |
commit | 3a9f91b6a00ccb00408ec9ca4e6e42aab6dc9dc8 (patch) | |
tree | 17db1800b0c15d346e1bbadcf8f402a9ae94e0dc | |
parent | 40541393153af5e36f212a39febbbb446db871ca (diff) | |
download | gitlab-ce-3a9f91b6a00ccb00408ec9ca4e6e42aab6dc9dc8.tar.gz |
Merge branch 'tr-embed-metrics-frontend' into 'master'
Embed metrics charts in issues
See merge request gitlab-org/gitlab-ce!29691
(cherry picked from commit 886a6957ec0d981426219f42d75e0af145a9f7cf)
80feba93 Add ability to embed metrics
b5cdde0c Integrate latest backend changes and feature flag
330b0414 Fix jest test
c260ad82 Migrate TODOs to issues
3a822d99 Seet appropriate default
57c2eb79 Put gfm rendering behind a feature flag
cfeda009 Rename link variable
81ff8d43 Move feature spec into new MR
9d08a5dd Add devensive check on `gon` features object
ca5e00cf Add w-100 style
6e63457b Condense border into shorthand
8de8b6cf Move sidebarAnimationDuration into constants
c9936bf9 Remove extraneous default export
010bb0e3 Reword conditional logic
f4ea4c81 Simplify filter logic
a2f1b4c2 Move styling from css to utility class
da9788df Tidy up component initialization
c69a119b Avoid duplication of `embedded` param supplied by backend
be4b7a11 Remove unnecessary mount
d203980b Apply suggestion to app/assets/javascripts/monitoring/components/charts/area.vue
62ebc6e3 Use object notation
26075703 Add missing class
d9464420 Make sidebarAnimationDuration match actual sidebar animation length
83014858 Remove nextTick and compute groupData object directly
259f700b Rename variable for accuracy
f34890cc Use composite key for dashboard metric groups
92ef33a0 Fix unit test to accommodate for removal of mount call
e2f10bba Remove changelog entry as feature is behind feature flag
c6d8b271 Use graph title as key for embeds
15 files changed, 410 insertions, 50 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index bfb073fdcdc..789a057caf8 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import syntaxHighlight from '~/syntax_highlight'; import renderMath from './render_math'; import renderMermaid from './render_mermaid'; +import renderMetrics from './render_metrics'; import highlightCurrentUser from './highlight_current_user'; import initUserPopovers from '../../user_popovers'; import initMRPopovers from '../../mr_popover'; @@ -17,6 +18,9 @@ $.fn.renderGFM = function renderGFM() { highlightCurrentUser(this.find('.gfm-project_member').get()); initUserPopovers(this.find('.gfm-project_member').get()); initMRPopovers(this.find('.gfm-merge_request').get()); + if (gon.features && gon.features.gfmEmbeddedMetrics) { + renderMetrics(this.find('.js-render-metrics').get()); + } return this; }; diff --git a/app/assets/javascripts/behaviors/markdown/render_metrics.js b/app/assets/javascripts/behaviors/markdown/render_metrics.js new file mode 100644 index 00000000000..252b98610b6 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/render_metrics.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import Metrics from '~/monitoring/components/embed.vue'; +import { createStore } from '~/monitoring/stores'; + +// TODO: Handle copy-pasting - https://gitlab.com/gitlab-org/gitlab-ce/issues/64369. +export default function renderMetrics(elements) { + if (!elements.length) { + return; + } + + elements.forEach(element => { + const { dashboardUrl } = element.dataset; + const MetricsComponent = Vue.extend(Metrics); + + // eslint-disable-next-line no-new + new MetricsComponent({ + el: element, + store: createStore(), + propsData: { + dashboardUrl, + }, + }); + }); +} diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 454ff4f284e..edf9423c74c 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -37,7 +37,13 @@ export default { }, projectPath: { type: String, - required: true, + required: false, + default: () => '', + }, + showBorder: { + type: Boolean, + required: false, + default: () => false, }, thresholds: { type: Array, @@ -234,52 +240,54 @@ export default { </script> <template> - <div class="prometheus-graph col-12 col-lg-6"> - <div class="prometheus-graph-header"> - <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> - <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> - </div> - <gl-area-chart - ref="areaChart" - v-bind="$attrs" - :data="chartData" - :option="chartOptions" - :format-tooltip-text="formatTooltipText" - :thresholds="thresholds" - :width="width" - :height="height" - @updated="onChartUpdated" - > - <template v-if="tooltip.isDeployment"> - <template slot="tooltipTitle"> - {{ __('Deployed') }} - </template> - <div slot="tooltipContent" class="d-flex align-items-center"> - <icon name="commit" class="mr-2" /> - <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> - </div> - </template> - <template v-else> - <template slot="tooltipTitle"> - <div class="text-nowrap"> - {{ tooltip.title }} + <div class="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-header"> + <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> + <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> + </div> + <gl-area-chart + ref="areaChart" + v-bind="$attrs" + :data="chartData" + :option="chartOptions" + :format-tooltip-text="formatTooltipText" + :thresholds="thresholds" + :width="width" + :height="height" + @updated="onChartUpdated" + > + <template v-if="tooltip.isDeployment"> + <template slot="tooltipTitle"> + {{ __('Deployed') }} + </template> + <div slot="tooltipContent" class="d-flex align-items-center"> + <icon name="commit" class="mr-2" /> + <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> </div> </template> - <template slot="tooltipContent"> - <div - v-for="(content, key) in tooltip.content" - :key="key" - class="d-flex justify-content-between" - > - <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> - {{ content.name }} - </gl-chart-series-label> - <div class="prepend-left-32"> - {{ content.value }} + <template v-else> + <template slot="tooltipTitle"> + <div class="text-nowrap"> + {{ tooltip.title }} </div> - </div> + </template> + <template slot="tooltipContent"> + <div + v-for="(content, key) in tooltip.content" + :key="key" + class="d-flex justify-content-between" + > + <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> + {{ content.name }} + </gl-chart-series-label> + <div class="prepend-left-32"> + {{ content.value }} + </div> + </div> + </template> </template> - </template> - </gl-area-chart> + </gl-area-chart> + </div> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 6eaced0c108..c7c55880040 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -11,10 +11,9 @@ import MonitorSingleStatChart from './charts/single_stat.vue'; import PanelType from './panel_type.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; -import { timeWindows, timeWindowsKeyNames } from '../constants'; +import { sidebarAnimationDuration, timeWindows, timeWindowsKeyNames } from '../constants'; import { getTimeDiff } from '../utils'; -const sidebarAnimationDuration = 150; let sidebarMutationObserver; export default { @@ -370,8 +369,8 @@ export default { </div> <div v-if="!showEmptyState"> <graph-group - v-for="(groupData, index) in groupsWithData" - :key="index" + v-for="groupData in groupsWithData" + :key="`${groupData.group}.${groupData.priority}`" :name="groupData.group" :show-panels="showPanels" > diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue new file mode 100644 index 00000000000..e17f03de0fd --- /dev/null +++ b/app/assets/javascripts/monitoring/components/embed.vue @@ -0,0 +1,97 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import GraphGroup from './graph_group.vue'; +import MonitorAreaChart from './charts/area.vue'; +import { sidebarAnimationDuration, timeWindowsKeyNames, timeWindows } from '../constants'; +import { getTimeDiff } from '../utils'; + +let sidebarMutationObserver; + +export default { + components: { + GraphGroup, + MonitorAreaChart, + }, + props: { + dashboardUrl: { + type: String, + required: true, + }, + }, + data() { + return { + params: { + ...getTimeDiff(timeWindows[timeWindowsKeyNames.eightHours]), + }, + elWidth: 0, + }; + }, + computed: { + ...mapState('monitoringDashboard', ['groups', 'metricsWithData']), + groupData() { + const groupsWithData = this.groups.filter(group => this.chartsWithData(group.metrics).length); + if (groupsWithData.length) { + return groupsWithData[0]; + } + return null; + }, + }, + mounted() { + this.setInitialState(); + this.fetchMetricsData(this.params); + sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); + sidebarMutationObserver.observe(document.querySelector('.layout-page'), { + attributes: true, + childList: false, + subtree: false, + }); + }, + beforeDestroy() { + if (sidebarMutationObserver) { + sidebarMutationObserver.disconnect(); + } + }, + methods: { + ...mapActions('monitoringDashboard', [ + 'fetchMetricsData', + 'setEndpoints', + 'setFeatureFlags', + 'setShowErrorBanner', + ]), + chartsWithData(charts) { + return charts.filter(chart => + chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), + ); + }, + onSidebarMutation() { + setTimeout(() => { + this.elWidth = this.$el.clientWidth; + }, sidebarAnimationDuration); + }, + setInitialState() { + this.setFeatureFlags({ + prometheusEndpointEnabled: true, + }); + this.setEndpoints({ + dashboardEndpoint: this.dashboardUrl, + }); + this.setShowErrorBanner(false); + }, + }, +}; +</script> +<template> + <div class="metrics-embed"> + <div v-if="groupData" class="row w-100 m-n2 pb-4"> + <monitor-area-chart + v-for="graphData in chartsWithData(groupData.metrics)" + :key="graphData.title" + :graph-data="graphData" + :container-width="elWidth" + group-id="monitor-area-chart" + :project-path="null" + :show-border="true" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 26f1bf3f68d..605c95e6da5 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -1,5 +1,7 @@ import { __ } from '~/locale'; +export const sidebarAnimationDuration = 300; // milliseconds. + export const chartHeight = 300; export const graphTypes = { diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 5b3da51e9a6..245cc2eaca3 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -44,6 +44,10 @@ export const setFeatureFlags = ( commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled); }; +export const setShowErrorBanner = ({ commit }, enabled) => { + commit(types.SET_SHOW_ERROR_BANNER, enabled); +}; + export const requestMetricsDashboard = ({ commit }) => { commit(types.REQUEST_METRICS_DATA); }; @@ -99,7 +103,9 @@ export const fetchMetricsData = ({ state, dispatch }, params) => { }) .catch(error => { dispatch('receiveMetricsDataFailure', error); - createFlash(s__('Metrics|There was an error while retrieving metrics')); + if (state.setShowErrorBanner) { + createFlash(s__('Metrics|There was an error while retrieving metrics')); + } }); }; @@ -119,7 +125,9 @@ export const fetchDashboard = ({ state, dispatch }, params) => { }) .catch(error => { dispatch('receiveMetricsDashboardFailure', error); - createFlash(s__('Metrics|There was an error while retrieving metrics')); + if (state.setShowErrorBanner) { + createFlash(s__('Metrics|There was an error while retrieving metrics')); + } }); }; diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index c89daba3df7..4b1aadbcf05 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -16,3 +16,4 @@ export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS'; export const SET_ENDPOINTS = 'SET_ENDPOINTS'; 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_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index 0104dcb867d..b19520d6638 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -96,4 +96,7 @@ export default { [types.SET_ADDITIONAL_PANEL_TYPES_ENABLED](state, enabled) { state.additionalPanelTypesEnabled = enabled; }, + [types.SET_SHOW_ERROR_BANNER](state, enabled) { + state.showErrorBanner = enabled; + }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index e54bb712695..440bdc951e0 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -12,6 +12,7 @@ export default () => ({ additionalPanelTypesEnabled: false, emptyState: 'gettingStarted', showEmptyState: true, + showErrorBanner: true, groups: [], deploymentData: [], environments: [], diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss index 2d600e3aef6..05a4cc168a8 100644 --- a/app/assets/stylesheets/pages/prometheus.scss +++ b/app/assets/stylesheets/pages/prometheus.scss @@ -29,6 +29,11 @@ padding: $gl-padding / 2; } +.prometheus-graph-embed { + border: 1px solid $border-color; + border-radius: $border-radius-default; +} + .prometheus-graph-header { display: flex; align-items: center; diff --git a/spec/frontend/behaviors/markdown/render_metrics_spec.js b/spec/frontend/behaviors/markdown/render_metrics_spec.js new file mode 100644 index 00000000000..6db0eabc16b --- /dev/null +++ b/spec/frontend/behaviors/markdown/render_metrics_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import renderMetrics from '~/behaviors/markdown/render_metrics'; +import { TEST_HOST } from 'helpers/test_constants'; + +const originalExtend = Vue.extend; + +describe('Render metrics for Gitlab Flavoured Markdown', () => { + const container = { + Metrics() {}, + }; + + let spyExtend; + + beforeEach(() => { + Vue.extend = () => container.Metrics; + spyExtend = jest.spyOn(Vue, 'extend'); + }); + + afterEach(() => { + Vue.extend = originalExtend; + }); + + it('does nothing when no elements are found', () => { + renderMetrics([]); + + expect(spyExtend).not.toHaveBeenCalled(); + }); + + it('renders a vue component when elements are found', () => { + const element = document.createElement('div'); + element.setAttribute('data-dashboard-url', TEST_HOST); + + renderMetrics([element]); + + expect(spyExtend).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js new file mode 100644 index 00000000000..3b18a0f77c7 --- /dev/null +++ b/spec/frontend/monitoring/embed/embed_spec.js @@ -0,0 +1,78 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import Embed from '~/monitoring/components/embed.vue'; +import MonitorAreaChart from '~/monitoring/components/charts/area.vue'; +import { TEST_HOST } from 'helpers/test_constants'; +import { groups, initialState, metricsData, metricsWithData } from './mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Embed', () => { + let wrapper; + let store; + let actions; + + function mountComponent() { + wrapper = shallowMount(Embed, { + localVue, + store, + propsData: { + dashboardUrl: TEST_HOST, + }, + }); + } + + beforeEach(() => { + actions = { + setFeatureFlags: () => {}, + setShowErrorBanner: () => {}, + setEndpoints: () => {}, + fetchMetricsData: () => {}, + }; + + store = new Vuex.Store({ + modules: { + monitoringDashboard: { + namespaced: true, + actions, + state: initialState, + }, + }, + }); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('no metrics are available yet', () => { + beforeEach(() => { + mountComponent(); + }); + + it('shows an empty state when no metrics are present', () => { + expect(wrapper.find('.metrics-embed').exists()).toBe(true); + expect(wrapper.find(MonitorAreaChart).exists()).toBe(false); + }); + }); + + describe('metrics are available', () => { + beforeEach(() => { + store.state.monitoringDashboard.groups = groups; + store.state.monitoringDashboard.groups[0].metrics = metricsData; + store.state.monitoringDashboard.metricsWithData = metricsWithData; + + mountComponent(); + }); + + it('shows a chart when metrics are present', () => { + wrapper.setProps({}); + expect(wrapper.find('.metrics-embed').exists()).toBe(true); + expect(wrapper.find(MonitorAreaChart).exists()).toBe(true); + expect(wrapper.findAll(MonitorAreaChart).length).toBe(2); + }); + }); +}); diff --git a/spec/frontend/monitoring/embed/mock_data.js b/spec/frontend/monitoring/embed/mock_data.js new file mode 100644 index 00000000000..df4acb82e95 --- /dev/null +++ b/spec/frontend/monitoring/embed/mock_data.js @@ -0,0 +1,87 @@ +export const metricsWithData = [15, 16]; + +export const groups = [ + { + panels: [ + { + title: 'Memory Usage (Total)', + type: 'area-chart', + y_label: 'Total Memory Used', + weight: 4, + metrics: [ + { + id: 'system_metrics_kubernetes_container_memory_total', + metric_id: 15, + }, + ], + }, + { + title: 'Core Usage (Total)', + type: 'area-chart', + y_label: 'Total Cores', + weight: 3, + metrics: [ + { + id: 'system_metrics_kubernetes_container_cores_total', + metric_id: 16, + }, + ], + }, + ], + }, +]; + +export const metrics = [ + { + id: 'system_metrics_kubernetes_container_memory_total', + metric_id: 15, + }, + { + id: 'system_metrics_kubernetes_container_cores_total', + metric_id: 16, + }, +]; + +const queries = [ + { + result: [ + { + values: [ + ['Mon', 1220], + ['Tue', 932], + ['Wed', 901], + ['Thu', 934], + ['Fri', 1290], + ['Sat', 1330], + ['Sun', 1320], + ], + }, + ], + }, +]; + +export const metricsData = [ + { + queries, + metrics: [ + { + metric_id: 15, + }, + ], + }, + { + queries, + metrics: [ + { + metric_id: 16, + }, + ], + }, +]; + +export const initialState = { + monitoringDashboard: {}, + groups: [], + metricsWithData: [], + useDashboardEndpoint: true, +}; diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 634c78ec029..e4d62b044ca 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -69,3 +69,9 @@ Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => { // Tech debt issue TBD testUtilsConfig.logModifiedComponents = false; + +// Basic stub for MutationObserver +global.MutationObserver = () => ({ + disconnect: () => {}, + observe: () => {}, +}); |