summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-14 15:09:05 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-14 15:09:05 +0000
commit66bd1f0fdcaf84fa3412c70d7962b49eb8a48fde (patch)
tree23f451b4e60a6e28bcc15043d7756bb27dcc2970 /app
parent49089d4fb1f5c17328ac61c955d95a68c6d4d545 (diff)
downloadgitlab-ce-66bd1f0fdcaf84fa3412c70d7962b49eb8a48fde.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/actions.js30
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/getters.js58
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/index.js16
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/mutations.js24
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/state.js15
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js41
-rw-r--r--app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js28
-rw-r--r--app/finders/events_finder.rb9
-rw-r--r--app/helpers/environments_helper.rb3
-rw-r--r--app/mailers/emails/service_desk.rb92
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/mailers/previews/notify_preview.rb12
-rw-r--r--app/models/event.rb1
-rw-r--r--app/models/event_collection.rb1
-rw-r--r--app/presenters/clusters/cluster_presenter.rb17
-rw-r--r--app/serializers/cluster_entity.rb4
-rw-r--r--app/serializers/cluster_serializer.rb1
-rw-r--r--app/services/event_create_service.rb2
-rw-r--r--app/services/git/wiki_push_service.rb2
-rw-r--r--app/services/wiki_pages/base_service.rb2
-rw-r--r--app/services/wiki_pages/event_create_service.rb2
-rw-r--r--app/views/layouts/service_desk.html.haml24
-rw-r--r--app/views/notify/service_desk_new_note_email.html.haml5
-rw-r--r--app/views/notify/service_desk_new_note_email.text.erb6
-rw-r--r--app/views/notify/service_desk_thank_you_email.html.haml2
-rw-r--r--app/views/notify/service_desk_thank_you_email.text.erb6
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/service_desk_email_receiver_worker.rb15
30 files changed, 415 insertions, 19 deletions
diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/reports/codequality_report/store/actions.js
new file mode 100644
index 00000000000..bf84d27b5ea
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/actions.js
@@ -0,0 +1,30 @@
+import axios from '~/lib/utils/axios_utils';
+import * as types from './mutation_types';
+import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequality_comparison';
+
+export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
+
+export const fetchReports = ({ state, dispatch, commit }) => {
+ commit(types.REQUEST_REPORTS);
+
+ if (!state.basePath) {
+ return dispatch('receiveReportsError');
+ }
+ return Promise.all([axios.get(state.headPath), axios.get(state.basePath)])
+ .then(results =>
+ doCodeClimateComparison(
+ parseCodeclimateMetrics(results[0].data, state.headBlobPath),
+ parseCodeclimateMetrics(results[1].data, state.baseBlobPath),
+ ),
+ )
+ .then(data => dispatch('receiveReportsSuccess', data))
+ .catch(() => dispatch('receiveReportsError'));
+};
+
+export const receiveReportsSuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_REPORTS_SUCCESS, data);
+};
+
+export const receiveReportsError = ({ commit }) => {
+ commit(types.RECEIVE_REPORTS_ERROR);
+};
diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/reports/codequality_report/store/getters.js
new file mode 100644
index 00000000000..5df58c7f85f
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/getters.js
@@ -0,0 +1,58 @@
+import { LOADING, ERROR, SUCCESS } from '../../constants';
+import { sprintf, __, s__, n__ } from '~/locale';
+
+export const hasCodequalityIssues = state =>
+ Boolean(state.newIssues?.length || state.resolvedIssues?.length);
+
+export const codequalityStatus = state => {
+ if (state.isLoading) {
+ return LOADING;
+ }
+ if (state.hasError) {
+ return ERROR;
+ }
+
+ return SUCCESS;
+};
+
+export const codequalityText = state => {
+ const { newIssues, resolvedIssues } = state;
+ const text = [];
+
+ if (!newIssues.length && !resolvedIssues.length) {
+ text.push(s__('ciReport|No changes to code quality'));
+ } else {
+ text.push(s__('ciReport|Code quality'));
+
+ if (resolvedIssues.length) {
+ text.push(n__(' improved on %d point', ' improved on %d points', resolvedIssues.length));
+ }
+
+ if (newIssues.length && resolvedIssues.length) {
+ text.push(__(' and'));
+ }
+
+ if (newIssues.length) {
+ text.push(n__(' degraded on %d point', ' degraded on %d points', newIssues.length));
+ }
+ }
+
+ return text.join('');
+};
+
+export const codequalityPopover = state => {
+ if (state.headPath && !state.basePath) {
+ return {
+ title: s__('ciReport|Base pipeline codequality artifact not found'),
+ content: sprintf(
+ s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'),
+ {
+ linkStartTag: `<a href="${state.helpPath}" target="_blank" rel="noopener noreferrer">`,
+ linkEndTag: '<i class="fa fa-external-link" aria-hidden="true"></i></a>',
+ },
+ false,
+ ),
+ };
+ }
+ return {};
+};
diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/reports/codequality_report/store/index.js
new file mode 100644
index 00000000000..047964260ad
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/index.js
@@ -0,0 +1,16 @@
+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);
+
+export default initialState =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: state(initialState),
+ });
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutation_types.js b/app/assets/javascripts/reports/codequality_report/store/mutation_types.js
new file mode 100644
index 00000000000..c362c973ae1
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/mutation_types.js
@@ -0,0 +1,5 @@
+export const SET_PATHS = 'SET_PATHS';
+
+export const REQUEST_REPORTS = 'REQUEST_REPORTS';
+export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
+export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutations.js b/app/assets/javascripts/reports/codequality_report/store/mutations.js
new file mode 100644
index 00000000000..7ef4f3ce2db
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/mutations.js
@@ -0,0 +1,24 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_PATHS](state, paths) {
+ state.basePath = paths.basePath;
+ state.headPath = paths.headPath;
+ state.baseBlobPath = paths.baseBlobPath;
+ state.headBlobPath = paths.headBlobPath;
+ state.helpPath = paths.helpPath;
+ },
+ [types.REQUEST_REPORTS](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_REPORTS_SUCCESS](state, data) {
+ state.hasError = false;
+ state.isLoading = false;
+ state.newIssues = data.newIssues;
+ state.resolvedIssues = data.resolvedIssues;
+ },
+ [types.RECEIVE_REPORTS_ERROR](state) {
+ state.isLoading = false;
+ state.hasError = true;
+ },
+};
diff --git a/app/assets/javascripts/reports/codequality_report/store/state.js b/app/assets/javascripts/reports/codequality_report/store/state.js
new file mode 100644
index 00000000000..38ab53b432e
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/state.js
@@ -0,0 +1,15 @@
+export default () => ({
+ basePath: null,
+ headPath: null,
+
+ baseBlobPath: null,
+ headBlobPath: null,
+
+ isLoading: false,
+ hasError: false,
+
+ newIssues: [],
+ resolvedIssues: [],
+
+ helpPath: null,
+});
diff --git a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
new file mode 100644
index 00000000000..eba9e340c4e
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
@@ -0,0 +1,41 @@
+import CodeQualityComparisonWorker from '../../workers/codequality_comparison_worker';
+
+export const parseCodeclimateMetrics = (issues = [], path = '') => {
+ return issues.map(issue => {
+ const parsedIssue = {
+ ...issue,
+ name: issue.description,
+ };
+
+ if (issue?.location?.path) {
+ let parseCodeQualityUrl = `${path}/${issue.location.path}`;
+ parsedIssue.path = issue.location.path;
+
+ if (issue?.location?.lines?.begin) {
+ parsedIssue.line = issue.location.lines.begin;
+ parseCodeQualityUrl += `#L${issue.location.lines.begin}`;
+ } else if (issue?.location?.positions?.begin?.line) {
+ parsedIssue.line = issue.location.positions.begin.line;
+ parseCodeQualityUrl += `#L${issue.location.positions.begin.line}`;
+ }
+
+ parsedIssue.urlPath = parseCodeQualityUrl;
+ }
+
+ return parsedIssue;
+ });
+};
+
+export const doCodeClimateComparison = (headIssues, baseIssues) => {
+ // Do these comparisons in worker threads to avoid blocking the main thread
+ return new Promise((resolve, reject) => {
+ const worker = new CodeQualityComparisonWorker();
+ worker.addEventListener('message', ({ data }) =>
+ data.newIssues && data.resolvedIssues ? resolve(data) : reject(data),
+ );
+ worker.postMessage({
+ headIssues,
+ baseIssues,
+ });
+ });
+};
diff --git a/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js b/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js
new file mode 100644
index 00000000000..fc55602f95c
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js
@@ -0,0 +1,28 @@
+import { differenceBy } from 'lodash';
+
+const KEY_TO_FILTER_BY = 'fingerprint';
+
+// eslint-disable-next-line no-restricted-globals
+self.addEventListener('message', e => {
+ const { data } = e;
+
+ if (data === undefined) {
+ return null;
+ }
+
+ const { headIssues, baseIssues } = data;
+
+ if (!headIssues || !baseIssues) {
+ // eslint-disable-next-line no-restricted-globals
+ return self.postMessage({});
+ }
+
+ // eslint-disable-next-line no-restricted-globals
+ self.postMessage({
+ newIssues: differenceBy(headIssues, baseIssues, KEY_TO_FILTER_BY),
+ resolvedIssues: differenceBy(baseIssues, headIssues, KEY_TO_FILTER_BY),
+ });
+
+ // eslint-disable-next-line no-restricted-globals
+ return self.close();
+});
diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb
index 004fbc4cd22..4c619f3d7ea 100644
--- a/app/finders/events_finder.rb
+++ b/app/finders/events_finder.rb
@@ -54,17 +54,10 @@ class EventsFinder
if current_user && scope == 'all'
EventCollection.new(current_user.authorized_projects).all_project_events
else
- # EventCollection is responsible for applying the feature flag
- apply_feature_flags(source.events)
+ source.events
end
end
- def apply_feature_flags(events)
- return events if ::Feature.enabled?(:wiki_events)
-
- events.not_wiki_page
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def by_current_user_access(events)
events.merge(Project.public_or_visible_to_user(current_user))
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 8dbbec8c21d..90ebab731ea 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -36,7 +36,8 @@ module EnvironmentsHelper
"environment-name": environment.name,
"environments-path": project_environments_path(project, format: :json),
"environment-id": environment.id,
- "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack')
+ "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
+ "clusters-path": project_clusters_path(project, format: :json)
}
end
diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb
new file mode 100644
index 00000000000..29fe608472d
--- /dev/null
+++ b/app/mailers/emails/service_desk.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module Emails
+ module ServiceDesk
+ extend ActiveSupport::Concern
+ include MarkupHelper
+
+ included do
+ layout 'service_desk', only: [:service_desk_thank_you_email, :service_desk_new_note_email]
+ end
+
+ def service_desk_thank_you_email(issue_id)
+ setup_service_desk_mail(issue_id)
+
+ email_sender = sender(
+ @support_bot.id,
+ send_from_user_email: false,
+ sender_name: @project.service_desk_setting&.outgoing_name
+ )
+ options = service_desk_options(email_sender, 'thank_you')
+ .merge(subject: "Re: #{subject_base}")
+
+ mail_new_thread(@issue, options)
+ end
+
+ def service_desk_new_note_email(issue_id, note_id)
+ @note = Note.find(note_id)
+ setup_service_desk_mail(issue_id)
+
+ email_sender = sender(@note.author_id)
+ options = service_desk_options(email_sender, 'new_note')
+ .merge(subject: subject_base)
+
+ mail_answer_thread(@issue, options)
+ end
+
+ private
+
+ def setup_service_desk_mail(issue_id)
+ @issue = Issue.find(issue_id)
+ @project = @issue.project
+ @support_bot = User.support_bot
+
+ @sent_notification = SentNotification.record(@issue, @support_bot.id, reply_key)
+ end
+
+ def service_desk_options(email_sender, email_type)
+ {
+ from: email_sender,
+ to: @issue.service_desk_reply_to
+ }.tap do |options|
+ next unless template_body = template_content(email_type)
+
+ options[:body] = template_body
+ options[:content_type] = 'text/html'
+ end
+ end
+
+ def template_content(email_type)
+ template = Gitlab::Template::ServiceDeskTemplate.find(email_type, @project)
+
+ text = substitute_template_replacements(template.content)
+
+ markdown(text, project: @project)
+ rescue Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
+ nil
+ end
+
+ def substitute_template_replacements(template_body)
+ template_body
+ .gsub(/%\{\s*ISSUE_ID\s*\}/, issue_id)
+ .gsub(/%\{\s*ISSUE_PATH\s*\}/, issue_path)
+ .gsub(/%\{\s*NOTE_TEXT\s*\}/, note_text)
+ end
+
+ def issue_id
+ "#{Issue.reference_prefix}#{@issue.iid}"
+ end
+
+ def issue_path
+ @issue.to_reference(full: true)
+ end
+
+ def note_text
+ @note&.note.to_s
+ end
+
+ def subject_base
+ "#{@issue.title} (##{@issue.iid})"
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 2cf72d40635..f9aba3fe4f2 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -19,6 +19,7 @@ class Notify < ApplicationMailer
include Emails::Releases
include Emails::Groups
include Emails::Reviews
+ include Emails::ServiceDesk
helper TimeboxesHelper
helper MergeRequestsHelper
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index cb7c6a36c27..f3a4076e69c 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -165,6 +165,18 @@ class NotifyPreview < ActionMailer::Preview
Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message
end
+ def service_desk_new_note_email
+ cleanup do
+ note = create_note(noteable_type: 'Issue', noteable_id: issue.id, note: 'Issue note content')
+
+ Notify.service_desk_new_note_email(issue.id, note.id).message
+ end
+ end
+
+ def service_desk_thank_you_email
+ Notify.service_desk_thank_you_email(issue.id).message
+ end
+
private
def project
diff --git a/app/models/event.rb b/app/models/event.rb
index 9c0fcbb354b..6cd091ca217 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -84,7 +84,6 @@ class Event < ApplicationRecord
scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
# Needed to implement feature flag: can be removed when feature flag is removed
- scope :not_wiki_page, -> { where('target_type IS NULL or target_type <> ?', 'WikiPage::Meta') }
scope :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') }
scope :with_associations, -> do
diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb
index 6c268bf6c36..ce062abeaaf 100644
--- a/app/models/event_collection.rb
+++ b/app/models/event_collection.rb
@@ -45,7 +45,6 @@ class EventCollection
private
def apply_feature_flags(events)
- events = events.not_wiki_page unless ::Feature.enabled?(:wiki_events)
events = events.not_design unless ::Feature.enabled?(:design_activity_events)
events
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 85a62fefd8f..3dfa9626a79 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -2,6 +2,7 @@
module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated
+ include ::Gitlab::Utils::StrongMemoize
include ActionView::Helpers::SanitizeHelper
include ActionView::Helpers::UrlHelper
include IconsHelper
@@ -60,6 +61,12 @@ module Clusters
end
end
+ def gitlab_managed_apps_logs_path
+ return unless logs_project && can_read_cluster?
+
+ project_logs_path(logs_project, cluster_id: cluster.id)
+ end
+
def read_only_kubernetes_platform_fields?
!cluster.provided_by_user?
end
@@ -85,6 +92,16 @@ module Clusters
ActionController::Base.helpers.image_path(path)
end
+ # currently log explorer is only available in the scope of the project
+ # for group and instance level cluster selected project does not affects
+ # fetching logs from gitlab managed apps namespace, therefore any project
+ # available to user will be sufficient.
+ def logs_project
+ strong_memoize(:logs_project) do
+ cluster.all_projects.first
+ end
+ end
+
def clusterable
if cluster.group_type?
cluster.group
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
index 8a1d41dbd96..a46f2889a96 100644
--- a/app/serializers/cluster_entity.rb
+++ b/app/serializers/cluster_entity.rb
@@ -16,4 +16,8 @@ class ClusterEntity < Grape::Entity
expose :path do |cluster|
Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter
end
+
+ expose :gitlab_managed_apps_logs_path do |cluster|
+ Clusters::ClusterPresenter.new(cluster, current_user: request.current_user).gitlab_managed_apps_logs_path # rubocop: disable CodeReuse/Presenter
+ end
end
diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb
index 27156d3178f..92363a4942c 100644
--- a/app/serializers/cluster_serializer.rb
+++ b/app/serializers/cluster_serializer.rb
@@ -10,6 +10,7 @@ class ClusterSerializer < BaseSerializer
:cluster_type,
:enabled,
:environment_scope,
+ :gitlab_managed_apps_logs_path,
:name,
:nodes,
:path,
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 7f71906bc89..5e184e41885 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -120,8 +120,6 @@ class EventCreateService
#
# @return a tuple of event and either :found or :created
def wiki_event(wiki_page_meta, author, action)
- return unless Feature.enabled?(:wiki_events)
-
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
if duplicate = existing_wiki_event(wiki_page_meta, action)
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index 8bdbc28f3e8..b3937a10a70 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -23,7 +23,7 @@ module Git
end
def can_process_wiki_events?
- Feature.enabled?(:wiki_events) && Feature.enabled?(:wiki_events_on_git_push, project)
+ Feature.enabled?(:wiki_events_on_git_push, project)
end
def push_changes
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
index a0256ea5e69..2967684f7bc 100644
--- a/app/services/wiki_pages/base_service.rb
+++ b/app/services/wiki_pages/base_service.rb
@@ -44,8 +44,6 @@ module WikiPages
end
def create_wiki_event(page)
- return unless ::Feature.enabled?(:wiki_events)
-
response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
log_error(response.message) if response.error?
diff --git a/app/services/wiki_pages/event_create_service.rb b/app/services/wiki_pages/event_create_service.rb
index 18a45d057a9..0453c90d693 100644
--- a/app/services/wiki_pages/event_create_service.rb
+++ b/app/services/wiki_pages/event_create_service.rb
@@ -10,8 +10,6 @@ module WikiPages
end
def execute(slug, page, action)
- return ServiceResponse.success(message: 'No event created as `wiki_events` feature is disabled') unless ::Feature.enabled?(:wiki_events)
-
event = Event.transaction do
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
diff --git a/app/views/layouts/service_desk.html.haml b/app/views/layouts/service_desk.html.haml
new file mode 100644
index 00000000000..26d15a74403
--- /dev/null
+++ b/app/views/layouts/service_desk.html.haml
@@ -0,0 +1,24 @@
+%html{ lang: "en" }
+ %head
+ %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
+ -# haml-lint:disable NoPlainNodes
+ %title
+ GitLab
+ -# haml-lint:enable NoPlainNodes
+ = stylesheet_link_tag 'notify'
+ = yield :head
+ %body
+ .content
+ = yield
+ .footer{ style: "margin-top: 10px;" }
+ %p
+ &mdash;
+ %br
+ = link_to "Unsubscribe", @unsubscribe_url
+
+ -# EE-specific start
+ - if Gitlab::CurrentSettings.email_additional_text.present?
+ %br
+ %br
+ = Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
+ -# EE-specific end
diff --git a/app/views/notify/service_desk_new_note_email.html.haml b/app/views/notify/service_desk_new_note_email.html.haml
new file mode 100644
index 00000000000..7c6be6688d0
--- /dev/null
+++ b/app/views/notify/service_desk_new_note_email.html.haml
@@ -0,0 +1,5 @@
+- if Gitlab::CurrentSettings.email_author_in_body
+ %div
+ #{link_to @note.author_name, user_url(@note.author)} wrote:
+%div
+ = markdown(@note.note, pipeline: :email, author: @note.author)
diff --git a/app/views/notify/service_desk_new_note_email.text.erb b/app/views/notify/service_desk_new_note_email.text.erb
new file mode 100644
index 00000000000..208953a437d
--- /dev/null
+++ b/app/views/notify/service_desk_new_note_email.text.erb
@@ -0,0 +1,6 @@
+New response for issue #<%= @issue.iid %>:
+
+Author: <%= sanitize_name(@note.author_name) %>
+
+<%= @note.note %>
+<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text'%><%# EE-specific end %>
diff --git a/app/views/notify/service_desk_thank_you_email.html.haml b/app/views/notify/service_desk_thank_you_email.html.haml
new file mode 100644
index 00000000000..a3407acd9ba
--- /dev/null
+++ b/app/views/notify/service_desk_thank_you_email.html.haml
@@ -0,0 +1,2 @@
+%p
+ Thank you for your support request! We are tracking your request as ticket ##{@issue.iid}, and will respond as soon as we can.
diff --git a/app/views/notify/service_desk_thank_you_email.text.erb b/app/views/notify/service_desk_thank_you_email.text.erb
new file mode 100644
index 00000000000..8281607a4a8
--- /dev/null
+++ b/app/views/notify/service_desk_thank_you_email.text.erb
@@ -0,0 +1,6 @@
+Thank you for your support request! We are tracking your request as ticket #<%= @issue.iid %>, and will respond as soon as we can.
+
+To unsubscribe from this issue, please paste the following link into your browser:
+
+<%= @unsubscribe_url %>
+<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text' %><%# EE-specific end %>
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index afa344e87f7..03534bf78d1 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -15,7 +15,7 @@
= render_if_exists 'events/epics_filter'
- if comments_visible?
= event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments')
- - if Feature.enabled?(:wiki_events) && (@project.nil? || @project.has_wiki?)
+ - if @project.nil? || @project.has_wiki?
= event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki')
- if event_filter_visible(:designs)
= event_filter_link EventFilter::DESIGNS, _('Designs'), s_('EventFilterBy|Filter by designs')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 62c0bcf0093..4b308f0cb3b 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1660,6 +1660,14 @@
:weight: 2
:idempotent:
:tags: []
+- :name: service_desk_email_receiver
+ :feature_category: :issue_tracking
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: system_hook_push
:feature_category: :source_code_management
:has_external_dependencies:
diff --git a/app/workers/service_desk_email_receiver_worker.rb b/app/workers/service_desk_email_receiver_worker.rb
new file mode 100644
index 00000000000..8649034445c
--- /dev/null
+++ b/app/workers/service_desk_email_receiver_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ServiceDeskEmailReceiverWorker < EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ def perform(raw)
+ return unless ::Gitlab::ServiceDeskEmail.enabled?
+
+ begin
+ Gitlab::Email::ServiceDeskReceiver.new(raw).execute
+ rescue => e
+ handle_failure(raw, e)
+ end
+ end
+end