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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-02 12:09:46 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-02 12:09:46 +0300
commitb2180a27bcf74e622df4d7fb173306d80b973a6c (patch)
treeb0966750894f10d6592a4c578d5687c169cd5e41 /app
parentb3e13e0dfd7e26ed569aa9b46f4ec55b41a62411 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue102
-rw-r--r--app/assets/javascripts/cycle_analytics/components/path_navigation.vue117
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/store/getters.js10
-rw-r--r--app/assets/javascripts/cycle_analytics/store/index.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutations.js5
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js109
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue2
-rw-r--r--app/assets/stylesheets/page_bundles/cycle_analytics.scss27
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/models/concerns/integrations/slack_mattermost_notifier.rb (renamed from app/models/project_services/slack_mattermost/notifier.rb)4
-rw-r--r--app/models/concerns/issue_available_features.rb3
-rw-r--r--app/models/integrations/mattermost.rb2
-rw-r--r--app/models/integrations/slack.rb2
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/serializers/analytics_stage_entity.rb6
-rw-r--r--app/views/admin/application_settings/integrations.html.haml2
-rw-r--r--app/views/groups/settings/integrations/index.html.haml2
18 files changed, 294 insertions, 112 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 11a263015e4..e3703fc066d 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -1,7 +1,8 @@
<script>
import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import { __ } from '~/locale';
import banner from './banner.vue';
import stageCodeComponent from './stage_code_component.vue';
@@ -29,6 +30,7 @@ export default {
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
'stage-nav-item': stageNavItem,
+ PathNavigation,
},
props: {
noDataSvgPath: {
@@ -56,17 +58,19 @@ export default {
'summary',
'startDate',
]),
+ ...mapGetters(['pathNavigationData']),
displayStageEvents() {
const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
},
displayNotEnoughData() {
- const { selectedStage, isEmptyStage, isLoadingStage } = this;
- return selectedStage && isEmptyStage && !isLoadingStage;
+ return this.selectedStageReady && this.isEmptyStage;
},
displayNoAccess() {
- const { selectedStage } = this;
- return selectedStage && !selectedStage.isUserAllowed;
+ return this.selectedStageReady && !this.selectedStage.isUserAllowed;
+ },
+ selectedStageReady() {
+ return !this.isLoadingStage && this.selectedStage;
},
},
methods: {
@@ -83,8 +87,8 @@ export default {
isActiveStage(stage) {
return stage.slug === this.selectedStage.slug;
},
- selectStage(stage) {
- if (this.selectedStage === stage) return;
+ onSelectStage(stage) {
+ if (this.isLoadingStage || this.selectedStage?.slug === stage?.slug) return;
this.setSelectedStage(stage);
if (!stage.isUserAllowed) {
@@ -106,9 +110,23 @@ export default {
</script>
<template>
<div class="cycle-analytics">
+ <path-navigation
+ v-if="selectedStageReady"
+ class="js-path-navigation gl-w-full gl-pb-2"
+ :loading="isLoading"
+ :stages="pathNavigationData"
+ :selected-stage="selectedStage"
+ :with-stage-counts="false"
+ @selected="onSelectStage"
+ />
<gl-loading-icon v-if="isLoading" size="lg" />
<div v-else class="wrapper">
- <div class="card">
+ <!--
+ We wont have access to the stage counts until we move to a default value stream
+ For now we can use the `withStageCounts` flag to ensure we don't display empty stage counts
+ Related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/326705
+ -->
+ <div class="card" data-testid="vsa-stage-overview-metrics">
<div class="card-header">{{ __('Recent Project Activity') }}</div>
<div class="d-flex justify-content-between">
<div v-for="item in summary" :key="item.title" class="gl-flex-grow-1 gl-text-center">
@@ -139,40 +157,12 @@ export default {
</div>
</div>
</div>
- <div class="stage-panel-container">
- <div class="card stage-panel">
+ <div class="stage-panel-container" data-testid="vsa-stage-table">
+ <div class="card stage-panel gl-px-5">
<div class="card-header border-bottom-0">
<nav class="col-headers">
- <ul>
- <li class="stage-header pl-5">
- <span class="stage-name font-weight-bold">{{
- s__('ProjectLifecycle|Stage')
- }}</span>
- <span
- class="has-tooltip"
- data-placement="top"
- :title="__('The phase of the development lifecycle.')"
- aria-hidden="true"
- >
- <gl-icon name="question-o" class="gl-text-gray-500" />
- </span>
- </li>
- <li class="median-header">
- <span class="stage-name font-weight-bold">{{ __('Median') }}</span>
- <span
- class="has-tooltip"
- data-placement="top"
- :title="
- __(
- 'The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.',
- )
- "
- aria-hidden="true"
- >
- <gl-icon name="question-o" class="gl-text-gray-500" />
- </span>
- </li>
- <li class="event-header pl-3">
+ <ul class="gl-display-flex gl-justify-content-space-between gl-list-style-none">
+ <li>
<span v-if="selectedStage" class="stage-name font-weight-bold">{{
selectedStage.legend ? __(selectedStage.legend) : __('Related Issues')
}}</span>
@@ -187,7 +177,7 @@ export default {
<gl-icon name="question-o" class="gl-text-gray-500" />
</span>
</li>
- <li class="total-time-header pr-5 text-right">
+ <li>
<span class="stage-name font-weight-bold">{{ __('Time') }}</span>
<span
class="has-tooltip"
@@ -201,45 +191,31 @@ export default {
</ul>
</nav>
</div>
-
<div class="stage-panel-body">
- <nav class="stage-nav">
- <ul>
- <stage-nav-item
- v-for="stage in stages"
- :key="stage.title"
- :title="stage.title"
- :is-user-allowed="stage.isUserAllowed"
- :value="stage.value"
- :is-active="isActiveStage(stage)"
- @select="selectStage(stage)"
- />
- </ul>
- </nav>
- <section class="stage-events overflow-auto">
- <gl-loading-icon v-show="isLoadingStage" size="lg" />
- <template v-if="displayNoAccess">
+ <section class="stage-events gl-overflow-auto gl-w-full">
+ <gl-loading-icon v-if="isLoadingStage" size="lg" />
+ <template v-else>
<gl-empty-state
+ v-if="displayNoAccess"
class="js-empty-state"
:title="__('You need permission.')"
:svg-path="noAccessSvgPath"
:description="__('Want to see the data? Please ask an administrator for access.')"
/>
- </template>
- <template v-else>
- <template v-if="displayNotEnoughData">
+ <template v-else>
<gl-empty-state
+ v-if="displayNotEnoughData"
class="js-empty-state"
:description="selectedStage.emptyStageText"
:svg-path="noDataSvgPath"
:title="__('We don\'t have enough data to show this stage.')"
/>
- </template>
- <template v-if="displayStageEvents">
<component
:is="selectedStage.component"
+ v-if="displayStageEvents"
:stage="selectedStage"
:items="selectedStageEvents"
+ data-testid="stage-table-events"
/>
</template>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
new file mode 100644
index 00000000000..abdc546632f
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
@@ -0,0 +1,117 @@
+<script>
+import {
+ GlPath,
+ GlPopover,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
+import { OVERVIEW_STAGE_ID } from '../constants';
+
+export default {
+ name: 'PathNavigation',
+ components: {
+ GlPath,
+ GlSkeletonLoading,
+ GlPopover,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ stages: {
+ type: Array,
+ required: true,
+ },
+ selectedStage: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ withStageCounts: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ methods: {
+ showPopover({ id }) {
+ return id && id !== OVERVIEW_STAGE_ID;
+ },
+ hasStageCount({ stageCount = null }) {
+ return stageCount !== null;
+ },
+ },
+ popoverOptions: {
+ triggers: 'hover',
+ placement: 'bottom',
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loading v-if="loading" :lines="2" class="h-auto pt-2 pb-1" />
+ <gl-path v-else :key="selectedStage.id" :items="stages" @selected="$emit('selected', $event)">
+ <template #default="{ pathItem, pathId }">
+ <gl-popover
+ v-if="showPopover(pathItem)"
+ v-bind="$options.popoverOptions"
+ :target="pathId"
+ :css-classes="['stage-item-popover']"
+ data-testid="stage-item-popover"
+ >
+ <template #title>{{ pathItem.title }}</template>
+ <div class="gl-px-4">
+ <div class="gl-display-flex gl-justify-content-space-between">
+ <div class="gl-pr-4 gl-pb-4">
+ {{ s__('ValueStreamEvent|Stage time (median)') }}
+ </div>
+ <div class="gl-pb-4 gl-font-weight-bold">{{ pathItem.metric }}</div>
+ </div>
+ </div>
+ <div v-if="withStageCounts" class="gl-px-4">
+ <div class="gl-display-flex gl-justify-content-space-between">
+ <div class="gl-pr-4 gl-pb-4">
+ {{ s__('ValueStreamEvent|Items in stage') }}
+ </div>
+ <div class="gl-pb-4 gl-font-weight-bold">
+ <template v-if="hasStageCount(pathItem)">{{
+ n__('%d item', '%d items', pathItem.stageCount)
+ }}</template>
+ <template v-else>-</template>
+ </div>
+ </div>
+ </div>
+ <div class="gl-px-4 gl-pt-4 gl-border-t-1 gl-border-t-solid gl-border-gray-50">
+ <div
+ v-if="pathItem.startEventHtmlDescription"
+ class="gl-display-flex gl-flex-direction-row"
+ >
+ <div class="gl-display-flex gl-flex-direction-column gl-pr-4 gl-pb-4 metric-label">
+ {{ s__('ValueStreamEvent|Start') }}
+ </div>
+ <div
+ v-safe-html="pathItem.startEventHtmlDescription"
+ class="gl-display-flex gl-flex-direction-column gl-pb-4 stage-event-description"
+ ></div>
+ </div>
+ <div
+ v-if="pathItem.endEventHtmlDescription"
+ class="gl-display-flex gl-flex-direction-row"
+ >
+ <div class="gl-display-flex gl-flex-direction-column gl-pr-4 metric-label">
+ {{ s__('ValueStreamEvent|Stop') }}
+ </div>
+ <div
+ v-safe-html="pathItem.endEventHtmlDescription"
+ class="gl-display-flex gl-flex-direction-column stage-event-description"
+ ></div>
+ </div>
+ </div>
+ </gl-popover>
+ </template>
+ </gl-path>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index d79de207afe..50b5ebba583 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -1 +1,2 @@
export const DEFAULT_DAYS_TO_DISPLAY = 30;
+export const OVERVIEW_STAGE_ID = 'overview';
diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/cycle_analytics/store/getters.js
new file mode 100644
index 00000000000..c60a70ef147
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/store/getters.js
@@ -0,0 +1,10 @@
+import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils';
+
+export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => {
+ return transformStagesForPathNavigation({
+ stages: filterStagesByHiddenStatus(stages, false),
+ medians,
+ stageCounts,
+ selectedStage,
+ });
+};
diff --git a/app/assets/javascripts/cycle_analytics/store/index.js b/app/assets/javascripts/cycle_analytics/store/index.js
index ab47538dcf5..c6ca88ea492 100644
--- a/app/assets/javascripts/cycle_analytics/store/index.js
+++ b/app/assets/javascripts/cycle_analytics/store/index.js
@@ -8,6 +8,7 @@
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';
@@ -16,6 +17,7 @@ Vue.use(Vuex);
export default () =>
new Vuex.Store({
actions,
+ getters,
mutations,
state,
});
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js
index 8fd5c78339a..d5038630503 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutations.js
@@ -1,4 +1,4 @@
-import { decorateData, decorateEvents } from '../utils';
+import { decorateData, decorateEvents, formatMedianValues } from '../utils';
import * as types from './mutation_types';
export default {
@@ -20,9 +20,10 @@ export default {
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
state.isLoading = false;
- const { stages, summary } = decorateData(data);
+ const { stages, summary, medians } = decorateData(data);
state.stages = stages;
state.summary = summary;
+ state.medians = formatMedianValues(medians);
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
index 3afe4b021be..8aa4b88a374 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -1,6 +1,9 @@
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { unescape } from 'lodash';
+import { sanitize } from '~/lib/dompurify';
+import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { parseSeconds } from '~/lib/utils/datetime_utility';
import { dasherize } from '~/lib/utils/text_utility';
-import { __ } from '../locale';
+import { __, s__, sprintf } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = {
@@ -40,10 +43,17 @@ const mapToEvent = (event, stage) => {
export const decorateEvents = (events, stage) => events.map((event) => mapToEvent(event, stage));
-const mapToStage = (permissions, item) => {
- const slug = dasherize(item.name.toLowerCase());
+/*
+ * NOTE: We currently use the `name` field since the project level stages are in memory
+ * once we migrate to a default value stream https://gitlab.com/gitlab-org/gitlab/-/issues/326705
+ * we can use the `id` to identify which median we are using
+ */
+const mapToStage = (permissions, { name, ...rest }) => {
+ const slug = dasherize(name.toLowerCase());
return {
- ...item,
+ ...rest,
+ name,
+ id: name,
slug,
active: false,
isUserAllowed: permissions[slug],
@@ -53,11 +63,98 @@ const mapToStage = (permissions, item) => {
};
const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' });
+const mapToMedians = ({ id, value }) => ({ id, value });
export const decorateData = (data = {}) => {
const { permissions, stats, summary } = data;
+ const stages = stats?.map((item) => mapToStage(permissions, item)) || [];
return {
- stages: stats?.map((item) => mapToStage(permissions, item)) || [],
+ stages,
summary: summary?.map((item) => mapToSummary(item)) || [],
+ medians: stages?.map((item) => mapToMedians(item)) || [],
};
};
+
+/**
+ * Takes the stages and median data, combined with the selected stage, to build an
+ * array which is formatted to proivde the data required for the path navigation.
+ *
+ * @param {Array} stages - The stages available to the group / project
+ * @param {Object} medians - The median values for the stages available to the group / project
+ * @param {Object} stageCounts - The total item count for the stages available
+ * @param {Object} selectedStage - The currently selected stage
+ * @returns {Array} An array of stages formatted with data required for the path navigation
+ */
+export const transformStagesForPathNavigation = ({
+ stages,
+ medians,
+ stageCounts = {},
+ selectedStage,
+}) => {
+ const formattedStages = stages.map((stage) => {
+ return {
+ metric: medians[stage?.id],
+ selected: stage?.id === selectedStage?.id, // Also could null === null cause an issue here?
+ stageCount: stageCounts && stageCounts[stage?.id],
+ icon: null,
+ ...stage,
+ };
+ });
+
+ return formattedStages;
+};
+
+export const timeSummaryForPathNavigation = ({ seconds, hours, days, minutes, weeks, months }) => {
+ if (months) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
+ value: roundToNearestHalf(months),
+ });
+ } else if (weeks) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
+ value: roundToNearestHalf(weeks),
+ });
+ } else if (days) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
+ value: roundToNearestHalf(days),
+ });
+ } else if (hours) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
+ } else if (minutes) {
+ return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
+ } else if (seconds) {
+ return unescape(sanitize(s__('ValueStreamAnalytics|&lt;1m'), { ALLOWED_TAGS: [] }));
+ }
+ return '-';
+};
+
+/**
+ * Takes a raw median value in seconds and converts it to a string representation
+ * ie. converts 172800 => 2d (2 days)
+ *
+ * @param {Number} Median - The number of seconds for the median calculation
+ * @returns {String} String representation ie 2w
+ */
+export const medianTimeToParsedSeconds = (value) =>
+ timeSummaryForPathNavigation({
+ ...parseSeconds(value, { daysPerWeek: 7, hoursPerDay: 24 }),
+ seconds: value,
+ });
+
+/**
+ * Takes the raw median value arrays and converts them into a useful object
+ * containing the string for display in the path navigation
+ * ie. converts [{ id: 'test', value: 172800 }] => { 'test': '2d' }
+ *
+ * @param {Array} Medians - Array of stage median objects, each contains a `id`, `value` and `error`
+ * @returns {Object} Returns key value pair with the stage name and its display median value
+ */
+export const formatMedianValues = (medians = []) =>
+ medians.reduce((acc, { id, value = 0 }) => {
+ return {
+ ...acc,
+ [id]: value ? medianTimeToParsedSeconds(value) : '-',
+ };
+ }, {});
+
+export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
+ stages.filter(({ hidden = false }) => hidden === isHidden);
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 73a30c00606..a666b11ba62 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
@@ -191,7 +191,7 @@ export default {
},
squashIsSelected() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.squashReadOnly ? this.state.squashOnMerge : this.state.squash;
+ return this.isSquashReadOnly ? this.state.squashOnMerge : this.state.squash;
}
return this.mr.squashIsSelected;
diff --git a/app/assets/stylesheets/page_bundles/cycle_analytics.scss b/app/assets/stylesheets/page_bundles/cycle_analytics.scss
index 2742c95c6e1..2248d95ae24 100644
--- a/app/assets/stylesheets/page_bundles/cycle_analytics.scss
+++ b/app/assets/stylesheets/page_bundles/cycle_analytics.scss
@@ -30,32 +30,12 @@
.col-headers {
ul {
- @include clearfix;
margin: 0;
padding: 0;
}
li {
- display: inline-block;
- float: left;
line-height: 50px;
- width: 20%;
- }
-
- .stage-header {
- width: 20.5%;
- }
-
- .median-header {
- width: 19.5%;
- }
-
- .event-header {
- width: 45%;
- }
-
- .total-time-header {
- width: 15%;
}
}
@@ -120,7 +100,6 @@
}
li {
- @include clearfix;
list-style-type: none;
}
@@ -169,7 +148,6 @@
.events-description {
line-height: 65px;
- padding: 0 $gl-padding;
}
.events-info {
@@ -178,7 +156,6 @@
}
.stage-events {
- width: 60%;
min-height: 467px;
}
@@ -190,8 +167,8 @@
.stage-event-item {
@include clearfix;
list-style-type: none;
- padding: 0 0 $gl-padding;
- margin: 0 $gl-padding $gl-padding;
+ padding-bottom: $gl-padding;
+ margin-bottom: $gl-padding;
border-bottom: 1px solid var(--gray-50, $gray-50);
&:last-child {
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 28a87f83451..edf45e7063a 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -79,11 +79,11 @@ class Projects::CompareController < Projects::ApplicationController
private
def validate_refs!
- valid = [head_ref, start_ref].map { |ref| valid_ref?(ref) }
+ invalid = [head_ref, start_ref].filter { |ref| !valid_ref?(ref) }
- return if valid.all?
+ return if invalid.empty?
- flash[:alert] = "Invalid branch name"
+ flash[:alert] = "Invalid branch name(s): #{invalid.join(', ')}"
redirect_to project_compare_index_path(source_project)
end
diff --git a/app/models/project_services/slack_mattermost/notifier.rb b/app/models/concerns/integrations/slack_mattermost_notifier.rb
index ae7ae02b8f0..a919fc840fd 100644
--- a/app/models/project_services/slack_mattermost/notifier.rb
+++ b/app/models/concerns/integrations/slack_mattermost_notifier.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module SlackMattermost
- module Notifier
+module Integrations
+ module SlackMattermostNotifier
private
def notify(message, opts)
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
index 28d12a033a6..3bb23435a2b 100644
--- a/app/models/concerns/issue_available_features.rb
+++ b/app/models/concerns/issue_available_features.rb
@@ -11,7 +11,8 @@ module IssueAvailableFeatures
def available_features_for_issue_types
{
assignee: %w(issue incident),
- confidentiality: %(issue incident)
+ confidentiality: %(issue incident),
+ time_tracking: %(issue incident)
}.with_indifferent_access
end
end
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index 97bb4342105..07a5086b8e9 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -2,7 +2,7 @@
module Integrations
class Mattermost < BaseChatNotification
- include SlackMattermost::Notifier
+ include SlackMattermostNotifier
include ActionView::Helpers::UrlHelper
def title
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index 35b376ce5f2..a83fd3bcbeb 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -2,7 +2,7 @@
module Integrations
class Slack < BaseChatNotification
- include SlackMattermost::Notifier
+ include SlackMattermostNotifier
extend ::Gitlab::Utils::Override
SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5b65d059ee6..b0a126c4442 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -464,6 +464,10 @@ class Issue < ApplicationRecord
issue_type_supports?(:assignee)
end
+ def supports_time_tracking?
+ issue_type_supports?(:time_tracking)
+ end
+
def email_participants_emails
issue_email_participants.pluck(:email)
end
diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb
index eb38b90fb18..8a96eb83a3f 100644
--- a/app/serializers/analytics_stage_entity.rb
+++ b/app/serializers/analytics_stage_entity.rb
@@ -8,9 +8,5 @@ class AnalyticsStageEntity < Grape::Entity
expose :legend
expose :description
- expose :project_median, as: :value do |stage|
- # median returns a BatchLoader instance which we first have to unwrap by using to_f
- # we use to_f to make sure results below 1 are presented to the end-user
- stage.project_median.to_f.nonzero? ? distance_of_time_in_words(stage.project_median) : nil
- end
+ expose :project_median, as: :value
end
diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml
index 92b545cad0a..7a81d53c085 100644
--- a/app/views/admin/application_settings/integrations.html.haml
+++ b/app/views/admin/application_settings/integrations.html.haml
@@ -5,5 +5,5 @@
%h3= s_('Integrations|Project integration management')
- integrations_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: integrations_help_page_path }
-%p= s_('Integrations|As a GitLab administrator, you can set default configuration parameters for a given integration that all projects can inherit and use. When you set these parameters, your changes update the integration for all projects that are not already using custom settings. Learn more about %{integrations_link_start}Project integration management%{link_end}.').html_safe % { integrations_link_start: integrations_link_start, link_end: '</a>'.html_safe }
+%p= s_("Integrations|GitLab administrators can set up integrations that all projects inherit and use by default. These integrations apply to all projects that don't already use custom settings. You can override custom settings for a group or project if the settings are necessary at that level. Learn more about %{integrations_link_start}project integration management%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, link_end: "</a>".html_safe }
= render 'shared/integrations/index', integrations: @integrations
diff --git a/app/views/groups/settings/integrations/index.html.haml b/app/views/groups/settings/integrations/index.html.haml
index 92b545cad0a..7a81d53c085 100644
--- a/app/views/groups/settings/integrations/index.html.haml
+++ b/app/views/groups/settings/integrations/index.html.haml
@@ -5,5 +5,5 @@
%h3= s_('Integrations|Project integration management')
- integrations_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: integrations_help_page_path }
-%p= s_('Integrations|As a GitLab administrator, you can set default configuration parameters for a given integration that all projects can inherit and use. When you set these parameters, your changes update the integration for all projects that are not already using custom settings. Learn more about %{integrations_link_start}Project integration management%{link_end}.').html_safe % { integrations_link_start: integrations_link_start, link_end: '</a>'.html_safe }
+%p= s_("Integrations|GitLab administrators can set up integrations that all projects inherit and use by default. These integrations apply to all projects that don't already use custom settings. You can override custom settings for a group or project if the settings are necessary at that level. Learn more about %{integrations_link_start}project integration management%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, link_end: "</a>".html_safe }
= render 'shared/integrations/index', integrations: @integrations