[MR Widget V2] Add scrolling to the widget content
Summary
The widget content needs to be scrollable.
Implementation plan
-
In https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue, add the Virtual Scroller
Verification steps
- Make sure
:refactor_security_extensionfeature flag is turned on - Apply the following patch:
diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/ee/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
index 255f6ff546a1..0c86d4c7e0dd 100644
--- a/ee/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
+++ b/ee/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
@@ -7,15 +7,18 @@ export default {
import(
'ee/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue'
),
+ MrMetricsWidget: () =>
+ import('ee/vue_merge_request_widget/extensions/metrics/mr_widget_metrics.vue'),
},
extends: CEWidgetApp,
computed: {
widgets() {
- return [window.gon?.features?.refactorSecurityExtension && 'MrSecurityWidget'].filter(
- (w) => w,
- );
+ return [
+ window.gon?.features?.refactorSecurityExtension && 'MrSecurityWidget',
+ 'MrMetricsWidget',
+ ].filter((w) => w);
},
},
diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/extensions/metrics/mr_widget_metrics.vue b/ee/app/assets/javascripts/vue_merge_request_widget/extensions/metrics/mr_widget_metrics.vue
new file mode 100644
index 000000000000..7cd0f47ceb85
--- /dev/null
+++ b/ee/app/assets/javascripts/vue_merge_request_widget/extensions/metrics/mr_widget_metrics.vue
@@ -0,0 +1,184 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { __, n__, s__, sprintf } from '~/locale';
+import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
+import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
+
+export default {
+ name: 'WidgetMetrics',
+ components: {
+ MrWidget,
+ GlSprintf,
+ },
+ i18n: {
+ label: s__('Reports|metrics report'),
+ loading: s__('Reports|Metrics reports are loading'),
+ error: s__('Reports|Metrics reports failed to load results'),
+ detectedChanges: s__(
+ 'Reports|Metrics reports: %{strongStart}%{numberOfChanges}%{strongEnd} %{changes}',
+ ),
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isLoading: true,
+ metrics: {
+ collapsed: null,
+ expanded: null,
+ },
+ };
+ },
+ computed: {
+ numberOfChanges() {
+ const collapsedData = this.metrics.collapsed;
+ const changedMetrics =
+ collapsedData?.existing_metrics?.filter((metric) => metric?.previous_value) || [];
+ const newMetrics = collapsedData?.new_metrics || [];
+ const removedMetrics = collapsedData?.removed_metrics || [];
+
+ return changedMetrics.length + newMetrics.length + removedMetrics.length;
+ },
+ hasChanges() {
+ return this.numberOfChanges > 0;
+ },
+ statusIcon() {
+ return this.hasChanges ? EXTENSION_ICONS.warning : EXTENSION_ICONS.success;
+ },
+ shouldCollapse() {
+ return this.hasChanges;
+ },
+ summary() {
+ const { hasChanges, numberOfChanges } = this;
+ const changesSummary = sprintf(
+ s__('Reports|Metrics reports: %{strong_start}%{numberOfChanges}%{strong_end} %{changes}'),
+ {
+ numberOfChanges,
+ changes: n__('change', 'changes', numberOfChanges),
+ },
+ );
+ const noChangesSummary = s__('Reports|Metrics report scanning detected no new changes');
+ return hasChanges ? changesSummary : noChangesSummary;
+ },
+ },
+ methods: {
+ fetchCollapsedData() {
+ return new Promise((resolve) => {
+ resolve({
+ data: {
+ new_metrics: [],
+ existing_metrics: [
+ { name: 'setup_db', value: '34', previous_value: '30' },
+ ...Array.from(Array(20).keys()).map(() => ({
+ name: 'total_memory_used_by_dependencies_on_boot_prod_env_mb',
+ value: '878.5',
+ previous_value: '897.2',
+ })),
+ ],
+ removed_metrics: [],
+ },
+ });
+ });
+ },
+ fetchExpandedData() {
+ return new Promise((resolve) => {
+ resolve({ data: this.prepareReports() });
+ });
+ },
+ formatMetricDelta(metric) {
+ // calculate metric delta for sorting if numeric
+ const delta = Math.abs(parseFloat(metric.value) - parseFloat(metric.previous_value));
+
+ // give non-numeric metrics high delta so they appear first
+ return Number.isNaN(delta) ? Infinity : delta;
+ },
+ handleIsLoading(val) {
+ this.isLoading = val;
+ },
+ prepareReports() {
+ const {
+ new_metrics: newMetrics = [],
+ existing_metrics: existingMetrics = [],
+ removed_metrics: removedMetrics = [],
+ } = this.metrics.collapsed;
+
+ return [
+ ...newMetrics.map((metric, index) => {
+ return {
+ header: index === 0 ? __('New') : undefined,
+ id: uniqueId('new-metric-'),
+ text: `${metric.name}: ${metric.value}`,
+ icon: { name: EXTENSION_ICONS.neutral },
+ };
+ }),
+ ...removedMetrics.map((metric, index) => {
+ return {
+ header: index === 0 ? __('Removed') : undefined,
+ id: uniqueId('resolved-metric-'),
+ text: `${metric.name}: ${metric.value}`,
+ icon: { name: EXTENSION_ICONS.neutral },
+ };
+ }),
+ ...existingMetrics
+ .filter((metric) => metric?.previous_value)
+ .map((metric) => {
+ return {
+ id: uniqueId('changed-metric-'),
+ text: `${metric.name}: ${metric.value} (${metric.previous_value})`,
+ icon: { name: EXTENSION_ICONS.neutral },
+ delta: this.formatMetricDelta(metric),
+ };
+ })
+ .sort((a, b) => b.delta - a.delta)
+ .map((metric, index) => {
+ return {
+ header: index === 0 ? __('Changed') : undefined,
+ ...metric,
+ };
+ }),
+ ...existingMetrics
+ .filter((metric) => !metric?.previous_value)
+ .map((metric, index) => {
+ return {
+ header: index === 0 ? __('No changes') : undefined,
+ id: uniqueId('unchanged-metric-'),
+ text: `${metric.name}: ${metric.value}`,
+ icon: { name: EXTENSION_ICONS.neutral },
+ };
+ }),
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <mr-widget
+ v-model="metrics"
+ :error-text="$options.i18n.error"
+ :fetch-collapsed-data="fetchCollapsedData"
+ :fetch-expanded-data="fetchExpandedData"
+ :content="metrics.expanded"
+ :widget-name="$options.name"
+ :is-collapsible="shouldCollapse"
+ :is-loading="isLoading"
+ :status-icon-name="statusIcon"
+ @isLoading="handleIsLoading"
+ >
+ <template #summary>
+ <gl-sprintf v-if="hasChanges" :message="$options.i18n.detectedChanges">
+ <template #strong>
+ <strong>{{ numberOfChanges }}</strong>
+ </template>
+ <template #changes>
+ {{ n__('change', 'changes', numberOfChanges) }}
+ </template>
+ </gl-sprintf>
+ </template>
+ </mr-widget>
+</template>
- Go to the MR widget and check the content of the Metrics widget. It should be scrollable.
Edited by Savas Vedova