diff options
Diffstat (limited to 'app')
14 files changed, 346 insertions, 114 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_metrics.js b/app/assets/javascripts/behaviors/markdown/render_metrics.js index 8050604e6e7..9260a89bd52 100644 --- a/app/assets/javascripts/behaviors/markdown/render_metrics.js +++ b/app/assets/javascripts/behaviors/markdown/render_metrics.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import Metrics from '~/monitoring/components/embed.vue'; -import { createStore } from '~/monitoring/stores'; +import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue'; +import { createStore } from '~/monitoring/stores/embed_group/'; // TODO: Handle copy-pasting - https://gitlab.com/gitlab-org/gitlab-foss/issues/64369. export default function renderMetrics(elements) { @@ -8,16 +8,36 @@ export default function renderMetrics(elements) { return; } + const EmbedGroupComponent = Vue.extend(EmbedGroup); + + const wrapperList = []; + elements.forEach(element => { - const { dashboardUrl } = element.dataset; - const MetricsComponent = Vue.extend(Metrics); + let wrapper; + const { previousElementSibling } = element; + const isFirstElementInGroup = !previousElementSibling?.urls; + + if (isFirstElementInGroup) { + wrapper = document.createElement('div'); + wrapper.urls = [element.dataset.dashboardUrl]; + element.parentNode.insertBefore(wrapper, element); + wrapperList.push(wrapper); + } else { + wrapper = previousElementSibling; + wrapper.urls.push(element.dataset.dashboardUrl); + } + + // Clean up processed element + element.parentNode.removeChild(element); + }); + wrapperList.forEach(wrapper => { // eslint-disable-next-line no-new - new MetricsComponent({ - el: element, + new EmbedGroupComponent({ + el: wrapper, store: createStore(), propsData: { - dashboardUrl, + urls: wrapper.urls, }, }); }); diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue deleted file mode 100644 index 6182b570e76..00000000000 --- a/app/assets/javascripts/monitoring/components/embed.vue +++ /dev/null @@ -1,99 +0,0 @@ -<script> -import { mapActions, mapState, mapGetters } from 'vuex'; -import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; -import { convertToFixedRange } from '~/lib/utils/datetime_range'; -import { timeRangeFromUrl, removeTimeRangeParams } from '../utils'; -import { sidebarAnimationDuration } from '../constants'; -import { defaultTimeRange } from '~/vue_shared/constants'; - -let sidebarMutationObserver; - -export default { - components: { - PanelType, - }, - props: { - dashboardUrl: { - type: String, - required: true, - }, - }, - data() { - const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange; - return { - timeRange: convertToFixedRange(timeRange), - elWidth: 0, - }; - }, - computed: { - ...mapState('monitoringDashboard', ['dashboard']), - ...mapGetters('monitoringDashboard', ['metricsWithData']), - charts() { - if (!this.dashboard || !this.dashboard.panelGroups) { - return []; - } - const groupWithMetrics = this.dashboard.panelGroups.find(group => - group.panels.find(chart => this.chartHasData(chart)), - ) || { panels: [] }; - - return groupWithMetrics.panels.filter(chart => this.chartHasData(chart)); - }, - isSingleChart() { - return this.charts.length === 1; - }, - }, - mounted() { - this.setInitialState(); - this.setTimeRange(this.timeRange); - this.fetchDashboard(); - - 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', [ - 'setTimeRange', - 'fetchDashboard', - 'setEndpoints', - 'setFeatureFlags', - 'setShowErrorBanner', - ]), - chartHasData(chart) { - return chart.metrics.some(metric => this.metricsWithData().includes(metric.metricId)); - }, - onSidebarMutation() { - setTimeout(() => { - this.elWidth = this.$el.clientWidth; - }, sidebarAnimationDuration); - }, - setInitialState() { - this.setEndpoints({ - dashboardEndpoint: removeTimeRangeParams(this.dashboardUrl), - }); - this.setShowErrorBanner(false); - }, - }, -}; -</script> -<template> - <div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }"> - <div v-if="charts.length" class="row w-100 m-n2 pb-4"> - <panel-type - v-for="(graphData, graphIndex) in charts" - :key="`panel-type-${graphIndex}`" - class="w-100" - :graph-data="graphData" - :group-id="dashboardUrl" - /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue new file mode 100644 index 00000000000..b8562afe441 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue @@ -0,0 +1,101 @@ +<script> +import { mapState, mapActions, mapGetters } from 'vuex'; +import sum from 'lodash/sum'; +import { GlButton, GlCard, GlIcon } from '@gitlab/ui'; +import { n__ } from '~/locale'; +import { monitoringDashboard } from '~/monitoring/stores'; +import MetricEmbed from './metric_embed.vue'; + +export default { + components: { + GlButton, + GlCard, + GlIcon, + MetricEmbed, + }, + props: { + urls: { + type: Array, + required: true, + validator: urls => urls.length > 0, + }, + }, + data() { + return { + isCollapsed: false, + }; + }, + computed: { + ...mapState('embedGroup', ['module']), + ...mapGetters('embedGroup', ['metricsWithData']), + arrowIconName() { + return this.isCollapsed ? 'chevron-right' : 'chevron-down'; + }, + bodyClass() { + return ['border-top', 'pl-3', 'pt-3', { 'd-none': this.isCollapsed }]; + }, + buttonLabel() { + return this.isCollapsed + ? n__('View chart', 'View charts', this.numCharts) + : n__('Hide chart', 'Hide charts', this.numCharts); + }, + containerClass() { + return this.isSingleChart ? 'col-lg-12' : 'col-lg-6'; + }, + numCharts() { + if (this.metricsWithData === null) { + return 0; + } + return sum(this.metricsWithData); + }, + isSingleChart() { + return this.numCharts === 1; + }, + }, + created() { + this.urls.forEach((url, index) => { + const name = this.getNamespace(index); + this.$store.registerModule(name, monitoringDashboard); + this.addModule(name); + }); + }, + methods: { + ...mapActions('embedGroup', ['addModule']), + getNamespace(id) { + return `monitoringDashboard/${id}`; + }, + toggleCollapsed() { + this.isCollapsed = !this.isCollapsed; + }, + }, +}; +</script> +<template> + <gl-card + v-show="numCharts > 0" + class="collapsible-card border p-0 mb-3" + header-class="d-flex align-items-center border-bottom-0 py-2" + :body-class="bodyClass" + > + <template #header> + <gl-button + class="collapsible-card-btn d-flex text-decoration-none" + :aria-label="buttonLabel" + variant="link" + @click="toggleCollapsed" + > + <gl-icon class="mr-1" :name="arrowIconName" /> + {{ buttonLabel }} + </gl-button> + </template> + <div class="d-flex flex-wrap"> + <metric-embed + v-for="(url, index) in urls" + :key="`${index}/${url}`" + :dashboard-url="url" + :namespace="getNamespace(index)" + :container-class="containerClass" + /> + </div> + </gl-card> +</template> diff --git a/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue new file mode 100644 index 00000000000..8a44e6bd737 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue @@ -0,0 +1,131 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; +import { convertToFixedRange } from '~/lib/utils/datetime_range'; +import { defaultTimeRange } from '~/vue_shared/constants'; +import { timeRangeFromUrl, removeTimeRangeParams } from '../../utils'; +import { sidebarAnimationDuration } from '../../constants'; + +let sidebarMutationObserver; + +export default { + components: { + PanelType, + }, + props: { + containerClass: { + type: String, + required: false, + default: 'col-lg-12', + }, + dashboardUrl: { + type: String, + required: true, + }, + namespace: { + type: String, + required: false, + default: 'monitoringDashboard', + }, + }, + data() { + const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange; + return { + timeRange: convertToFixedRange(timeRange), + elWidth: 0, + }; + }, + computed: { + ...mapState({ + dashboard(state) { + return state[this.namespace].dashboard; + }, + metricsWithData(state, getters) { + return getters[`${this.namespace}/metricsWithData`](); + }, + }), + charts() { + if (!this.dashboard || !this.dashboard.panelGroups) { + return []; + } + return this.dashboard.panelGroups.reduce( + (acc, currentGroup) => acc.concat(currentGroup.panels.filter(this.chartHasData)), + [], + ); + }, + isSingleChart() { + return this.charts.length === 1; + }, + embedClass() { + return this.isSingleChart ? this.containerClass : 'col-lg-12'; + }, + panelClass() { + return this.isSingleChart ? 'col-lg-12' : 'col-lg-6'; + }, + }, + mounted() { + this.setInitialState(); + this.setTimeRange(this.timeRange); + this.fetchDashboard(); + + sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); + sidebarMutationObserver.observe(document.querySelector('.layout-page'), { + attributes: true, + childList: false, + subtree: false, + }); + }, + beforeDestroy() { + if (sidebarMutationObserver) { + sidebarMutationObserver.disconnect(); + } + }, + methods: { + // Use function args to support dynamic namespaces in mapXXX helpers. Pattern described + // in https://github.com/vuejs/vuex/issues/863#issuecomment-329510765 + ...mapActions({ + setTimeRange(dispatch, payload) { + return dispatch(`${this.namespace}/setTimeRange`, payload); + }, + fetchDashboard(dispatch, payload) { + return dispatch(`${this.namespace}/fetchDashboard`, payload); + }, + setEndpoints(dispatch, payload) { + return dispatch(`${this.namespace}/setEndpoints`, payload); + }, + setFeatureFlags(dispatch, payload) { + return dispatch(`${this.namespace}/setFeatureFlags`, payload); + }, + setShowErrorBanner(dispatch, payload) { + return dispatch(`${this.namespace}/setShowErrorBanner`, payload); + }, + }), + chartHasData(chart) { + return chart.metrics.some(metric => this.metricsWithData.includes(metric.metricId)); + }, + onSidebarMutation() { + setTimeout(() => { + this.elWidth = this.$el.clientWidth; + }, sidebarAnimationDuration); + }, + setInitialState() { + this.setEndpoints({ + dashboardEndpoint: removeTimeRangeParams(this.dashboardUrl), + }); + this.setShowErrorBanner(false); + }, + }, +}; +</script> +<template> + <div class="metrics-embed p-0 d-flex flex-wrap" :class="embedClass"> + <panel-type + v-for="(graphData, graphIndex) in charts" + :key="`panel-type-${graphIndex}`" + :class="panelClass" + :graph-data="graphData" + :group-id="dashboardUrl" + :namespace="namespace" + /> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index da305c7dda3..d6d60c2d5da 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -68,6 +68,11 @@ export default { required: false, default: 'panel-type-chart', }, + namespace: { + type: String, + required: false, + default: 'monitoringDashboard', + }, }, data() { return { @@ -76,7 +81,22 @@ export default { }; }, computed: { - ...mapState('monitoringDashboard', ['deploymentData', 'projectPath', 'logsPath', 'timeRange']), + // Use functions to support dynamic namespaces in mapXXX helpers. Pattern described + // in https://github.com/vuejs/vuex/issues/863#issuecomment-329510765 + ...mapState({ + deploymentData(state) { + return state[this.namespace].deploymentData; + }, + projectPath(state) { + return state[this.namespace].projectPath; + }, + logsPath(state) { + return state[this.namespace].logsPath; + }, + timeRange(state) { + return state[this.namespace].timeRange; + }, + }), title() { return this.graphData.title || ''; }, diff --git a/app/assets/javascripts/monitoring/stores/embed_group/actions.js b/app/assets/javascripts/monitoring/stores/embed_group/actions.js new file mode 100644 index 00000000000..cbe0950d954 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/actions.js @@ -0,0 +1,5 @@ +import * as types from './mutation_types'; + +export const addModule = ({ commit }, data) => commit(types.ADD_MODULE, data); + +export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/getters.js b/app/assets/javascripts/monitoring/stores/embed_group/getters.js new file mode 100644 index 00000000000..9b08cf762c1 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/getters.js @@ -0,0 +1,4 @@ +export const metricsWithData = (state, getters, rootState, rootGetters) => + state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length); + +export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/index.js b/app/assets/javascripts/monitoring/stores/embed_group/index.js new file mode 100644 index 00000000000..773bca9f87e --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/index.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import state from './state'; + +Vue.use(Vuex); + +// In practice this store will have a number of `monitoringDashboard` modules added dynamically +export const createStore = () => + new Vuex.Store({ + modules: { + embedGroup: { + namespaced: true, + actions, + getters, + mutations, + state, + }, + }, + }); + +export default createStore(); diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js new file mode 100644 index 00000000000..e7a425d3623 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js @@ -0,0 +1,3 @@ +export const ADD_MODULE = 'ADD_MODULE'; + +export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutations.js b/app/assets/javascripts/monitoring/stores/embed_group/mutations.js new file mode 100644 index 00000000000..3c66129f239 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/mutations.js @@ -0,0 +1,7 @@ +import * as types from './mutation_types'; + +export default { + [types.ADD_MODULE](state, module) { + state.modules.push(module); + }, +}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/state.js b/app/assets/javascripts/monitoring/stores/embed_group/state.js new file mode 100644 index 00000000000..016c7e5dac7 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/state.js @@ -0,0 +1,3 @@ +export default () => ({ + modules: [], +}); diff --git a/app/assets/javascripts/monitoring/stores/index.js b/app/assets/javascripts/monitoring/stores/index.js index c1c466b7cf0..f08a6402aa6 100644 --- a/app/assets/javascripts/monitoring/stores/index.js +++ b/app/assets/javascripts/monitoring/stores/index.js @@ -7,16 +7,18 @@ import state from './state'; Vue.use(Vuex); +export const monitoringDashboard = { + namespaced: true, + actions, + getters, + mutations, + state, +}; + export const createStore = () => new Vuex.Store({ modules: { - monitoringDashboard: { - namespaced: true, - actions, - getters, - mutations, - state, - }, + monitoringDashboard, }, }); diff --git a/app/assets/stylesheets/components/collapsible_card.scss b/app/assets/stylesheets/components/collapsible_card.scss new file mode 100644 index 00000000000..c7c7423c1cd --- /dev/null +++ b/app/assets/stylesheets/components/collapsible_card.scss @@ -0,0 +1,9 @@ +.collapsible-card { + .collapsible-card-btn { + color: $gl-text-color; + + &:hover { + color: $blue-600; + } + } +} diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml index 0b747082de0..6a703d0b70c 100644 --- a/app/views/admin/application_settings/metrics_and_profiling.html.haml +++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml @@ -60,4 +60,6 @@ .settings-content = render 'usage' += render_if_exists 'admin/application_settings/seat_link_setting', expanded: expanded_by_default? + = render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded_by_default? |