Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget/components')
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue97
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/humanized_text.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue84
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js207
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue100
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue6
19 files changed, 418 insertions, 184 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index e7d5e4086bc..4163d195e0f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import createFlash from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -11,9 +11,11 @@ import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
import MrWidgetContainer from '../mr_widget_container.vue';
import MrWidgetIcon from '../mr_widget_icon.vue';
+import { INVALID_RULES_DOCS_PATH } from '../../constants';
import ApprovalsSummary from './approvals_summary.vue';
import ApprovalsSummaryOptional from './approvals_summary_optional.vue';
import { FETCH_LOADING, FETCH_ERROR, APPROVE_ERROR, UNAPPROVE_ERROR } from './messages';
+import { humanizeInvalidApproversRules } from './humanized_text';
export default {
name: 'MRWidgetApprovals',
@@ -23,6 +25,8 @@ export default {
ApprovalsSummary,
ApprovalsSummaryOptional,
GlButton,
+ GlSprintf,
+ GlLink,
},
mixins: [approvalsMixin, glFeatureFlagsMixin()],
props: {
@@ -78,6 +82,15 @@ export default {
approvals() {
return this.mr.approvals || {};
},
+ invalidRules() {
+ return this.approvals.invalid_approvers_rules || [];
+ },
+ hasInvalidRules() {
+ return this.approvals.merge_request_approvers_available && this.invalidRules.length;
+ },
+ invalidRulesText() {
+ return humanizeInvalidApproversRules(this.invalidRules);
+ },
approvedBy() {
return this.approvals.approved_by ? this.approvals.approved_by.map((x) => x.user) : [];
},
@@ -104,20 +117,24 @@ export default {
return {
text: this.approvalText,
category: this.isApproved ? 'secondary' : 'primary',
- variant: 'info',
+ variant: 'confirm',
action: () => this.approve(),
};
} else if (this.showUnapprove) {
return {
text: s__('mrWidget|Revoke approval'),
- variant: 'warning',
- category: 'secondary',
+ variant: 'default',
action: () => this.unapprove(),
};
}
return null;
},
+ pluralizedRuleText() {
+ return this.invalidRules.length > 1
+ ? this.$options.i18n.invalidRulesPlural
+ : this.$options.i18n.invalidRuleSingular;
+ },
},
created() {
this.refreshApprovals()
@@ -194,6 +211,16 @@ export default {
},
},
FETCH_LOADING,
+ linkToInvalidRules: INVALID_RULES_DOCS_PATH,
+ i18n: {
+ invalidRuleSingular: s__(
+ 'mrWidget|Approval rule %{rules} is invalid. GitLab has approved this rule automatically to unblock the merge request. %{link}',
+ ),
+ invalidRulesPlural: s__(
+ 'mrWidget|Approval rules %{rules} are invalid. GitLab has approved these rules automatically to unblock the merge request. %{link}',
+ ),
+ learnMore: __('Learn more.'),
+ },
};
</script>
<template>
@@ -202,29 +229,45 @@ export default {
<mr-widget-icon name="approval" />
<div v-if="fetchingApprovals">{{ $options.FETCH_LOADING }}</div>
<template v-else>
- <gl-button
- v-if="action"
- :variant="action.variant"
- :category="action.category"
- :loading="isApproving"
- class="mr-3"
- data-qa-selector="approve_button"
- @click="action.action"
- >
- {{ action.text }}
- </gl-button>
- <approvals-summary-optional
- v-if="isOptional"
- :can-approve="hasAction"
- :help-path="mr.approvalsHelpPath"
- />
- <approvals-summary
- v-else
- :approved="isApproved"
- :approvals-left="approvals.approvals_left || 0"
- :rules-left="approvals.approvalRuleNamesLeft"
- :approvers="approvedBy"
- />
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div class="gl-display-flex gl-flex-direction-row gl-align-items-center">
+ <gl-button
+ v-if="action"
+ :variant="action.variant"
+ :category="action.category"
+ :loading="isApproving"
+ class="gl-mr-5"
+ data-qa-selector="approve_button"
+ @click="action.action"
+ >
+ {{ action.text }}
+ </gl-button>
+ <approvals-summary-optional
+ v-if="isOptional"
+ :can-approve="hasAction"
+ :help-path="mr.approvalsHelpPath"
+ />
+ <approvals-summary
+ v-else
+ :approved="isApproved"
+ :approvals-left="approvals.approvals_left || 0"
+ :rules-left="approvals.approvalRuleNamesLeft"
+ :approvers="approvedBy"
+ />
+ </div>
+ <div v-if="hasInvalidRules" class="gl-text-gray-400 gl-mt-2" data-testid="invalid-rules">
+ <gl-sprintf :message="pluralizedRuleText">
+ <template #rules>
+ {{ invalidRulesText }}
+ </template>
+ <template #link>
+ <gl-link :href="$options.linkToInvalidRules" target="_blank">
+ {{ $options.i18n.learnMore }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
<slot
:is-approving="isApproving"
:approve-with-auth="approveWithAuth"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
index 0e31f97b9db..b1c4f7c5a7c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -98,10 +98,10 @@ export default {
<template>
<div data-qa-selector="approvals_summary_content">
- <strong>{{ approvalLeftMessage }}</strong>
+ <span class="gl-font-weight-bold">{{ approvalLeftMessage }}</span>
<template v-if="hasApprovers">
<span v-if="approvalLeftMessage">{{ message }}</span>
- <strong v-else>{{ message }}</strong>
+ <span v-else class="gl-font-weight-bold">{{ message }}</span>
<user-avatar-list
class="gl-display-inline-block gl-vertical-align-middle"
:img-size="24"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/humanized_text.js b/app/assets/javascripts/vue_merge_request_widget/components/approvals/humanized_text.js
new file mode 100644
index 00000000000..6689d070053
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/humanized_text.js
@@ -0,0 +1,23 @@
+import { __ } from '~/locale';
+
+const humanizeRules = (invalidRules) => {
+ if (invalidRules.length > 1) {
+ return invalidRules.reduce((rules, { name }, index) => {
+ if (index === invalidRules.length - 1) {
+ return `${rules}${__(' and ')}"${name}"`;
+ }
+ return rules ? `${rules}, "${name}"` : `"${name}"`;
+ }, '');
+ }
+ return `"${invalidRules[0].name}"`;
+};
+
+export const humanizeInvalidApproversRules = (invalidRules) => {
+ const ruleCount = invalidRules.length;
+
+ if (!ruleCount) {
+ return '';
+ }
+
+ return humanizeRules(invalidRules);
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
index d878a1fa2e0..655ceb5f700 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
@@ -26,6 +26,7 @@ export default {
},
methods: {
onClickAction(action) {
+ this.$emit('clickedAction', action);
if (action.onClick) {
action.onClick();
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 0bc17de638b..4ba620da00a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -6,16 +6,16 @@ import {
GlTooltipDirective,
GlIntersectionObserver,
} from '@gitlab/ui';
-import { once } from 'lodash';
import * as Sentry from '@sentry/browser';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
-import api from '~/api';
import { sprintf, s__, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
+import { normalizeHeaders } from '~/lib/utils/common_utils';
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
import StatusIcon from './status_icon.vue';
import Actions from './actions.vue';
import ChildContent from './child_content.vue';
+import { createTelemetryHub } from './telemetry';
import { generateText } from './utils';
export const LOADING_STATES = {
@@ -26,6 +26,7 @@ export const LOADING_STATES = {
};
export default {
+ telemetry: true,
components: {
GlButton,
GlLoadingIcon,
@@ -49,6 +50,7 @@ export default {
showFade: false,
modalData: undefined,
modalName: undefined,
+ telemetry: null,
};
},
computed: {
@@ -131,50 +133,85 @@ export default {
}
},
},
+ created() {
+ if (this.$options.telemetry) {
+ this.telemetry = createTelemetryHub(this.$options.name);
+ }
+ },
mounted() {
this.loadCollapsedData();
+
+ this.telemetry?.viewed();
},
methods: {
- triggerRedisTracking: once(function triggerRedisTracking() {
- if (this.$options.expandEvent) {
- api.trackRedisHllUserEvent(this.$options.expandEvent);
- }
- }),
toggleCollapsed(e) {
if (this.isCollapsible && !e?.target?.closest('.btn:not(.btn-icon),a')) {
- this.isCollapsed = !this.isCollapsed;
+ if (this.isCollapsed) {
+ this.telemetry?.expanded({ type: this.statusIconName });
+ }
- this.triggerRedisTracking();
+ this.isCollapsed = !this.isCollapsed;
}
},
+ initExtensionMultiPolling() {
+ const allData = [];
+ const requests = this.fetchMultiData();
+
+ requests.forEach((request) => {
+ const poll = new Poll({
+ resource: {
+ fetchData: () => request(this),
+ },
+ method: 'fetchData',
+ successCallback: (response) => {
+ this.headerCheck(response, (data) => allData.push(data));
+
+ if (allData.length === requests.length) {
+ this.setCollapsedData(allData);
+ }
+ },
+ errorCallback: (e) => {
+ this.setCollapsedError(e);
+ },
+ });
+
+ poll.makeRequest();
+ });
+ },
initExtensionPolling() {
const poll = new Poll({
resource: {
- fetchData: () => this.fetchCollapsedData(this.$props),
+ fetchData: () => this.fetchCollapsedData(this),
},
method: 'fetchData',
- successCallback: ({ data }) => {
- if (Object.keys(data).length > 0) {
- poll.stop();
- this.setCollapsedData(data);
- }
+ successCallback: (response) => {
+ this.headerCheck(response, (data) => this.setCollapsedData(data));
},
errorCallback: (e) => {
- poll.stop();
-
this.setCollapsedError(e);
},
});
poll.makeRequest();
},
+ headerCheck(response, callback) {
+ const headers = normalizeHeaders(response.headers);
+
+ if (!headers['POLL-INTERVAL']) {
+ callback(response.data);
+ }
+ },
loadCollapsedData() {
this.loadingState = LOADING_STATES.collapsedLoading;
if (this.$options.enablePolling) {
- this.initExtensionPolling();
+ if (this.fetchMultiData) {
+ this.initExtensionMultiPolling();
+ } else {
+ this.initExtensionPolling();
+ }
} else {
- this.fetchCollapsedData(this.$props)
+ this.fetchCollapsedData(this)
.then((data) => {
this.setCollapsedData(data);
})
@@ -197,7 +234,7 @@ export default {
this.loadingState = LOADING_STATES.expandedLoading;
- this.fetchFullData(this.$props)
+ this.fetchFullData(this)
.then((data) => {
this.loadingState = null;
this.fullData = data.map((x, i) => ({ id: i, ...x }));
@@ -231,6 +268,11 @@ export default {
this.toggleCollapsed(e);
}
},
+ onClickedAction(action) {
+ if (action.fullReport) {
+ this.telemetry?.fullReportClicked();
+ }
+ },
generateText,
},
EXTENSION_ICON_CLASS,
@@ -268,6 +310,7 @@ export default {
<actions
:widget="$options.label || $options.name"
:tertiary-buttons="tertiaryActionsButtons"
+ @clickedAction="onClickedAction"
/>
<div
v-if="isCollapsible"
@@ -324,6 +367,7 @@ export default {
:widget-label="widgetLabel"
:modal-id="modalId"
:level="2"
+ @clickedAction="onClickedAction"
/>
</gl-intersection-observer>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
index 0ca4c92a5ae..38f83a61b30 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
@@ -39,6 +39,9 @@ export default {
isArray(arr) {
return Array.isArray(arr);
},
+ onClickedAction(action) {
+ this.$emit('clickedAction', action);
+ },
generateText,
},
};
@@ -63,14 +66,14 @@ export default {
<div class="gl-w-full">
<div class="gl-display-flex gl-flex-nowrap">
<div class="gl-flex-wrap gl-display-flex gl-w-full">
- <div class="gl-mr-4 gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center">
<p v-safe-html="generateText(data.text)" class="gl-m-0"></p>
</div>
- <div v-if="data.link">
+ <div v-if="data.link" class="gl-pr-2">
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
</div>
- <div v-if="data.modal">
- <gl-link v-gl-modal="modalId" @click="data.modal.onClick">
+ <div v-if="data.modal" class="gl-pr-2">
+ <gl-link v-gl-modal="modalId" data-testid="modal-link" @click="data.modal.onClick">
{{ data.modal.text }}
</gl-link>
</div>
@@ -81,7 +84,12 @@ export default {
{{ data.badge.text }}
</gl-badge>
</div>
- <actions :widget="widgetLabel" :tertiary-buttons="data.actions" class="gl-ml-auto" />
+ <actions
+ :widget="widgetLabel"
+ :tertiary-buttons="data.actions"
+ class="gl-ml-auto gl-pl-3"
+ @clickedAction="onClickedAction"
+ />
</div>
<p
v-if="data.subtext"
@@ -101,6 +109,7 @@ export default {
:modal-id="modalId"
:level="3"
data-testid="child-content"
+ @clickedAction="onClickedAction"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
index b9dfd3bd41e..a58d524b9ed 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
@@ -14,7 +14,7 @@ export default {
if (extensions.length === 0) return null;
return h(
- 'div',
+ 'section',
{
attrs: {
role: 'region',
@@ -34,13 +34,7 @@ export default {
{ ...extension },
{
props: {
- ...extension.props.reduce(
- (acc, key) => ({
- ...acc,
- [key]: this.mr[key],
- }),
- {},
- ),
+ mr: this.mr,
},
},
),
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index 65273678fb9..f4fcf4c9571 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -10,12 +10,27 @@ export const registerExtension = (extension) => {
registeredExtensions.extensions.push({
extends: ExtensionBase,
name: extension.name,
- props: extension.props,
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ telemetry: extension.telemetry,
i18n: extension.i18n,
expandEvent: extension.expandEvent,
enablePolling: extension.enablePolling,
modalComponent: extension.modalComponent,
computed: {
+ ...extension.props.reduce(
+ (acc, propKey) => ({
+ ...acc,
+ [propKey]() {
+ return this.mr[propKey];
+ },
+ }),
+ {},
+ ),
...Object.keys(extension.computed).reduce(
(acc, computedKey) => ({
...acc,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
index 456a1f17aae..bb626c9adba 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
@@ -49,7 +49,7 @@ export default {
]"
class="gl-rounded-full gl-mr-3 gl-relative gl-p-2"
>
- <gl-loading-icon v-if="isLoading" size="lg" inline class="gl-display-block" />
+ <gl-loading-icon v-if="isLoading" size="sm" inline class="gl-display-block" />
<gl-icon
v-else
:name="$options.EXTENSION_ICON_NAMES[iconName]"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
new file mode 100644
index 00000000000..aec3a35f37c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
@@ -0,0 +1,207 @@
+import api from '~/api';
+import createEventHub from '~/helpers/event_hub_factory';
+import {
+ TELEMETRY_WIDGET_VIEWED,
+ TELEMETRY_WIDGET_EXPANDED,
+ TELEMETRY_WIDGET_FULL_REPORT_CLICKED,
+} from '../../constants';
+
+/*
+ * Additional events to send beyond the defaults for certain widget extensions
+ */
+const nonStandardEvents = {
+ codeQuality: {
+ uniqueUser: {
+ expand: ['i_testing_code_quality_widget_total'],
+ },
+ counter: {},
+ },
+ terraform: {
+ uniqueUser: {
+ expand: ['i_testing_terraform_widget_total'],
+ },
+ counter: {},
+ },
+ issues: {
+ uniqueUser: {
+ expand: ['i_testing_load_performance_widget_total'],
+ },
+ counter: {},
+ },
+ testReport: {
+ uniqueUser: {
+ expand: ['i_testing_summary_widget_total'],
+ },
+ counter: {},
+ },
+};
+
+function combineDeepArray(path, ...objects) {
+ const parts = path.split('.');
+ const allEntries = objects.reduce((entries, currentObject) => {
+ let expandedEntries = entries;
+ let traversed = currentObject;
+
+ parts.forEach((part) => {
+ traversed = traversed?.[part];
+ });
+
+ if (traversed) {
+ expandedEntries = [...entries, ...traversed];
+ }
+
+ return expandedEntries;
+ }, []);
+
+ return Array.from(new Set(allEntries));
+}
+
+function simplifyWidgetName(componentName) {
+ const noWidget = componentName.replace(/^Widget/, '');
+
+ return noWidget.charAt(0).toLowerCase() + noWidget.slice(1);
+}
+
+function baseRedisEventName(extensionName) {
+ const redisEventName = extensionName.replace(/([A-Z])/g, '_$1').toLowerCase();
+
+ return `i_merge_request_widget_${redisEventName}`;
+}
+
+function whenable(bus) {
+ return function when(event) {
+ return ({ execute, track, special }) => {
+ bus.$on(event, (busEvent) => {
+ track.forEach((redisEvent) => {
+ execute(redisEvent);
+ });
+
+ special?.({ event, execute, track, bus, busEvent });
+ });
+ };
+ };
+}
+
+function defaultBehaviorEvents({ bus, config }) {
+ const when = whenable(bus);
+ const isViewed = when(TELEMETRY_WIDGET_VIEWED);
+ const isExpanded = when(TELEMETRY_WIDGET_EXPANDED);
+ const fullReportIsClicked = when(TELEMETRY_WIDGET_FULL_REPORT_CLICKED);
+ const toHll = config?.uniqueUser || {};
+ const toCounts = config?.counter || {};
+ const user = api.trackRedisHllUserEvent.bind(api);
+ const count = api.trackRedisCounterEvent.bind(api);
+
+ if (toHll.view) {
+ isViewed({ execute: user, track: toHll.view });
+ }
+ if (toCounts.view) {
+ isViewed({ execute: count, track: toCounts.view });
+ }
+
+ if (toHll.expand) {
+ isExpanded({
+ execute: user,
+ track: toHll.expand,
+ special: ({ execute, track, busEvent }) => {
+ if (busEvent.type) {
+ track.forEach((event) => {
+ execute(`${event}_${busEvent.type}`);
+ });
+ }
+ },
+ });
+ }
+ if (toCounts.expand) {
+ isExpanded({
+ execute: count,
+ track: toCounts.expand,
+ special: ({ execute, track, busEvent }) => {
+ if (busEvent.type) {
+ track.forEach((event) => {
+ execute(`${event}_${busEvent.type}`);
+ });
+ }
+ },
+ });
+ }
+
+ if (toHll.clickFullReport) {
+ fullReportIsClicked({ execute: user, track: toHll.clickFullReport });
+ }
+ if (toCounts.clickFullReport) {
+ fullReportIsClicked({ execute: count, track: toCounts.clickFullReport });
+ }
+}
+
+function baseTelemetry(componentName) {
+ const simpleExtensionName = simplifyWidgetName(componentName);
+ const additionalNonStandard = nonStandardEvents[simpleExtensionName] || {};
+ /*
+ * Telemetry config format is:
+ * {
+ * TELEMETRY_TYPE: {
+ * BEHAVIOR: [ EVENT_NAME, ... ]
+ * }
+ * }
+ *
+ * Right now, there are currently configurations for these telemetry types:
+ * - uniqueUser is sent to RedisHLL
+ * - counter is sent to a regular Redis counter
+ */
+ const defaultTelemetry = {
+ uniqueUser: {
+ view: [`${baseRedisEventName(simpleExtensionName)}_view`],
+ expand: [`${baseRedisEventName(simpleExtensionName)}_expand`],
+ clickFullReport: [`${baseRedisEventName(simpleExtensionName)}_click_full_report`],
+ },
+ counter: {
+ view: [`${baseRedisEventName(simpleExtensionName)}_count_view`],
+ expand: [`${baseRedisEventName(simpleExtensionName)}_count_expand`],
+ clickFullReport: [`${baseRedisEventName(simpleExtensionName)}_count_click_full_report`],
+ },
+ };
+
+ return {
+ uniqueUser: {
+ view: combineDeepArray('uniqueUser.view', defaultTelemetry, additionalNonStandard),
+ expand: combineDeepArray('uniqueUser.expand', defaultTelemetry, additionalNonStandard),
+ clickFullReport: combineDeepArray(
+ 'uniqueUser.clickFullReport',
+ defaultTelemetry,
+ additionalNonStandard,
+ ),
+ },
+ counter: {
+ view: combineDeepArray('counter.view', defaultTelemetry, additionalNonStandard),
+ expand: combineDeepArray('counter.expand', defaultTelemetry, additionalNonStandard),
+ clickFullReport: combineDeepArray(
+ 'counter.clickFullReport',
+ defaultTelemetry,
+ additionalNonStandard,
+ ),
+ },
+ };
+}
+
+export function createTelemetryHub(componentName) {
+ const bus = createEventHub();
+ const config = baseTelemetry(componentName);
+
+ defaultBehaviorEvents({ bus, config });
+
+ return {
+ viewed() {
+ bus.$emit(TELEMETRY_WIDGET_VIEWED);
+ },
+ expanded({ type }) {
+ bus.$emit(TELEMETRY_WIDGET_EXPANDED, { type });
+ },
+ fullReportClicked() {
+ bus.$emit(TELEMETRY_WIDGET_FULL_REPORT_CLICKED);
+ },
+ /* I want a Record here: #{ ...config } // and then the comment would be: This is for debugging only, changing your reference to it does nothing. 😘 */
+ config: Object.freeze({ ...config }), // This is *intended* to be for debugging only, but it's pretty mutable, and it has references to live data in child props
+ bus,
+ };
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
index f5667aee15b..f8d2732b385 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
@@ -32,7 +32,7 @@ export default {
computed: {
arrowIconName() {
- return this.isCollapsed ? 'angle-right' : 'angle-down';
+ return this.isCollapsed ? 'chevron-lg-right' : 'chevron-lg-down';
},
ariaLabel() {
return this.isCollapsed ? __('Expand') : __('Collapse');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
deleted file mode 100644
index e1d88099580..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ /dev/null
@@ -1,100 +0,0 @@
-<script>
-import {
- GlLink,
- GlTooltipDirective,
- GlModalDirective,
- GlSafeHtmlDirective as SafeHtml,
- GlSprintf,
-} from '@gitlab/ui';
-import { constructWebIDEPath } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
-import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import MrWidgetIcon from './mr_widget_icon.vue';
-
-export default {
- name: 'MRWidgetHeader',
- components: {
- clipboardButton,
- TooltipOnTruncate,
- MrWidgetIcon,
- GlLink,
- GlSprintf,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- GlModalDirective,
- SafeHtml,
- },
- props: {
- mr: {
- type: Object,
- required: true,
- },
- },
- computed: {
- shouldShowCommitsBehindText() {
- return this.mr.divergedCommitsCount > 0;
- },
- branchNameClipboardData() {
- // This supports code in app/assets/javascripts/copy_to_clipboard.js that
- // works around ClipboardJS limitations to allow the context-specific
- // copy/pasting of plain text or GFM.
- return JSON.stringify({
- text: this.mr.sourceBranch,
- gfm: `\`${this.mr.sourceBranch}\``,
- });
- },
- webIdePath() {
- return constructWebIDEPath(this.mr);
- },
- isFork() {
- return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
- },
- },
- i18n: {
- webIdeText: s__('mrWidget|Open in Web IDE'),
- gitpodText: s__('mrWidget|Open in Gitpod'),
- },
-};
-</script>
-<template>
- <div class="gl-display-flex mr-source-target">
- <mr-widget-icon name="git-merge" />
- <div class="git-merge-container d-flex">
- <div class="normal">
- <strong>
- {{ s__('mrWidget|Request to merge') }}
- <tooltip-on-truncate
- v-safe-html="mr.sourceBranchLink"
- :title="mr.sourceBranch"
- truncate-target="child"
- class="label-branch label-truncate js-source-branch"
- /><clipboard-button
- data-testid="mr-widget-copy-clipboard"
- :text="branchNameClipboardData"
- :title="__('Copy branch name')"
- category="tertiary"
- />
- {{ s__('mrWidget|into') }}
- <tooltip-on-truncate
- :title="mr.targetBranch"
- truncate-target="child"
- class="label-branch label-truncate"
- >
- <a :href="mr.targetBranchTreePath" class="js-target-branch"> {{ mr.targetBranch }} </a>
- </tooltip-on-truncate>
- </strong>
- <div v-if="shouldShowCommitsBehindText" class="diverged-commits-count">
- <gl-sprintf :message="s__('mrWidget|The source branch is %{link} the target branch')">
- <template #link>
- <gl-link :href="mr.targetBranchPath">{{
- n__('%d commit behind', '%d commits behind', mr.divergedCommitsCount)
- }}</gl-link>
- </template>
- </gl-sprintf>
- </div>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
index 472df8e3110..437342bf438 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
@@ -13,7 +13,7 @@ export default {
</script>
<template>
- <div class="circle-icon-container gl-mr-3 align-self-start align-self-lg-center">
+ <div class="circle-icon-container gl-mr-3 align-self-start">
<gl-icon :name="name" :size="24" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 3b3b46e9772..1e1a2049414 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -172,7 +172,7 @@ export default {
</p>
</template>
<template v-else-if="!hasPipeline">
- <gl-loading-icon size="md" />
+ <gl-loading-icon size="lg" />
<p
class="gl-flex-grow-1 gl-display-flex gl-ml-3 gl-mb-0"
data-testid="monitoring-pipeline-message"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index b8a1f89d232..913aa0e1e34 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -93,9 +93,7 @@ export default {
</span>
</p>
<div
- v-if="
- divergedCommitsCount > 0 && glFeatures.updatedMrHeader && !glFeatures.restructuredMrWidget
- "
+ v-if="divergedCommitsCount > 0 && !glFeatures.restructuredMrWidget"
class="diverged-commits-count"
>
<gl-sprintf :message="s__('mrWidget|The source branch is %{link} the target branch')">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index 8b410926c46..45958d7fb8d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -113,7 +113,7 @@ export default {
data-testid="ok"
category="primary"
class="gl-mt-2"
- variant="info"
+ variant="confirm"
:href="pipelinePath"
:data-track-property="humanAccess"
:data-track-value="$options.SP_SHOW_TRACK_VALUE"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 4fb95fe635c..cf482410bef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -417,6 +417,7 @@ export default {
}
this.isMakingRequest = true;
+ this.editCommitMessage = false;
if (!useAutoMerge) {
this.mr.transitionStateMachine({ transition: MERGE });
@@ -663,7 +664,11 @@ export default {
<gl-sprintf v-else :message="mergeDisabledText" />
</div>
<template v-if="glFeatures.restructuredMrWidget">
- <div v-show="editCommitMessage" class="gl-w-full gl-order-n1">
+ <div
+ v-if="editCommitMessage"
+ class="gl-w-full gl-order-n1"
+ data-testid="edit_commit_message"
+ >
<ul
:class="{
'content-list': !glFeatures.restructuredMrWidget,
@@ -711,15 +716,13 @@ export default {
<div
v-if="!restructuredWidgetShowMergeButtons"
class="gl-w-full gl-order-n1 gl-text-gray-500"
+ data-qa-selector="merged_status_content"
>
<strong v-if="mr.state !== 'closed'">
{{ __('Merge details') }}
</strong>
<ul class="gl-pl-4 gl-m-0">
- <li
- v-if="mr.divergedCommitsCount > 0 && glFeatures.updatedMrHeader"
- class="gl-line-height-normal"
- >
+ <li v-if="mr.divergedCommitsCount > 0" class="gl-line-height-normal">
<gl-sprintf
:message="s__('mrWidget|The source branch is %{link} the target branch')"
>
@@ -788,11 +791,7 @@ export default {
</div>
</div>
<template v-if="shouldShowMergeControls && !glFeatures.restructuredMrWidget">
- <div
- v-if="!shouldShowMergeEdit"
- class="mr-fast-forward-message"
- data-qa-selector="fast_forward_message_content"
- >
+ <div v-if="!shouldShowMergeEdit" class="mr-fast-forward-message">
{{ __('Fast-forward merge without a merge commit') }}
</div>
<commits-header
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index e0e19094c40..5bd7745d704 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -126,6 +126,7 @@ export default {
}) => {
toast(__('Marked as ready. Merging is now allowed.'));
$('.merge-request .detail-page-description .title').text(title);
+ eventHub.$emit('MRWidgetUpdateRequested');
},
)
.catch(() =>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
index 2ba945a3ecf..18fdb29ba54 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { n__ } from '~/locale';
@@ -9,7 +9,7 @@ import TerraformPlan from './terraform_plan.vue';
export default {
name: 'MRWidgetTerraformContainer',
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
GlSprintf,
MrWidgetExpanableSection,
TerraformPlan,
@@ -100,7 +100,7 @@ export default {
<template>
<section class="mr-widget-section">
<div v-if="loading" class="mr-widget-body">
- <gl-skeleton-loading />
+ <gl-skeleton-loader />
</div>
<mr-widget-expanable-section v-else>