Commit cf1d4237 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 0ac82f99
<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>
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,
},
});
},
});
};
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 = '' } = {}) {
......
......@@ -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,
};
},
},
......
......@@ -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: {
......
......@@ -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();
......
/*
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',
},
};
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',
},
},
};
import white from './white';
import dark from './dark';
export const themes = [
{
name: 'white',
data: white,
},
{
name: 'dark',
data: dark,
},
];
export const DEFAULT_THEME = 'white';
export default {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editorLineNumber.foreground': '#CCCCCC',
'diffEditor.insertedTextBackground': '#A0F5B420',
'diffEditor.removedTextBackground': '#f9d7dc20',
'editor.selectionBackground': '#aad6f8',
'editorIndentGuide.activeBackground': '#cccccc',
},
};
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,
});
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'));
});
......@@ -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 {
......
......@@ -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: [
......
......@@ -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')
......
......@@ -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
......
......@@ -2334,7 +2334,7 @@ class Project < ApplicationRecord
end
def alerts_service_activated?
false
alerts_service&.active?
end
def self_monitoring?
......
......@@ -260,6 +260,7 @@ class Service < ApplicationRecord
def self.available_services_names
service_names = %w[
alerts
asana
assembla
bamboo
......
......@@ -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
......
.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' } }
- 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'
......@@ -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'
......
---
title: Dark syntax highlighting theme for Web IDE
merge_request: 24158
author:
type: added
---
title: Move Settings->Operations->Incidents to the Core
merge_request: 24600
author:
type: changed
---
title: Makes the generic alerts endpoint available with the free tier
merge_request: 23339
author:
type: changed
---
title: Refactor error tracking specs and add validation to enabled field in error tracking model
merge_request: 24892
author: Rajendra Kadam
type: added
# 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
......
......@@ -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,
......
......@@ -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
......
......@@ -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),
......
......@@ -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
......
# 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
......@@ -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/' }
......
// 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."`;
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);
});
});
});
});
});
......@@ -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) }
......
......@@ -74,6 +74,7 @@ describe('Multi-file editor library', () => {
renderSideBySide: true,
renderLineHighlight: 'all',
hideCursorInOverviewRuler: false,
theme: 'vs white',
});
});
});
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment