diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index ccdc8cf259f..bc811e0504c 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -0c29d111e214eff897572517c13dcf3f524b3c19 +c6c944b8fd09e6fa3b9a607ffdea574a87a56d93 diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js index 7a9f89a0c87..0c393ca6b2f 100644 --- a/app/assets/javascripts/analytics/shared/constants.js +++ b/app/assets/javascripts/analytics/shared/constants.js @@ -75,6 +75,16 @@ export const MAX_DATE_RANGE_TEXT = (maxDateRange) => { ); }; +// Limits the number of decimals we round values to +export const MAX_METRIC_PRECISION = 4; + +export const UNITS = { + COUNT: 'COUNT', + DAYS: 'DAYS', + PER_DAY: 'PER_DAY', + PERCENT: 'PERCENT', +}; + export const NUMBER_OF_DAYS_SELECTED = (numDays) => { return n__('1 day selected', '%d days selected', numDays); }; @@ -134,8 +144,17 @@ export const AI_METRICS = { DUO_CHAT_USAGE_RATE: 'duo_chat_usage_rate', }; -export const METRIC_TOOLTIPS = { +export const VALUE_STREAM_METRIC_DISPLAY_UNITS = { + [UNITS.COUNT]: '', + [UNITS.DAYS]: __('days'), + [UNITS.PER_DAY]: __('/day'), + [UNITS.PERCENT]: '%', +}; + +export const VALUE_STREAM_METRIC_TILE_METADATA = { [DORA_METRICS.DEPLOYMENT_FREQUENCY]: { + label: s__('DORA4Metrics|Deployment frequency'), + unit: UNITS.PER_DAY, description: s__( 'ValueStreamAnalytics|Average number of deployments to production per day. This metric measures how often value is delivered to end users.', ), @@ -144,6 +163,8 @@ export const METRIC_TOOLTIPS = { docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'deployment-frequency' }), }, [DORA_METRICS.LEAD_TIME_FOR_CHANGES]: { + label: s__('DORA4Metrics|Lead time for changes'), + unit: UNITS.DAYS, description: s__( 'ValueStreamAnalytics|The time to successfully deliver a commit into production. This metric reflects the efficiency of CI/CD pipelines.', ), @@ -152,6 +173,8 @@ export const METRIC_TOOLTIPS = { docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'lead-time-for-changes' }), }, [DORA_METRICS.TIME_TO_RESTORE_SERVICE]: { + label: s__('DORA4Metrics|Time to restore service'), + unit: UNITS.DAYS, description: s__( 'ValueStreamAnalytics|The time it takes an organization to recover from a failure in production.', ), @@ -160,22 +183,27 @@ export const METRIC_TOOLTIPS = { docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'time-to-restore-service' }), }, [DORA_METRICS.CHANGE_FAILURE_RATE]: { + label: s__('DORA4Metrics|Change failure rate'), description: s__( 'ValueStreamAnalytics|Percentage of deployments that cause an incident in production.', ), groupLink: '-/analytics/ci_cd?tab=change-failure-rate', projectLink: '-/pipelines/charts?chart=change-failure-rate', docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'change-failure-rate' }), + unit: UNITS.PERCENT, }, [FLOW_METRICS.LEAD_TIME]: { + label: s__('DORA4Metrics|Lead time'), description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'), groupLink: '-/analytics/value_stream_analytics', projectLink: '-/value_stream_analytics', docsLink: helpPagePath('user/group/value_stream_analytics/index', { anchor: 'lifecycle-metrics', }), + unit: UNITS.DAYS, }, [FLOW_METRICS.CYCLE_TIME]: { + label: s__('DORA4Metrics|Cycle time'), description: s__( "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.", ), @@ -184,25 +212,39 @@ export const METRIC_TOOLTIPS = { docsLink: helpPagePath('user/group/value_stream_analytics/index', { anchor: 'lifecycle-metrics', }), + unit: UNITS.DAYS, }, [FLOW_METRICS.ISSUES]: { + label: s__('DORA4Metrics|Issues created'), + unit: UNITS.COUNT, description: s__('ValueStreamAnalytics|Number of new issues created.'), groupLink: '-/issues_analytics', projectLink: '-/analytics/issues_analytics', docsLink: helpPagePath('user/group/issues_analytics/index'), }, + [FLOW_METRICS.COMMITS]: { + label: s__('DORA4Metrics|Commits'), + unit: UNITS.COUNT, + description: s__('ValueStreamAnalytics|Number of commits pushed to the default branch'), + }, + [FLOW_METRICS.DEPLOYS]: { + label: s__('DORA4Metrics|Deploys'), + unit: UNITS.COUNT, + description: s__('ValueStreamAnalytics|Total number of deploys to production.'), + groupLink: '-/analytics/productivity_analytics', + projectLink: '-/analytics/merge_request_analytics', + docsLink: helpPagePath('user/analytics/merge_request_analytics'), + }, +}; + +export const VALUE_STREAM_METRIC_METADATA = { + ...VALUE_STREAM_METRIC_TILE_METADATA, [FLOW_METRICS.ISSUES_COMPLETED]: { description: s__('ValueStreamAnalytics|Number of issues closed by month.'), groupLink: '-/issues_analytics', projectLink: '-/analytics/issues_analytics', docsLink: helpPagePath('user/group/issues_analytics/index'), }, - [FLOW_METRICS.DEPLOYS]: { - description: s__('ValueStreamAnalytics|Total number of deploys to production.'), - groupLink: '-/analytics/productivity_analytics', - projectLink: '-/analytics/merge_request_analytics', - docsLink: helpPagePath('user/analytics/merge_request_analytics'), - }, [CONTRIBUTOR_METRICS.COUNT]: { description: s__( 'ValueStreamAnalytics|Number of monthly unique users with contributions in the group.', @@ -254,42 +296,6 @@ export const METRIC_TOOLTIPS = { }, }; -// TODO: Remove this once the migration to METRIC_TOOLTIPS is complete -// https://gitlab.com/gitlab-org/gitlab/-/issues/388067 -export const METRICS_POPOVER_CONTENT = { - lead_time: { - description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'), - }, - cycle_time: { - description: s__( - "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.", - ), - }, - lead_time_for_changes: { - description: s__( - 'ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period.', - ), - }, - issues: { description: s__('ValueStreamAnalytics|Number of new issues created.') }, - deploys: { description: s__('ValueStreamAnalytics|Total number of deploys to production.') }, - deployment_frequency: { - description: s__('ValueStreamAnalytics|Average number of deployments to production per day.'), - }, - commits: { - description: s__('ValueStreamAnalytics|Number of commits pushed to the default branch'), - }, - time_to_restore_service: { - description: s__( - 'ValueStreamAnalytics|Median time an incident was open on a production environment in the given time period.', - ), - }, - change_failure_rate: { - description: s__( - 'ValueStreamAnalytics|Percentage of deployments that cause an incident in production.', - ), - }, -}; - export const USAGE_OVERVIEW_NO_DATA_ERROR = s__( 'ValueStreamAnalytics|Failed to load usage overview data', ); diff --git a/app/assets/javascripts/analytics/shared/graphql/constants.js b/app/assets/javascripts/analytics/shared/graphql/constants.js new file mode 100644 index 00000000000..dd913409276 --- /dev/null +++ b/app/assets/javascripts/analytics/shared/graphql/constants.js @@ -0,0 +1,2 @@ +export const BUCKETING_INTERVAL_ALL = 'ALL'; +export const BUCKETING_INTERVAL_MONTHLY = 'MONTHLY'; diff --git a/app/assets/javascripts/analytics/shared/graphql/dora_metric_item.fragment.graphql b/app/assets/javascripts/analytics/shared/graphql/dora_metric_item.fragment.graphql new file mode 100644 index 00000000000..780dbcc32f9 --- /dev/null +++ b/app/assets/javascripts/analytics/shared/graphql/dora_metric_item.fragment.graphql @@ -0,0 +1,7 @@ +fragment DoraMetricItem on DoraMetric { + date + deployment_frequency: deploymentFrequency + change_failure_rate: changeFailureRate + lead_time_for_changes: leadTimeForChanges + time_to_restore_service: timeToRestoreService +} diff --git a/app/assets/javascripts/analytics/shared/graphql/dora_metrics.query.graphql b/app/assets/javascripts/analytics/shared/graphql/dora_metrics.query.graphql new file mode 100644 index 00000000000..ae48c0117bb --- /dev/null +++ b/app/assets/javascripts/analytics/shared/graphql/dora_metrics.query.graphql @@ -0,0 +1,25 @@ +#import "./dora_metric_item.fragment.graphql" + +query doraMetricsQuery( + $fullPath: ID! + $startDate: Date! + $endDate: Date! + $interval: DoraMetricBucketingInterval! +) { + project(fullPath: $fullPath) { + id + dora { + metrics(startDate: $startDate, endDate: $endDate, interval: $interval) { + ...DoraMetricItem + } + } + } + group(fullPath: $fullPath) { + id + dora { + metrics(startDate: $startDate, endDate: $endDate, interval: $interval) { + ...DoraMetricItem + } + } + } +} diff --git a/app/assets/javascripts/analytics/shared/graphql/flow_metric_item.fragment.graphql b/app/assets/javascripts/analytics/shared/graphql/flow_metric_item.fragment.graphql new file mode 100644 index 00000000000..74756649ebf --- /dev/null +++ b/app/assets/javascripts/analytics/shared/graphql/flow_metric_item.fragment.graphql @@ -0,0 +1,12 @@ +fragment FlowMetricItem on ValueStreamAnalyticsMetric { + unit + value + identifier + links { + label + name + docsLink + url + } + title +} diff --git a/app/assets/javascripts/analytics/shared/graphql/flow_metrics.query.graphql b/app/assets/javascripts/analytics/shared/graphql/flow_metrics.query.graphql new file mode 100644 index 00000000000..0132ce7cbc0 --- /dev/null +++ b/app/assets/javascripts/analytics/shared/graphql/flow_metrics.query.graphql @@ -0,0 +1,58 @@ +#import "./flow_metric_item.fragment.graphql" + +query flowMetricsQuery($fullPath: ID!, $startDate: Time!, $endDate: Time!, $labelNames: [String!]) { + project(fullPath: $fullPath) { + id + flowMetrics { + issues: issueCount(from: $startDate, to: $endDate, labelNames: $labelNames) { + ...FlowMetricItem + } + issues_completed: issuesCompletedCount( + from: $startDate + to: $endDate + labelNames: $labelNames + ) { + ...FlowMetricItem + } + cycle_time: cycleTime(from: $startDate, to: $endDate, labelNames: $labelNames) { + ...FlowMetricItem + } + lead_time: leadTime(from: $startDate, to: $endDate, labelNames: $labelNames) { + ...FlowMetricItem + } + deploys: deploymentCount(from: $startDate, to: $endDate) { + ...FlowMetricItem + } + median_time_to_merge: timeToMerge(from: $startDate, to: $endDate) { + ...FlowMetricItem + } + } + } + group(fullPath: $fullPath) { + id + flowMetrics { + issues: issueCount(from: $startDate, to: $endDate, labelNames: $labelNames) { + ...FlowMetricItem + } + issues_completed: issuesCompletedCount( + from: $startDate + to: $endDate + labelNames: $labelNames + ) { + ...FlowMetricItem + } + cycle_time: cycleTime(from: $startDate, to: $endDate, labelNames: $labelNames) { + ...FlowMetricItem + } + lead_time: leadTime(from: $startDate, to: $endDate, labelNames: $labelNames) { + ...FlowMetricItem + } + deploys: deploymentCount(from: $startDate, to: $endDate) { + ...FlowMetricItem + } + median_time_to_merge: timeToMerge(from: $startDate, to: $endDate) { + ...FlowMetricItem + } + } + } +} diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js index 99a9f05cd5d..98ad20b9b14 100644 --- a/app/assets/javascripts/analytics/shared/utils.js +++ b/app/assets/javascripts/analytics/shared/utils.js @@ -3,7 +3,7 @@ import dateFormat from '~/lib/dateformat'; import { slugify } from '~/lib/utils/text_utility'; import { joinPaths } from '~/lib/utils/url_utility'; import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; -import { dateFormats, METRICS_POPOVER_CONTENT } from './constants'; +import { dateFormats, VALUE_STREAM_METRIC_METADATA } from './constants'; export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => { if (!searchTerm?.length) return data; @@ -117,7 +117,7 @@ const requestData = ({ request, endpoint, requestPath, params, name }) => { export const fetchMetricsData = (requests = [], requestPath, params) => { const promises = requests.map((r) => requestData({ ...r, requestPath, params })); return Promise.all(promises).then((responses) => - prepareTimeMetricsData(flatten(responses), METRICS_POPOVER_CONTENT), + prepareTimeMetricsData(flatten(responses), VALUE_STREAM_METRIC_METADATA), ); }; diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue index a599a8bf35c..9ce061157bf 100644 --- a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue +++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue @@ -229,7 +229,9 @@ export default { " >

diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js index cf9794e6a87..8cd150279c0 100644 --- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js @@ -1,3 +1,5 @@ +import { EMOJI_VERSION } from '~/emoji'; + // On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ const flagACodePoint = 127462; // parseInt('1F1E6', 16) const flagZCodePoint = 127487; // parseInt('1F1FF', 16) @@ -72,6 +74,14 @@ function isPersonZwjEmoji(emojiUnicode) { return hasPersonEmoji && hasZwj; } +// If the backend emoji support is newer, then there may already be emojis in use +// that were not "supported" before but were displayable. In that scenario, we want to +// allow those emojis to be recognized and displayed, until the frontend (usually in the +// following release) is updated. +function isBackendEmojiNewer() { + return EMOJI_VERSION < gon.emoji_backend_version; +} + // Helper so we don't have to run `isFlagEmoji` twice // in `isEmojiUnicodeSupported` logic function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { @@ -119,7 +129,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe // For comments about each scenario, see the comments above each individual respective function return ( - unicodeSupportMap[unicodeVersion] && + (unicodeSupportMap[unicodeVersion] || isBackendEmojiNewer()) && !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && diff --git a/app/assets/javascripts/glql/components/presenters/list.vue b/app/assets/javascripts/glql/components/presenters/list.vue index 84d5603ac53..ed52e1200a8 100644 --- a/app/assets/javascripts/glql/components/presenters/list.vue +++ b/app/assets/javascripts/glql/components/presenters/list.vue @@ -1,10 +1,15 @@ @@ -43,16 +54,23 @@ export default { :key="itemIndex" :data-testid="`list-item-${itemIndex}`" > - + -
  • - {{ __('No data found for this query') }} -
  • +
    + {{ __('No data found for this query') }} +
    - {{ __('Generated by GLQL') }} +
    + + + + +
    diff --git a/app/assets/javascripts/glql/components/presenters/table.vue b/app/assets/javascripts/glql/components/presenters/table.vue index 326455c7cb1..bfb941b5d2f 100644 --- a/app/assets/javascripts/glql/components/presenters/table.vue +++ b/app/assets/javascripts/glql/components/presenters/table.vue @@ -1,5 +1,7 @@ diff --git a/app/assets/javascripts/integrations/beyond_identity/components/exclusions_list.vue b/app/assets/javascripts/integrations/beyond_identity/components/exclusions_list.vue index 47cc8f45aff..eacd83c7959 100644 --- a/app/assets/javascripts/integrations/beyond_identity/components/exclusions_list.vue +++ b/app/assets/javascripts/integrations/beyond_identity/components/exclusions_list.vue @@ -201,7 +201,7 @@ export default { class="gl-border-b gl-flex gl-items-center gl-justify-between gl-bg-gray-10 gl-p-4 gl-py-5" > {{ $options.i18n.helpText }} - {{ + {{ $options.i18n.addExclusions }} diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue index 288b8ffc90b..25d4588f97c 100644 --- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue +++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue @@ -278,19 +278,25 @@ export default { v-if="showSaveAndAdd" variant="confirm" category="secondary" + data-testid="save-and-add-button" :disabled="!isTimelineTextValid" :loading="isEventProcessed" @click="handleSave(true)" > {{ $options.i18n.saveAndAdd }} - + {{ $options.i18n.cancel }} diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue index af770e92d18..2af48f4bcc5 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue @@ -170,6 +170,7 @@ export default { {{ s__('ContainerRegistry|Add rule') }} @@ -323,6 +324,7 @@ export default { :title="__('Delete')" :aria-label="__('Delete')" :disabled="isProtectionRuleDeleteButtonDisabled(item)" + data-testid="delete-btn" @click="showProtectionRuleDeletionConfirmModal(item)" /> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue index 99e6157bb4c..26e9c51c84a 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue @@ -203,6 +203,7 @@ export default { type="submit" :disabled="isSubmitButtonDisabled" :loading="showLoadingIcon" + data-testid="add-rule-btn" >{{ s__('PackageRegistry|Add rule') }} {{ __('Cancel') }} diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue index 672423b0aa3..f6127a64e88 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue @@ -305,6 +305,7 @@ export default { :aria-label="$options.i18n.minimumAccessLevelForPush" :options="minimumAccessLevelOptions" :disabled="isProtectionRuleMinimumAccessLevelFormSelectDisabled(item)" + data-testid="push-access-select" @change="updatePackageProtectionRule(item)" /> @@ -317,6 +318,7 @@ export default { icon="remove" :title="__('Delete')" :aria-label="__('Delete')" + data-testid="delete-rule-btn" :disabled="isProtectionRuleDeleteButtonDisabled(item)" @click="showProtectionRuleDeletionConfirmModal(item)" /> diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/rule_drawer.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/rule_drawer.vue index f13671952ff..156c7fc42b0 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/rule_drawer.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/rule_drawer.vue @@ -250,7 +250,12 @@ export default { > {{ $options.i18n.saveChanges }} - + {{ $options.i18n.cancel }} diff --git a/app/assets/javascripts/projects/your_work/components/app.vue b/app/assets/javascripts/projects/your_work/components/app.vue index 3c1253d333a..868376e5784 100644 --- a/app/assets/javascripts/projects/your_work/components/app.vue +++ b/app/assets/javascripts/projects/your_work/components/app.vue @@ -222,9 +222,13 @@ export default { diff --git a/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue b/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue index d17144669fe..6e855b09cd0 100644 --- a/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue +++ b/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue @@ -66,12 +66,14 @@ export default { {{ $options.i18n.viewTagButton }} {{ $options.i18n.viewBranchButton }} 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 9a39fd3e66c..0c5f3a1b9b3 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 @@ -617,11 +617,10 @@ export default {