diff options
43 files changed, 1162 insertions, 88 deletions
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 00000000000..5e16f6f3873 --- /dev/null +++ b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue @@ -0,0 +1,168 @@ +<script> +import { GlButton, GlFormGroup, GlFormInput, GlModal, GlModalDirective } from '@gitlab/ui'; +import _ from 'underscore'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ToggleButton from '~/vue_shared/components/toggle_button.vue'; +import axios from '~/lib/utils/axios_utils'; +import { s__, __, sprintf } from '~/locale'; +import createFlash from '~/flash'; + +export default { + COPY_TO_CLIPBOARD: __('Copy'), + RESET_KEY: __('Reset key'), + components: { + GlButton, + GlFormGroup, + GlFormInput, + GlModal, + ClipboardButton, + ToggleButton, + }, + directives: { + 'gl-modal': GlModalDirective, + }, + props: { + initialAuthorizationKey: { + type: String, + required: false, + default: '', + }, + formPath: { + type: String, + required: true, + }, + url: { + type: String, + required: true, + }, + learnMoreUrl: { + type: String, + required: false, + default: '', + }, + initialActivated: { + type: Boolean, + required: true, + }, + }, + data() { + return { + activated: this.initialActivated, + loadingActivated: false, + authorizationKey: this.initialAuthorizationKey, + }; + }, + computed: { + learnMoreDescription() { + return sprintf( + s__( + 'AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts.', + ), + { + linkStart: `<a href="${_.escape( + this.learnMoreUrl, + )}" target="_blank" rel="noopener noreferrer">`, + linkEnd: '</a>', + }, + false, + ); + }, + sectionDescription() { + const desc = s__( + 'AlertService|Each alert source must be authorized using the following URL and authorization key.', + ); + const learnMoreDesc = this.learnMoreDescription ? ` ${this.learnMoreDescription}` : ''; + + return `${desc}${learnMoreDesc}`; + }, + }, + watch: { + activated() { + this.updateIcon(); + }, + }, + methods: { + updateIcon() { + return document.querySelectorAll('.js-service-active-status').forEach(icon => { + if (icon.dataset.value === this.activated.toString()) { + icon.classList.remove('d-none'); + } else { + icon.classList.add('d-none'); + } + }); + }, + resetKey() { + return axios + .put(this.formPath, { service: { token: '' } }) + .then(res => { + this.authorizationKey = res.data.token; + }) + .catch(() => { + createFlash(__('Failed to reset key. Please try again.')); + }); + }, + toggleActivated(value) { + this.loadingActivated = true; + return axios + .put(this.formPath, { service: { active: value } }) + .then(() => { + this.activated = value; + this.loadingActivated = false; + }) + .catch(() => { + createFlash(__('Update failed. Please try again.')); + this.loadingActivated = false; + }); + }, + }, +}; +</script> + +<template> + <div> + <p v-html="sectionDescription"></p> + <gl-form-group :label="__('Active')" label-for="activated" label-class="label-bold"> + <toggle-button + id="activated" + :disabled-input="loadingActivated" + :is-loading="loadingActivated" + :value="activated" + @change="toggleActivated" + /> + </gl-form-group> + <gl-form-group :label="__('URL')" label-for="url" label-class="label-bold"> + <div class="input-group"> + <gl-form-input id="url" :readonly="true" :value="url" /> + <span class="input-group-append"> + <clipboard-button :text="url" :title="$options.COPY_TO_CLIPBOARD" /> + </span> + </div> + </gl-form-group> + <gl-form-group + :label="__('Authorization key')" + label-for="authorization-key" + label-class="label-bold" + > + <div class="input-group"> + <gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" /> + <span class="input-group-append"> + <clipboard-button :text="authorizationKey" :title="$options.COPY_TO_CLIPBOARD" /> + </span> + </div> + <gl-button v-gl-modal.authKeyModal class="mt-2">{{ $options.RESET_KEY }}</gl-button> + <gl-modal + modal-id="authKeyModal" + :title="$options.RESET_KEY" + :ok-title="$options.RESET_KEY" + ok-variant="danger" + @ok="resetKey" + > + {{ + __( + 'Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.', + ) + }} + </gl-modal> + </gl-form-group> + </div> +</template> 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 00000000000..d49725c6a4d --- /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 bdfbcf71267..8711f6e65af 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 c8c3036812e..8af34dcb5cc 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 4c4166e11f5..a3450522697 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 a0f689065aa..3d729463cb4 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 00000000000..96aaa0cbb50 --- /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 439ae50448a..00000000000 --- 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 00000000000..6ed9f6679a4 --- /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 00000000000..273bc783fc6 --- /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 6488389977c..828cec3e141 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 ba4b271f09e..2d77f2686f7 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 990aca5f0c5..9c64714e5dd 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 3817da8596b..12b4f9ac56c 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 1ce76fd57b1..4ed99b229b5 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 88a2531d649..d328a609439 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 a215b6c881c..1e27ce9f344 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 95b7c6927cf..e60dda59176 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 f0144ad1213..27bbf5c6e57 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 00000000000..be910203125 --- /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 00000000000..fa2f3d7dc08 --- /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 3c955e5f558..30b914b5199 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 00000000000..2444b43487b --- /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 00000000000..f0b383e3a03 --- /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 00000000000..995c2ce982f --- /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 00000000000..9767519275b --- /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 bb07b97e456..f5d0f5eb21b 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 096c465c873..4c44aca2de4 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 1239b103cde..1d948883151 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 = '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?' + ANCHOR_PATTERN = '(?<anchor>\#[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 (?<url> #{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 - '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?' - end - - def anchor_pattern - '(?<anchor>\#[a-z0-9_-]+)?' - end end end end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index f10eb82e03e..23c3259b828 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 0a2cc6f6aa5..62b906e8507 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 00000000000..47de7fab859 --- /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 72e2865dd6a..0181ceed5b9 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 00000000000..36ec0badade --- /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`] = `"<gl-form-input-stub id=\\"authorization-key\\" readonly=\\"true\\" value=\\"abcedfg123\\"></gl-form-input-stub>"`; + +exports[`AlertsServiceForm with default values renders "url" input 1`] = `"<gl-form-input-stub id=\\"url\\" readonly=\\"true\\" value=\\"https://gitlab.com/endpoint-url\\"></gl-form-input-stub>"`; + +exports[`AlertsServiceForm with default values renders toggle button 1`] = `"<toggle-button-stub id=\\"activated\\"></toggle-button-stub>"`; + +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. <a href=\\"https://docs.gitlab.com/ee/user/project/integrations/generic_alerts.md\\" target=\\"_blank\\" rel=\\"noopener noreferrer\\">Learn more</a> 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 00000000000..b7a008c78d0 --- /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(` + <div> + <span class="js-service-active-status fa fa-circle" data-value="true"></span> + <span class="js-service-active-status fa fa-power-off" data-value="false"></span> + </div>`); + }); + + 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 d61289dabb6..37bc2b382cb 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 f1973f7798f..556bd45d3a5 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 9a49d334f52..3f7e412e80b 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 41630c71f21..e81480ab88f 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/'><script>alert(document.cookie)</script>", # unsafe diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b3d8ac83075..dc055244af7 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 059744898b8..120248bdbc6 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 efd168a0a8a..925d323584e 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 c526c8aba7c..21bc0651c44 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 |