diff --git a/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5e16f6f3873b7c58584cb690f8542dec52e1c483
--- /dev/null
+++ b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $options.RESET_KEY }}
+
+ {{
+ __(
+ 'Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
+ )
+ }}
+
+
+
+
diff --git a/app/assets/javascripts/alerts_service_settings/index.js b/app/assets/javascripts/alerts_service_settings/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d49725c6a4ddcc04691d0cf1e0d74e23ac4cdc9e
--- /dev/null
+++ b/app/assets/javascripts/alerts_service_settings/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import AlertsServiceForm from './components/alerts_service_form.vue';
+
+export default el => {
+ if (!el) {
+ return null;
+ }
+
+ const { activated: activatedStr, formPath, authorizationKey, url, learnMoreUrl } = el.dataset;
+ const activated = parseBoolean(activatedStr);
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(AlertsServiceForm, {
+ props: {
+ initialActivated: activated,
+ formPath,
+ learnMoreUrl,
+ initialAuthorizationKey: authorizationKey,
+ url,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js
index bdfbcf7126786c47a60d14e1e4b34e77c06c77c7..8711f6e65af13fad03cb2e1f016ba6a540f3e34f 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/editor_lite.js
@@ -1,5 +1,5 @@
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
-import gitlabTheme from '~/ide/lib/themes/gl_theme';
+import whiteTheme from '~/ide/lib/themes/white';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { clearDomElement } from './utils';
@@ -19,8 +19,8 @@ export default class Editor {
}
static setupMonacoTheme() {
- monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
- monacoEditor.setTheme('gitlab');
+ monacoEditor.defineTheme('white', whiteTheme);
+ monacoEditor.setTheme('white');
}
createInstance({ el = undefined, blobPath = '', blobContent = '' } = {}) {
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index c8c3036812e805dd394fcd71e3e00f600b0af7d7..8af34dcb5cc6a30d3de7d8b154f125863cfbe9b8 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -38,6 +38,7 @@ export default {
'panelResizing',
'currentActivityView',
'renderWhitespaceInCode',
+ 'editorTheme',
]),
...mapGetters([
'currentMergeRequest',
@@ -85,6 +86,7 @@ export default {
editorOptions() {
return {
renderWhitespace: this.renderWhitespaceInCode ? 'all' : 'none',
+ theme: this.editorTheme,
};
},
},
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 4c4166e11f53041cd7a91bfe33c22c87970d96f1..a3450522697d01625de8741926d3194bc6c6694b 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -7,6 +7,7 @@ import store from './stores';
import router from './ide_router';
import { parseBoolean } from '../lib/utils/common_utils';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
+import { DEFAULT_THEME } from './lib/themes';
Vue.use(Translate);
@@ -51,6 +52,7 @@ export function initIde(el, options = {}) {
this.setInitialData({
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
renderWhitespaceInCode: parseBoolean(el.dataset.renderWhitespaceInCode),
+ editorTheme: window.gon?.user_color_scheme || DEFAULT_THEME,
});
},
methods: {
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index a0f689065aa2190f41184cb6bc99ad52efa70a22..3d729463cb4c897581f161de774a4b67be4ed04c 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -6,13 +6,14 @@ import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options';
-import gitlabTheme from './themes/gl_theme';
+import { themes } from './themes';
import keymap from './keymap.json';
import { clearDomElement } from '~/editor/utils';
-function setupMonacoTheme() {
- monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
- monacoEditor.setTheme('gitlab');
+function setupThemes() {
+ themes.forEach(theme => {
+ monacoEditor.defineTheme(theme.name, theme.data);
+ });
}
export default class Editor {
@@ -35,7 +36,7 @@ export default class Editor {
...options,
};
- setupMonacoTheme();
+ setupThemes();
this.debouncedUpdate = _.debounce(() => {
this.updateDimensions();
diff --git a/app/assets/javascripts/ide/lib/themes/dark.js b/app/assets/javascripts/ide/lib/themes/dark.js
new file mode 100644
index 0000000000000000000000000000000000000000..96aaa0cbb50d0170f9a4e631d20299fb5f28dac9
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/themes/dark.js
@@ -0,0 +1,268 @@
+/*
+
+https://github.com/brijeshb42/monaco-themes/blob/master/themes/Tomorrow-Night.json
+
+The MIT License (MIT)
+
+Copyright (c) Brijesh Bittu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+export default {
+ base: 'vs-dark',
+ inherit: true,
+ rules: [
+ {
+ foreground: '969896',
+ token: 'comment',
+ },
+ {
+ foreground: 'ced1cf',
+ token: 'keyword.operator.class',
+ },
+ {
+ foreground: 'ced1cf',
+ token: 'constant.other',
+ },
+ {
+ foreground: 'ced1cf',
+ token: 'source.php.embedded.line',
+ },
+ {
+ foreground: 'cc6666',
+ token: 'variable',
+ },
+ {
+ foreground: 'cc6666',
+ token: 'support.other.variable',
+ },
+ {
+ foreground: 'cc6666',
+ token: 'string.other.link',
+ },
+ {
+ foreground: 'cc6666',
+ token: 'string.regexp',
+ },
+ {
+ foreground: 'cc6666',
+ token: 'entity.name.tag',
+ },
+ {
+ foreground: 'cc6666',
+ token: 'entity.other.attribute-name',
+ },
+ {
+ foreground: 'cc6666',
+ token: 'meta.tag',
+ },
+ {
+ foreground: 'cc6666',
+ token: 'declaration.tag',
+ },
+ {
+ foreground: 'cc6666',
+ token: 'markup.deleted.git_gutter',
+ },
+ {
+ foreground: 'de935f',
+ token: 'constant.numeric',
+ },
+ {
+ foreground: 'de935f',
+ token: 'constant.language',
+ },
+ {
+ foreground: 'de935f',
+ token: 'support.constant',
+ },
+ {
+ foreground: 'de935f',
+ token: 'constant.character',
+ },
+ {
+ foreground: 'de935f',
+ token: 'variable.parameter',
+ },
+ {
+ foreground: 'de935f',
+ token: 'punctuation.section.embedded',
+ },
+ {
+ foreground: 'de935f',
+ token: 'keyword.other.unit',
+ },
+ {
+ foreground: 'f0c674',
+ token: 'entity.name.class',
+ },
+ {
+ foreground: 'f0c674',
+ token: 'entity.name.type.class',
+ },
+ {
+ foreground: 'f0c674',
+ token: 'support.type',
+ },
+ {
+ foreground: 'f0c674',
+ token: 'support.class',
+ },
+ {
+ foreground: 'b5bd68',
+ token: 'string',
+ },
+ {
+ foreground: 'b5bd68',
+ token: 'constant.other.symbol',
+ },
+ {
+ foreground: 'b5bd68',
+ token: 'entity.other.inherited-class',
+ },
+ {
+ foreground: 'b5bd68',
+ token: 'markup.heading',
+ },
+ {
+ foreground: 'b5bd68',
+ token: 'markup.inserted.git_gutter',
+ },
+ {
+ foreground: '8abeb7',
+ token: 'keyword.operator',
+ },
+ {
+ foreground: '8abeb7',
+ token: 'constant.other.color',
+ },
+ {
+ foreground: '81a2be',
+ token: 'entity.name.function',
+ },
+ {
+ foreground: '81a2be',
+ token: 'meta.function-call',
+ },
+ {
+ foreground: '81a2be',
+ token: 'support.function',
+ },
+ {
+ foreground: '81a2be',
+ token: 'keyword.other.special-method',
+ },
+ {
+ foreground: '81a2be',
+ token: 'meta.block-level',
+ },
+ {
+ foreground: '81a2be',
+ token: 'markup.changed.git_gutter',
+ },
+ {
+ foreground: 'b294bb',
+ token: 'keyword',
+ },
+ {
+ foreground: 'b294bb',
+ token: 'storage',
+ },
+ {
+ foreground: 'b294bb',
+ token: 'storage.type',
+ },
+ {
+ foreground: 'b294bb',
+ token: 'entity.name.tag.css',
+ },
+ {
+ foreground: 'ced2cf',
+ background: 'df5f5f',
+ token: 'invalid',
+ },
+ {
+ foreground: 'ced2cf',
+ background: '82a3bf',
+ token: 'meta.separator',
+ },
+ {
+ foreground: 'ced2cf',
+ background: 'b798bf',
+ token: 'invalid.deprecated',
+ },
+ {
+ foreground: 'ffffff',
+ token: 'markup.inserted.diff',
+ },
+ {
+ foreground: 'ffffff',
+ token: 'markup.deleted.diff',
+ },
+ {
+ foreground: 'ffffff',
+ token: 'meta.diff.header.to-file',
+ },
+ {
+ foreground: 'ffffff',
+ token: 'meta.diff.header.from-file',
+ },
+ {
+ foreground: '718c00',
+ token: 'markup.inserted.diff',
+ },
+ {
+ foreground: '718c00',
+ token: 'meta.diff.header.to-file',
+ },
+ {
+ foreground: 'c82829',
+ token: 'markup.deleted.diff',
+ },
+ {
+ foreground: 'c82829',
+ token: 'meta.diff.header.from-file',
+ },
+ {
+ foreground: 'ffffff',
+ background: '4271ae',
+ token: 'meta.diff.header.from-file',
+ },
+ {
+ foreground: 'ffffff',
+ background: '4271ae',
+ token: 'meta.diff.header.to-file',
+ },
+ {
+ foreground: '3e999f',
+ fontStyle: 'italic',
+ token: 'meta.diff.range',
+ },
+ ],
+ colors: {
+ 'editor.foreground': '#C5C8C6',
+ 'editor.background': '#1D1F21',
+ 'editor.selectionBackground': '#373B41',
+ 'editor.lineHighlightBackground': '#282A2E',
+ 'editorCursor.foreground': '#AEAFAD',
+ 'editorWhitespace.foreground': '#4B4E55',
+ },
+};
diff --git a/app/assets/javascripts/ide/lib/themes/gl_theme.js b/app/assets/javascripts/ide/lib/themes/gl_theme.js
deleted file mode 100644
index 439ae50448accbc335d9955bcd9aecf3b0e3801a..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/ide/lib/themes/gl_theme.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export default {
- themeName: 'gitlab',
- monacoTheme: {
- base: 'vs',
- inherit: true,
- rules: [],
- colors: {
- 'editorLineNumber.foreground': '#CCCCCC',
- 'diffEditor.insertedTextBackground': '#ddfbe6',
- 'diffEditor.removedTextBackground': '#f9d7dc',
- 'editor.selectionBackground': '#aad6f8',
- 'editorIndentGuide.activeBackground': '#cccccc',
- },
- },
-};
diff --git a/app/assets/javascripts/ide/lib/themes/index.js b/app/assets/javascripts/ide/lib/themes/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..6ed9f6679a4ba114f64c3980a86e085427ac8bff
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/themes/index.js
@@ -0,0 +1,15 @@
+import white from './white';
+import dark from './dark';
+
+export const themes = [
+ {
+ name: 'white',
+ data: white,
+ },
+ {
+ name: 'dark',
+ data: dark,
+ },
+];
+
+export const DEFAULT_THEME = 'white';
diff --git a/app/assets/javascripts/ide/lib/themes/white.js b/app/assets/javascripts/ide/lib/themes/white.js
new file mode 100644
index 0000000000000000000000000000000000000000..273bc783fc6c470cfeaa4585c461a7cbeb71a9be
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/themes/white.js
@@ -0,0 +1,12 @@
+export default {
+ base: 'vs',
+ inherit: true,
+ rules: [],
+ colors: {
+ 'editorLineNumber.foreground': '#CCCCCC',
+ 'diffEditor.insertedTextBackground': '#A0F5B420',
+ 'diffEditor.removedTextBackground': '#f9d7dc20',
+ 'editor.selectionBackground': '#aad6f8',
+ 'editorIndentGuide.activeBackground': '#cccccc',
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6488389977cc6130aa36d6fb63ec2aea530991f5..828cec3e1410b3868394fc36246b7dad87dd2293 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,4 +1,5 @@
import { activityBarViews, viewerTypes } from '../constants';
+import { DEFAULT_THEME } from '../lib/themes';
export default () => ({
currentProjectId: '',
@@ -32,4 +33,5 @@ export default () => ({
},
clientsidePreviewEnabled: false,
renderWhitespaceInCode: false,
+ editorTheme: DEFAULT_THEME,
});
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index ba4b271f09ee9278b16cd69d49b29a2f242b4db3..2d77f2686f77a478a89de268ecbb243180d32d51 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -1,5 +1,6 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
+import initAlertsSettings from '~/alerts_service_settings';
document.addEventListener('DOMContentLoaded', () => {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
@@ -10,4 +11,6 @@ document.addEventListener('DOMContentLoaded', () => {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
}
+
+ initAlertsSettings(document.querySelector('.js-alerts-service-settings'));
});
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 990aca5f0c5f4a11828abdff3a73941fbabb4bbc..9c64714e5dd645f58e1ecff575747c86762da5f0 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -296,8 +296,8 @@ $ide-commit-header-height: 48px;
height: 100%;
min-height: 0; // firefox fix
- &.is-readonly,
- .editor.original {
+ &.is-readonly .vs,
+ .vs .editor.original {
.monaco-editor,
.monaco-editor-background,
.monaco-editor .inputarea.ime-input {
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 3817da8596b4596f23dd192fd4a782de564b87d2..12b4f9ac56c291d201563945c5b90a2aa99af987 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -19,19 +19,36 @@ module Projects
# overridden in EE
def track_events(result)
+ if result[:status] == :success
+ ::Gitlab::Tracking::IncidentManagement.track_from_params(
+ update_params[:incident_management_setting_attributes]
+ )
+ end
end
private
- # overridden in EE
def render_update_response(result)
respond_to do |format|
+ format.html do
+ render_update_html_response(result)
+ end
+
format.json do
render_update_json_response(result)
end
end
end
+ def render_update_html_response(result)
+ if result[:status] == :success
+ flash[:notice] = _('Your changes have been saved')
+ redirect_to project_settings_operations_path(@project)
+ else
+ render 'show'
+ end
+ end
+
def render_update_json_response(result)
if result[:status] == :success
flash[:notice] = _('Your changes have been saved')
@@ -61,6 +78,8 @@ module Projects
# overridden in EE
def permitted_project_params
project_params = {
+ incident_management_setting_attributes: ::Gitlab::Tracking::IncidentManagement.tracking_keys.keys,
+
metrics_setting_attributes: [:external_dashboard_url],
error_tracking_setting_attributes: [
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 1ce76fd57b1d57750d985ad689dbf2de903dabf8..4ed99b229b5593eeebdd9e4d9d2c05d28f086728 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -3,6 +3,11 @@
module ProjectsHelper
prepend_if_ee('::EE::ProjectsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ def project_incident_management_setting
+ @project_incident_management_setting ||= @project.incident_management_setting ||
+ @project.build_incident_management_setting
+ end
+
def link_to_project(project)
link_to namespace_project_path(namespace_id: project.namespace, id: project), title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 88a2531d649a9eafac5da5d1e42b9bb421ba1736..d328a6094395641050adfa9ab00e93bf95a2e1c0 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -27,6 +27,8 @@ module ErrorTracking
validates :api_url, length: { maximum: 255 }, public_url: { enforce_sanitization: true, ascii_only: true }, allow_nil: true
+ validates :enabled, inclusion: { in: [true, false] }
+
validates :api_url, presence: { message: 'is a required field' }, if: :enabled
validate :validate_api_url_path, if: :enabled
diff --git a/app/models/project.rb b/app/models/project.rb
index a215b6c881c18752c812d13b4c19b8b10c724d6f..1e27ce9f34443ca24c20caa75b2d8348cf3e524f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2334,7 +2334,7 @@ class Project < ApplicationRecord
end
def alerts_service_activated?
- false
+ alerts_service&.active?
end
def self_monitoring?
diff --git a/app/models/service.rb b/app/models/service.rb
index 95b7c6927cf2cab7ac79d55dae3b30a066ffa96c..e60dda591765413e103f997139e420417058a70b 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -260,6 +260,7 @@ class Service < ApplicationRecord
def self.available_services_names
service_names = %w[
+ alerts
asana
assembla
bamboo
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index f0144ad1213a2909736104abdf74bb18073c9f8c..27bbf5c6e57620d3044774a2bf89f420efd62eb6 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -16,6 +16,7 @@ module Projects
.merge(metrics_setting_params)
.merge(grafana_integration_params)
.merge(prometheus_integration_params)
+ .merge(incident_management_setting_params)
end
def metrics_setting_params
@@ -87,6 +88,10 @@ module Projects
{ prometheus_service_attributes: service.attributes.except(*%w(id project_id created_at updated_at)) }
end
+
+ def incident_management_setting_params
+ params.slice(:incident_management_setting_attributes)
+ end
end
end
end
diff --git a/app/views/projects/services/alerts/_help.html.haml b/app/views/projects/services/alerts/_help.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..be91020312558b1a14101d17ed9e364ff13dfa35
--- /dev/null
+++ b/app/views/projects/services/alerts/_help.html.haml
@@ -0,0 +1,3 @@
+.js-alerts-service-settings{ data: { activated: @service.activated?.to_s,
+ form_path: project_service_path(@project, @service.to_param),
+ authorization_key: @service.token, url: @service.url, learn_more_url: 'https://docs.gitlab.com/ee/user/project/integrations/generic_alerts.html' } }
diff --git a/app/views/projects/settings/operations/_incidents.html.haml b/app/views/projects/settings/operations/_incidents.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..fa2f3d7dc08988fbec4234834181fd2e6b3b1858
--- /dev/null
+++ b/app/views/projects/settings/operations/_incidents.html.haml
@@ -0,0 +1,32 @@
+- templates = []
+- setting = project_incident_management_setting
+- templates = setting.available_issue_templates.map { |t| [t.name, t.key] }
+
+%section.settings.no-animate.js-incident-management-settings
+ .settings-header
+ %h4= _('Incidents')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = _('Expand')
+ %p
+ = _('Action to take when receiving an alert.')
+ = link_to help_page_path('user/project/integrations/prometheus', anchor: 'taking-action-on-an-alert-ultimate') do
+ = _('More information')
+ .settings-content
+ = form_for @project, url: project_settings_operations_path(@project), method: :patch do |f|
+ = form_errors(@project.incident_management_setting)
+ .form-group
+ = f.fields_for :incident_management_setting_attributes, setting do |form|
+ .form-group
+ = form.check_box :create_issue
+ = form.label :create_issue, _('Create an issue. Issues are created for each alert triggered.'), class: 'form-check-label'
+ .form-group.col-sm-8
+ = form.label :issue_template_key, class: 'label-bold' do
+ = _('Issue template (optional)')
+ = link_to icon('question-circle'), help_page_path('user/project/description_templates', anchor: 'creating-issue-templates'), target: '_blank', rel: 'noopener noreferrer'
+ .select-wrapper
+ = form.select :issue_template_key, templates, {include_blank: 'No template selected'}, class: "form-control select-control"
+ = icon('chevron-down')
+ .form-group
+ = form.check_box :send_email
+ = form.label :send_email, _('Send a separate email notification to Developers.'), class: 'form-check-label'
+ = f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 3c955e5f5580bb865471acb1a13c92801ceb1c6e..30b914b5199f1e7bcd5bba15a8d149f24e0219d8 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -2,7 +2,7 @@
- page_title _('Operations Settings')
- breadcrumb_title _('Operations Settings')
-= render_if_exists 'projects/settings/operations/incidents'
+= render 'projects/settings/operations/incidents'
= render 'projects/settings/operations/error_tracking'
= render 'projects/settings/operations/external_dashboard'
= render 'projects/settings/operations/grafana_integration'
diff --git a/changelogs/unreleased/195701-web-ide-dark-theme.yml b/changelogs/unreleased/195701-web-ide-dark-theme.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2444b43487b4f68ec382440cf7c7e5beacbee12c
--- /dev/null
+++ b/changelogs/unreleased/195701-web-ide-dark-theme.yml
@@ -0,0 +1,5 @@
+---
+title: Dark syntax highlighting theme for Web IDE
+merge_request: 24158
+author:
+type: added
diff --git a/changelogs/unreleased/200011-move-error-tracking-incidents-to-core.yml b/changelogs/unreleased/200011-move-error-tracking-incidents-to-core.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f0b383e3a03ac89272804151b2900046a7a141c3
--- /dev/null
+++ b/changelogs/unreleased/200011-move-error-tracking-incidents-to-core.yml
@@ -0,0 +1,5 @@
+---
+title: Move Settings->Operations->Incidents to the Core
+merge_request: 24600
+author:
+type: changed
diff --git a/changelogs/unreleased/42640-move-generic-alerts-endpoint-to-core.yml b/changelogs/unreleased/42640-move-generic-alerts-endpoint-to-core.yml
new file mode 100644
index 0000000000000000000000000000000000000000..995c2ce982f5a26019d8050ef066ac8cc3871592
--- /dev/null
+++ b/changelogs/unreleased/42640-move-generic-alerts-endpoint-to-core.yml
@@ -0,0 +1,5 @@
+---
+title: Makes the generic alerts endpoint available with the free tier
+merge_request: 23339
+author:
+type: changed
diff --git a/changelogs/unreleased/error-tracking-api-followup.yml b/changelogs/unreleased/error-tracking-api-followup.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9767519275bb0f7df923d3ec4d689e68bfb7998e
--- /dev/null
+++ b/changelogs/unreleased/error-tracking-api-followup.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor error tracking specs and add validation to enabled field in error tracking model
+merge_request: 24892
+author: Rajendra Kadam
+type: added
diff --git a/doc/user/project/integrations/generic_alerts.md b/doc/user/project/integrations/generic_alerts.md
index bb07b97e456859b788c178c3d69ac94bf7aaa5e9..f5d0f5eb21bd2623ef00d868bab6eae913cc4566 100644
--- a/doc/user/project/integrations/generic_alerts.md
+++ b/doc/user/project/integrations/generic_alerts.md
@@ -1,6 +1,7 @@
-# Generic alerts integration **(ULTIMATE)**
+# Generic alerts integration
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13203) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.4.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13203) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.4.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/issues/42640) to [GitLab Core](https://about.gitlab.com/pricing/) in 12.8.
GitLab can accept alerts from any source via a generic webhook receiver.
When you set up the generic alerts integration, a unique endpoint will
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index 096c465c8739024e1a6cd9f93b54a1d6c382e01d..4c44aca2de4dd047687a1679a5c4535dc22ef6ee 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -161,6 +161,7 @@ module API
def self.services
{
+ 'alerts' => [],
'asana' => [
{
required: true,
@@ -729,6 +730,7 @@ module API
def self.service_classes
[
+ ::AlertsService,
::AsanaService,
::AssemblaService,
::BambooService,
diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb
index 1239b103cdecd14441e60556298523d6d6188d59..1d9488831510f97e6fccd276911af56af6dde33d 100644
--- a/lib/gitlab/metrics/dashboard/url.rb
+++ b/lib/gitlab/metrics/dashboard/url.rb
@@ -8,6 +8,10 @@ module Gitlab
class << self
include Gitlab::Utils::StrongMemoize
+ QUERY_PATTERN = '(?\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?'
+ ANCHOR_PATTERN = '(?\#[a-z0-9_-]+)?'
+ OPTIONAL_DASH_PATTERN = '(?:/-)?'
+
# Matches urls for a metrics dashboard. This could be
# either the /metrics endpoint or the /metrics_dashboard
# endpoint.
@@ -63,10 +67,10 @@ module Gitlab
(?
#{gitlab_host_pattern}
#{project_path_pattern}
- (?:/-)?
+ #{OPTIONAL_DASH_PATTERN}
#{path_suffix_pattern}
- #{query_pattern}
- #{anchor_pattern}
+ #{QUERY_PATTERN}
+ #{ANCHOR_PATTERN}
)
}x
end
@@ -78,14 +82,6 @@ module Gitlab
def project_path_pattern
"\/#{Project.reference_pattern}"
end
-
- def query_pattern
- '(?\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?'
- end
-
- def anchor_pattern
- '(?\#[a-z0-9_-]+)?'
- end
end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index f10eb82e03e1b3eecae7403593d4c2418996a67a..23c3259b828f8670375fb36444e09d7b6e643223 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -87,6 +87,7 @@ module Gitlab
issues_with_associated_zoom_link: count(ZoomMeeting.added_to_issue),
issues_using_zoom_quick_actions: count(ZoomMeeting.select(:issue_id).distinct),
issues_with_embedded_grafana_charts_approx: ::Gitlab::GrafanaEmbedUsageData.issue_count,
+ incident_issues: count(::Issue.authored(::User.alert_bot)),
keys: count(Key),
label_lists: count(List.label),
lfs_objects: count(LfsObject),
@@ -98,6 +99,7 @@ module Gitlab
projects_imported_from_github: count(Project.where(import_type: 'github')),
projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
+ projects_with_alerts_service_enabled: count(AlertsService.active),
protected_branches: count(ProtectedBranch),
releases: count(Release),
remote_mirrors: count(RemoteMirror),
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index 0a2cc6f6aa5d834b1e67d9315150aa22c0cfb53a..62b906e85078480aeb54fc8a6c3f61f91106cb7e 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe Projects::Settings::OperationsController do
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
+ let_it_be(:project, reload: true) { create(:project) }
before do
sign_in(user)
@@ -121,6 +121,74 @@ describe Projects::Settings::OperationsController do
end
end
+ context 'incident management' do
+ describe 'GET #show' do
+ context 'with existing setting' do
+ let!(:incident_management_setting) do
+ create(:project_incident_management_setting, project: project)
+ end
+
+ it 'loads existing setting' do
+ get :show, params: project_params(project)
+
+ expect(controller.helpers.project_incident_management_setting)
+ .to eq(incident_management_setting)
+ end
+ end
+
+ context 'without an existing setting' do
+ it 'builds a new setting' do
+ get :show, params: project_params(project)
+
+ expect(controller.helpers.project_incident_management_setting).to be_new_record
+ end
+ end
+ end
+
+ describe 'PATCH #update' do
+ let(:params) do
+ {
+ incident_management_setting_attributes: {
+ create_issue: 'false',
+ send_email: 'false',
+ issue_template_key: 'some-other-template'
+ }
+ }
+ end
+
+ it_behaves_like 'PATCHable'
+
+ context 'updating each incident management setting' do
+ let(:project) { create(:project) }
+ let(:new_incident_management_settings) { {} }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ shared_examples 'a gitlab tracking event' do |params, event_key|
+ it "creates a gitlab tracking event #{event_key}" do
+ new_incident_management_settings = params
+
+ expect(Gitlab::Tracking).to receive(:event)
+ .with('IncidentManagement::Settings', event_key, kind_of(Hash))
+
+ patch :update, params: project_params(project, incident_management_setting_attributes: new_incident_management_settings)
+
+ project.reload
+ end
+ end
+
+ it_behaves_like 'a gitlab tracking event', { create_issue: '1' }, 'enabled_issue_auto_creation_on_alerts'
+ it_behaves_like 'a gitlab tracking event', { create_issue: '0' }, 'disabled_issue_auto_creation_on_alerts'
+ it_behaves_like 'a gitlab tracking event', { issue_template_key: 'template' }, 'enabled_issue_template_on_alerts'
+ it_behaves_like 'a gitlab tracking event', { issue_template_key: nil }, 'disabled_issue_template_on_alerts'
+ it_behaves_like 'a gitlab tracking event', { send_email: '1' }, 'enabled_sending_emails'
+ it_behaves_like 'a gitlab tracking event', { send_email: '0' }, 'disabled_sending_emails'
+ end
+ end
+ end
+
context 'error tracking' do
describe 'GET #show' do
context 'with existing setting' do
diff --git a/spec/features/projects/services/user_activates_alerts_spec.rb b/spec/features/projects/services/user_activates_alerts_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..47de7fab85977bf32b6c2f9b673237bd970d813a
--- /dev/null
+++ b/spec/features/projects/services/user_activates_alerts_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User activates Alerts', :js do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:service_name) { 'alerts' }
+ let(:service_title) { 'Alerts endpoint' }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+ end
+
+ context 'when service is deactivated' do
+ it 'activates service' do
+ visit_project_services
+
+ expect(page).to have_link(service_title)
+ click_link(service_title)
+
+ expect(page).not_to have_active_service
+
+ click_activate_service
+ wait_for_requests
+
+ expect(page).to have_active_service
+ end
+ end
+
+ context 'when service is activated' do
+ before do
+ visit_alerts_service
+ click_activate_service
+ end
+
+ it 're-generates key' do
+ expect(reset_key.value).to be_blank
+
+ click_reset_key
+ click_confirm_reset_key
+ wait_for_requests
+
+ expect(reset_key.value).to be_present
+ end
+ end
+
+ private
+
+ def visit_project_services
+ visit(project_settings_integrations_path(project))
+ end
+
+ def visit_alerts_service
+ visit(edit_project_service_path(project, service_name))
+ end
+
+ def click_activate_service
+ find('#activated').click
+ end
+
+ def click_reset_key
+ click_button('Reset key')
+ end
+
+ def click_confirm_reset_key
+ within '.modal-content' do
+ click_reset_key
+ end
+ end
+
+ def reset_key
+ find_field('Authorization key')
+ end
+
+ def have_active_service
+ have_selector('.js-service-active-status[data-value="true"]')
+ end
+end
diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb
index 72e2865dd6ae6bcd0039c1222c53292b18b9090c..0181ceed5b9185a6fdb38c7d8f3574eeabac13eb 100644
--- a/spec/features/projects/settings/operations_settings_spec.rb
+++ b/spec/features/projects/settings/operations_settings_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe 'Projects > Settings > For a forked project', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository, create_templates: :issue) }
let(:role) { :maintainer }
before do
@@ -22,6 +22,54 @@ describe 'Projects > Settings > For a forked project', :js do
end
describe 'Settings > Operations' do
+ describe 'Incidents' do
+ let(:create_issue) { 'Create an issue. Issues are created for each alert triggered.' }
+ let(:send_email) { 'Send a separate email notification to Developers.' }
+
+ before do
+ create(:project_incident_management_setting, send_email: true, project: project)
+ visit project_settings_operations_path(project)
+
+ wait_for_requests
+ click_expand_incident_management_button
+ end
+
+ it 'renders form for incident management' do
+ expect(page).to have_selector('h4', text: 'Incidents')
+ end
+
+ it 'sets correct default values' do
+ expect(find_field(create_issue)).not_to be_checked
+ expect(find_field(send_email)).to be_checked
+ end
+
+ it 'updates form values' do
+ check(create_issue)
+ template_select = find_field('Issue template')
+ template_select.find(:xpath, 'option[2]').select_option
+ uncheck(send_email)
+
+ save_form
+ click_expand_incident_management_button
+
+ expect(find_field(create_issue)).to be_checked
+ expect(page).to have_select('Issue template', selected: 'bug')
+ expect(find_field(send_email)).not_to be_checked
+ end
+
+ def click_expand_incident_management_button
+ within '.js-incident-management-settings' do
+ click_button('Expand')
+ end
+ end
+
+ def save_form
+ page.within "#edit_project_#{project.id}" do
+ click_on 'Save changes'
+ end
+ end
+ end
+
context 'error tracking settings form' do
let(:sentry_list_projects_url) { 'http://sentry.example.com/api/0/projects/' }
diff --git a/spec/frontend/alerts_service_settings/components/__snapshots__/alerts_service_form_spec.js.snap b/spec/frontend/alerts_service_settings/components/__snapshots__/alerts_service_form_spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..36ec0badadeee4f6d8537621fffdd6dffba89af6
--- /dev/null
+++ b/spec/frontend/alerts_service_settings/components/__snapshots__/alerts_service_form_spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AlertsServiceForm with default values renders "authorization-key" input 1`] = `""`;
+
+exports[`AlertsServiceForm with default values renders "url" input 1`] = `""`;
+
+exports[`AlertsServiceForm with default values renders toggle button 1`] = `""`;
+
+exports[`AlertsServiceForm with default values shows description and "Learn More" link 1`] = `"Each alert source must be authorized using the following URL and authorization key. Learn more about configuring this endpoint to receive alerts."`;
diff --git a/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js b/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b7a008c78d0201b2af355d635d3c817a230b299b
--- /dev/null
+++ b/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js
@@ -0,0 +1,168 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import AlertsServiceForm from '~/alerts_service_settings/components/alerts_service_form.vue';
+import ToggleButton from '~/vue_shared/components/toggle_button.vue';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
+
+const defaultProps = {
+ initialAuthorizationKey: 'abcedfg123',
+ formPath: 'http://invalid',
+ url: 'https://gitlab.com/endpoint-url',
+ learnMoreUrl: 'https://docs.gitlab.com/ee/user/project/integrations/generic_alerts.md',
+ initialActivated: false,
+};
+
+describe('AlertsServiceForm', () => {
+ let wrapper;
+ let mockAxios;
+
+ const createComponent = (props = defaultProps, { methods } = {}) => {
+ wrapper = shallowMount(AlertsServiceForm, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ methods,
+ });
+ };
+
+ const findUrl = () => wrapper.find('#url');
+ const findAuthorizationKey = () => wrapper.find('#authorization-key');
+ const findDescription = () => wrapper.find('p');
+ const findActiveStatusIcon = val =>
+ document.querySelector(`.js-service-active-status[data-value=${val.toString()}]`);
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ setFixtures(`
+
+
+
+
`);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mockAxios.restore();
+ });
+
+ describe('with default values', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders "url" input', () => {
+ expect(findUrl().html()).toMatchSnapshot();
+ });
+
+ it('renders "authorization-key" input', () => {
+ expect(findAuthorizationKey().html()).toMatchSnapshot();
+ });
+
+ it('renders toggle button', () => {
+ expect(wrapper.find(ToggleButton).html()).toMatchSnapshot();
+ });
+
+ it('shows description and "Learn More" link', () => {
+ expect(findDescription().element.innerHTML).toMatchSnapshot();
+ });
+ });
+
+ describe('reset key', () => {
+ it('triggers resetKey method', () => {
+ const resetKey = jest.fn();
+ const methods = { resetKey };
+ createComponent(defaultProps, { methods });
+
+ wrapper.find(GlModal).vm.$emit('ok');
+
+ expect(resetKey).toHaveBeenCalled();
+ });
+
+ it('updates the authorization key on success', () => {
+ const formPath = 'some/path';
+ mockAxios.onPut(formPath, { service: { token: '' } }).replyOnce(200, { token: 'newToken' });
+
+ createComponent({ formPath });
+
+ return wrapper.vm.resetKey().then(() => {
+ expect(findAuthorizationKey().attributes('value')).toBe('newToken');
+ });
+ });
+
+ it('shows flash message on error', () => {
+ const formPath = 'some/path';
+ mockAxios.onPut(formPath).replyOnce(404);
+
+ createComponent({ formPath });
+
+ return wrapper.vm.resetKey().then(() => {
+ expect(findAuthorizationKey().attributes('value')).toBe(
+ defaultProps.initialAuthorizationKey,
+ );
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('activate toggle', () => {
+ it('triggers toggleActivated method', () => {
+ const toggleActivated = jest.fn();
+ const methods = { toggleActivated };
+ createComponent(defaultProps, { methods });
+
+ wrapper.find(ToggleButton).vm.$emit('change', true);
+
+ expect(toggleActivated).toHaveBeenCalled();
+ });
+
+ describe('successfully completes', () => {
+ describe.each`
+ initialActivated | value
+ ${false} | ${true}
+ ${true} | ${false}
+ `(
+ 'when initialActivated=$initialActivated and value=$value',
+ ({ initialActivated, value }) => {
+ beforeEach(() => {
+ const formPath = 'some/path';
+ mockAxios
+ .onPut(formPath, { service: { active: value } })
+ .replyOnce(200, { active: value });
+ createComponent({ initialActivated, formPath });
+
+ return wrapper.vm.toggleActivated(value);
+ });
+
+ it(`updates toggle button value to ${value}`, () => {
+ expect(wrapper.find(ToggleButton).props('value')).toBe(value);
+ });
+
+ it('updates visible status icons', () => {
+ expect(findActiveStatusIcon(!value)).toHaveClass('d-none');
+ expect(findActiveStatusIcon(value)).not.toHaveClass('d-none');
+ });
+ },
+ );
+ });
+
+ describe('error is encountered', () => {
+ beforeEach(() => {
+ const formPath = 'some/path';
+ mockAxios.onPut(formPath).replyOnce(500);
+ });
+
+ it('restores previous value', () => {
+ createComponent({ initialActivated: false });
+
+ return wrapper.vm.toggleActivated(true).then(() => {
+ expect(wrapper.find(ToggleButton).props('value')).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index d61289dabb6885896ab60ae552383f7873165640..37bc2b382cba82108f9f5530541d2b594db2ecf2 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -5,6 +5,37 @@ require 'spec_helper'
describe ProjectsHelper do
include ProjectForksHelper
+ describe '#project_incident_management_setting' do
+ let(:project) { create(:project) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ context 'when incident_management_setting exists' do
+ let(:project_incident_management_setting) do
+ create(:project_incident_management_setting, project: project)
+ end
+
+ it 'return project_incident_management_setting' do
+ expect(helper.project_incident_management_setting).to(
+ eq(project_incident_management_setting)
+ )
+ end
+ end
+
+ context 'when incident_management_setting does not exist' do
+ it 'builds incident_management_setting' do
+ setting = helper.project_incident_management_setting
+
+ expect(setting).not_to be_persisted
+ expect(setting.send_email).to be_falsey
+ expect(setting.create_issue).to be_truthy
+ expect(setting.issue_template_key).to be_nil
+ end
+ end
+ end
+
describe '#error_tracking_setting_project_json' do
let(:project) { create(:project) }
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index f1973f7798f768dde08c5b5fff72c56c1bac6275..556bd45d3a54c201d417e1c3b79dfd5381da4e64 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -74,6 +74,7 @@ describe('Multi-file editor library', () => {
renderSideBySide: true,
renderLineHighlight: 'all',
hideCursorInOverviewRuler: false,
+ theme: 'vs white',
});
});
});
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 9a49d334f524b3e4b74b671b727e9b30d6d92ef3..3f7e412e80bee0272f9bff56cd1dd8709922bfdf 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -22,6 +22,10 @@ describe Gitlab::UsageData do
create(:service, project: projects[2], type: 'CustomIssueTrackerService', active: true)
create(:project_error_tracking_setting, project: projects[0])
create(:project_error_tracking_setting, project: projects[1], enabled: false)
+ create(:alerts_service, project: projects[0])
+ create(:alerts_service, :inactive, project: projects[1])
+ create_list(:issue, 2, project: projects[0], author: User.alert_bot)
+ create_list(:issue, 2, project: projects[1], author: User.alert_bot)
create_list(:issue, 4, project: projects[0])
create(:zoom_meeting, project: projects[0], issue: projects[0].issues[0], issue_status: :added)
create_list(:zoom_meeting, 2, project: projects[0], issue: projects[0].issues[1], issue_status: :removed)
@@ -159,6 +163,7 @@ describe Gitlab::UsageData do
issues_with_associated_zoom_link
issues_using_zoom_quick_actions
issues_with_embedded_grafana_charts_approx
+ incident_issues
keys
label_lists
labels
@@ -183,6 +188,7 @@ describe Gitlab::UsageData do
projects_prometheus_active
projects_with_repositories_enabled
projects_with_error_tracking_enabled
+ projects_with_alerts_service_enabled
pages_domains
protected_branches
releases
@@ -220,10 +226,12 @@ describe Gitlab::UsageData do
expect(count_data[:projects_mattermost_active]).to eq(0)
expect(count_data[:projects_with_repositories_enabled]).to eq(3)
expect(count_data[:projects_with_error_tracking_enabled]).to eq(1)
+ expect(count_data[:projects_with_alerts_service_enabled]).to eq(1)
expect(count_data[:issues_created_from_gitlab_error_tracking_ui]).to eq(1)
expect(count_data[:issues_with_associated_zoom_link]).to eq(2)
expect(count_data[:issues_using_zoom_quick_actions]).to eq(3)
expect(count_data[:issues_with_embedded_grafana_charts_approx]).to eq(2)
+ expect(count_data[:incident_issues]).to eq(4)
expect(count_data[:clusters_enabled]).to eq(4)
expect(count_data[:project_clusters_enabled]).to eq(3)
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index 41630c71f21f8b99240fa8b73302321572dd83cc..e81480ab88fea10a19cab159abe62b409d21bf0b 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -19,6 +19,19 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
it { is_expected.to allow_value("http://gitlab.com/api/0/projects/project1/something").for(:api_url) }
it { is_expected.not_to allow_values("http://gitlab.com/api/0/projects/project1/something€").for(:api_url) }
+ it 'disallows non-booleans in enabled column' do
+ is_expected.not_to allow_value(
+ nil
+ ).for(:enabled)
+ end
+
+ it 'allows booleans in enabled column' do
+ is_expected.to allow_value(
+ true,
+ false
+ ).for(:enabled)
+ end
+
it 'rejects invalid api_urls' do
is_expected.not_to allow_values(
"https://replaceme.com/'>", # unsafe
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index b3d8ac83075307a8d0aed8f590744ba4e26ac454..dc055244af7bda2dd08fbb00503dbf51f063565d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -5607,7 +5607,21 @@ describe Project do
subject { project.alerts_service_activated? }
- it { is_expected.to be_falsey }
+ context 'when project has an activated alerts service' do
+ before do
+ create(:alerts_service, project: project)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when project has an inactive alerts service' do
+ before do
+ create(:alerts_service, :inactive, project: project)
+ end
+
+ it { is_expected.to be_falsey }
+ end
end
describe '#self_monitoring?' do
diff --git a/spec/requests/api/error_tracking_spec.rb b/spec/requests/api/error_tracking_spec.rb
index 059744898b84159e64e5b7dada8bca38a8081548..120248bdbc657fabca9a8ecce2a662d459273412 100644
--- a/spec/requests/api/error_tracking_spec.rb
+++ b/spec/requests/api/error_tracking_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
describe API::ErrorTracking do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
let(:setting) { create(:project_error_tracking_setting) }
let(:project) { setting.project }
shared_examples 'returns project settings' do
it 'returns correct project settings' do
- subject
+ make_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq(
@@ -23,7 +23,7 @@ describe API::ErrorTracking do
shared_examples 'returns 404' do
it 'returns correct project settings' do
- subject
+ make_request
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message'])
@@ -32,7 +32,9 @@ describe API::ErrorTracking do
end
describe "PATCH /projects/:id/error_tracking/settings" do
- def make_patch_request(**params)
+ let(:params) { { active: false } }
+
+ def make_request
patch api("/projects/#{project.id}/error_tracking/settings", user), params: params
end
@@ -42,26 +44,39 @@ describe API::ErrorTracking do
end
context 'patch settings' do
- subject do
- make_patch_request(active: false)
+ it_behaves_like 'returns project settings'
+
+ it 'updates enabled flag' do
+ expect(setting).to be_enabled
+
+ make_request
+
+ expect(json_response).to include('active' => false)
+ expect(setting.reload).not_to be_enabled
end
- it_behaves_like 'returns project settings'
+ context 'active is invalid' do
+ let(:params) { { active: "randomstring" } }
- it 'returns active is invalid if non boolean' do
- make_patch_request(active: "randomstring")
+ it 'returns active is invalid if non boolean' do
+ make_request
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error'])
- .to eq('active is invalid')
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error'])
+ .to eq('active is invalid')
+ end
end
- it 'returns 400 if active is empty' do
- make_patch_request(active: '')
+ context 'active is empty' do
+ let(:params) { { active: '' } }
+
+ it 'returns 400' do
+ make_request
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error'])
- .to eq('active is empty')
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error'])
+ .to eq('active is empty')
+ end
end
end
@@ -73,10 +88,6 @@ describe API::ErrorTracking do
end
context 'patch settings' do
- subject do
- make_patch_request(active: true)
- end
-
it_behaves_like 'returns 404'
end
end
@@ -87,10 +98,12 @@ describe API::ErrorTracking do
project.add_reporter(user)
end
- it 'returns 403 for update request' do
- make_patch_request(active: true)
+ context 'patch request' do
+ it 'returns 403' do
+ make_request
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
end
end
@@ -99,28 +112,34 @@ describe API::ErrorTracking do
project.add_developer(user)
end
- it 'returns 403 for update request' do
- make_patch_request(active: true)
+ context 'patch request' do
+ it 'returns 403' do
+ make_request
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
end
end
context 'when authenticated as non-member' do
- it 'returns 404 for update request' do
- make_patch_request(active: false)
+ context 'patch request' do
+ it 'returns 404' do
+ make_request
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
context 'when unauthenticated' do
let(:user) { nil }
- it 'returns 401 for update request' do
- make_patch_request(active: true)
+ context 'patch request' do
+ it 'returns 401 for update request' do
+ make_request
- expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
end
end
end
@@ -136,10 +155,6 @@ describe API::ErrorTracking do
end
context 'get settings' do
- subject do
- make_request
- end
-
it_behaves_like 'returns project settings'
end
end
@@ -152,10 +167,6 @@ describe API::ErrorTracking do
end
context 'get settings' do
- subject do
- make_request
- end
-
it_behaves_like 'returns 404'
end
end
diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb
index efd168a0a8a5f63daddff93cf0373028be86a7cf..925d323584e6043d7d1c44f6071a85c3139dba51 100644
--- a/spec/services/projects/alerting/notify_service_spec.rb
+++ b/spec/services/projects/alerting/notify_service_spec.rb
@@ -5,6 +5,26 @@ require 'spec_helper'
describe Projects::Alerting::NotifyService do
let_it_be(:project, reload: true) { create(:project) }
+ before do
+ # We use `let_it_be(:project)` so we make sure to clear caches
+ project.clear_memoization(:licensed_feature_available)
+ end
+
+ shared_examples 'processes incident issues' do |amount|
+ let(:create_incident_service) { spy }
+
+ it 'processes issues' do
+ expect(IncidentManagement::ProcessAlertWorker)
+ .to receive(:perform_async)
+ .with(project.id, kind_of(Hash))
+ .exactly(amount).times
+
+ Sidekiq::Testing.inline! do
+ expect(subject.status).to eq(:success)
+ end
+ end
+ end
+
shared_examples 'does not process incident issues' do |http_status:|
it 'does not process issues' do
expect(IncidentManagement::ProcessAlertWorker)
@@ -29,6 +49,36 @@ describe Projects::Alerting::NotifyService do
subject { service.execute(token) }
- it_behaves_like 'does not process incident issues', http_status: 403
+ context 'with activated Alerts Service' do
+ let!(:alerts_service) { create(:alerts_service, project: project) }
+
+ context 'with valid token' do
+ let(:token) { alerts_service.token }
+
+ context 'with a valid payload' do
+ it_behaves_like 'processes incident issues', 1
+ end
+
+ context 'with an invalid payload' do
+ before do
+ allow(Gitlab::Alerting::NotificationPayloadParser)
+ .to receive(:call)
+ .and_raise(Gitlab::Alerting::NotificationPayloadParser::BadPayloadError)
+ end
+
+ it_behaves_like 'does not process incident issues', http_status: 400
+ end
+ end
+
+ context 'with invalid token' do
+ it_behaves_like 'does not process incident issues', http_status: 401
+ end
+ end
+
+ context 'with deactivated Alerts Service' do
+ let!(:alerts_service) { create(:alerts_service, :inactive, project: project) }
+
+ it_behaves_like 'does not process incident issues', http_status: 403
+ end
end
end
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index c526c8aba7c98d7abb1e5cc4f916a51f682f68b6..21bc0651c445ce5b41d2d662acd6949ff3941332 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -32,8 +32,7 @@ Service.available_services_names.each do |service|
{
'github' => :github_project_service_integration,
'jenkins' => :jenkins_integration,
- 'jenkins_deprecated' => :jenkins_integration,
- 'alerts' => :incident_management
+ 'jenkins_deprecated' => :jenkins_integration
}
end