Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-01 09:11:01 +00:00
parent 21543f57d6
commit c7cb372557
43 changed files with 198 additions and 630 deletions

View file

@ -291,8 +291,6 @@ linters:
- 'app/views/shared/milestones/_sidebar.html.haml'
- 'app/views/shared/milestones/_top.html.haml'
- 'app/views/shared/notes/_hints.html.haml'
- 'app/views/shared/notifications/_button.html.haml'
- 'app/views/shared/notifications/_new_button.html.haml'
- 'app/views/shared/runners/_runner_description.html.haml'
- 'app/views/shared/runners/show.html.haml'
- 'app/views/shared/snippets/_header.html.haml'

View file

@ -1 +1 @@
59efecafe0838b6c940f67b00726c8c748d7dad5
5d9c71d8d7188bd58f272dc62a7939ece747909d

View file

@ -115,7 +115,11 @@ export default {
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" />
<template v-else>
<gl-form-group v-for="event in events" :key="event.id">
<gl-form-checkbox v-model="event.enabled" @change="updateEvent($event, event)">
<gl-form-checkbox
v-model="event.enabled"
:data-testid="`notification-setting-${event.id}`"
@change="updateEvent($event, event)"
>
<strong>{{ event.name }}</strong
><gl-loading-icon v-if="event.loading" :inline="true" class="gl-ml-2" />
</gl-form-checkbox>

View file

@ -128,7 +128,8 @@ export default {
<gl-button-group
v-if="isCustomNotification"
v-gl-tooltip="{ title: buttonTooltip }"
data-testid="notificationButton"
data-testid="notification-button"
:class="{ disabled: disabled }"
:size="buttonSize"
>
<gl-button
@ -165,12 +166,13 @@ export default {
<gl-dropdown
v-else
v-gl-tooltip="{ title: buttonTooltip }"
data-testid="notificationButton"
data-testid="notification-button"
:text="buttonText"
:icon="buttonIcon"
:loading="isLoading"
:size="buttonSize"
:disabled="disabled"
:class="{ disabled: disabled }"
>
<notifications-dropdown-item
v-for="item in notificationLevels"

View file

@ -33,7 +33,13 @@ export default {
</script>
<template>
<gl-dropdown-item is-check-item :is-checked="isActive" @click="$emit('item-selected', level)">
<gl-dropdown-item
is-check-item
:is-checked="isActive"
:class="{ 'is-active': isActive }"
data-testid="notification-item"
@click="$emit('item-selected', level)"
>
<div class="gl-display-flex gl-flex-direction-column">
<span class="gl-font-weight-bold">{{ title }}</span>
<span class="gl-text-gray-500">{{ description }}</span>

View file

@ -53,6 +53,7 @@ export const i18n = {
reassign_merge_request: s__('NotificationEvent|Reassign merge request'),
reopen_issue: s__('NotificationEvent|Reopen issue'),
reopen_merge_request: s__('NotificationEvent|Reopen merge request'),
merge_when_pipeline_succeeds: s__('NotificationEvent|Merge when pipeline succeeds'),
success_pipeline: s__('NotificationEvent|Successful pipeline'),
},
};

View file

@ -1,35 +0,0 @@
import $ from 'jquery';
import { Rails } from '~/lib/utils/rails_ujs';
import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from './flash';
export default function notificationsDropdown() {
$(document).on('click', '.update-notification', function updateNotificationCallback(e) {
e.preventDefault();
if ($(this).is('.is-active') && $(this).data('notificationLevel') === 'custom') {
return;
}
const notificationLevel = $(this).data('notificationLevel');
const form = $(this).parents('.notification-form').first();
form.find('.js-notification-loading').toggleClass('spinner');
if (form.hasClass('no-label')) {
form.find('.js-notification-loading').toggleClass('hidden');
form.find('.js-notifications-icon').toggleClass('hidden');
}
form.find('#notification_setting_level').val(notificationLevel);
Rails.fire(form[0], 'submit');
});
$(document).on('ajax:success', '.notification-form', (e) => {
const data = e.detail[0];
if (data.saved) {
$(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html);
} else {
Flash(__('Failed to save new settings'), 'alert');
}
});
}

View file

@ -1,48 +0,0 @@
import $ from 'jquery';
import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
export default class NotificationsForm {
constructor() {
this.toggleCheckbox = this.toggleCheckbox.bind(this);
this.initEventListeners();
}
initEventListeners() {
$(document).on('change', '.js-custom-notification-event', this.toggleCheckbox);
}
toggleCheckbox(e) {
const $checkbox = $(e.currentTarget);
const $parent = $checkbox.closest('.form-check');
this.saveEvent($checkbox, $parent);
}
// eslint-disable-next-line class-methods-use-this
showCheckboxLoadingSpinner($parent) {
$parent.find('.is-loading').removeClass('gl-display-none');
$parent.find('.is-done').addClass('gl-display-none');
}
saveEvent($checkbox, $parent) {
const form = $parent.parents('form').first();
this.showCheckboxLoadingSpinner($parent);
axios[form.attr('method')](form.attr('action'), form.serialize())
.then(({ data }) => {
$checkbox.enable();
if (data.saved) {
$parent.find('.is-loading').addClass('gl-display-none');
$parent.find('.is-done').removeClass('gl-display-none');
setTimeout(() => {
$parent.find('.is-done').addClass('gl-display-none');
}, 2000);
}
})
.catch(() => flash(__('There was an error saving your notification settings.')));
}
}

View file

@ -7,8 +7,6 @@ import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { getPagePath, getDashPath } from '~/lib/utils/common_utils';
import initNotificationsDropdown from '~/notifications';
import notificationsDropdown from '~/notifications_dropdown';
import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import GroupTabs from './group_tabs';
@ -22,13 +20,8 @@ export default function initGroupDetails(actionName = 'show') {
new GroupTabs({ parentEl: '.groups-listing', action });
new ShortcutsNavigation();
new NotificationsForm();
if (gon.features?.vueNotificationDropdown) {
initNotificationsDropdown();
} else {
notificationsDropdown();
}
initNotificationsDropdown();
new ProjectsList();

View file

@ -1,7 +0,0 @@
import notificationsDropdown from '../../../notifications_dropdown';
import NotificationsForm from '../../../notifications_form';
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
});

View file

@ -1,9 +1,5 @@
import initNotificationsDropdown from '~/notifications';
import notificationsDropdown from '../../../../notifications_dropdown';
import NotificationsForm from '../../../../notifications_form';
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
initNotificationsDropdown();
});

View file

@ -7,16 +7,13 @@ import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
import NotificationsForm from '~/notifications_form';
import initReadMore from '~/read_more';
import UserCallout from '~/user_callout';
import notificationsDropdown from '../../../notifications_dropdown';
import Star from '../../../star';
initReadMore();
new Star(); // eslint-disable-line no-new
new NotificationsForm(); // eslint-disable-line no-new
// eslint-disable-next-line no-new
new UserCallout({
setCalloutPerProject: false,
@ -43,12 +40,6 @@ if (document.querySelector('.project-show-activity')) {
leaveByUrl('project');
if (gon.features?.vueNotificationDropdown) {
initVueNotificationsDropdown();
} else {
notificationsDropdown();
}
initVueNotificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new

View file

@ -192,11 +192,6 @@ ul.content-list {
display: flex;
align-items: center;
white-space: nowrap;
// Override style that allows the flex-row text to wrap.
&.allow-wrap {
white-space: normal;
}
}
.row-main-content {

View file

@ -30,7 +30,6 @@ class GroupsController < Groups::ApplicationController
before_action do
push_frontend_feature_flag(:vue_issuables_list, @group)
push_frontend_feature_flag(:vue_notification_dropdown, @group, default_enabled: :yaml)
end
before_action do

View file

@ -1,61 +0,0 @@
# frozen_string_literal: true
class NotificationSettingsController < ApplicationController
before_action :authenticate_user!
feature_category :users
def create
return render_404 unless can_read?(resource)
@notification_setting = current_user.notification_settings_for(resource)
@saved = @notification_setting.update(notification_setting_params_for(resource))
render_response
end
def update
@notification_setting = current_user.notification_settings.find(params[:id])
@saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source))
render_response
end
private
def resource
@resource ||=
if params[:project_id].present?
Project.find(params[:project_id])
elsif params[:namespace_id].present?
Group.find(params[:namespace_id])
end
end
def can_read?(resource)
ability_name = resource.class.name.downcase
ability_name = "read_#{ability_name}".to_sym
can?(current_user, ability_name, resource)
end
def render_response
btn_class = nil
if params[:hide_label].present?
btn_class = 'btn-xs' if params[:project_id].present?
response_template = 'shared/notifications/_new_button'
else
response_template = 'shared/notifications/_button'
end
render json: {
html: view_to_html_string(response_template, notification_setting: @notification_setting, btn_class: btn_class),
saved: @saved
}
end
def notification_setting_params_for(source)
params.require(:notification_setting).permit(NotificationSetting.allowed_fields(source))
end
end

View file

@ -31,10 +31,6 @@ class ProjectsController < Projects::ApplicationController
# Project Export Rate Limit
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
before_action do
push_frontend_feature_flag(:vue_notification_dropdown, @project, default_enabled: :yaml)
end
before_action only: [:edit] do
push_frontend_feature_flag(:allow_editing_commit_messages, @project)
end

View file

@ -6,7 +6,8 @@ module Types
graphql_name 'JobArtifactFileType'
::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS.keys.each do |file_type|
value file_type.to_s.upcase, value: file_type.to_s
description = file_type == :codequality ? "CODE QUALITY" : file_type.to_s.titleize.upcase # This is needed as doc lint will not allow codequality as one word
value file_type.to_s.upcase, value: file_type.to_s, description: "#{description} job artifact file type."
end
end
end

View file

@ -23,11 +23,8 @@
.home-panel-buttons.col-md-12.col-lg-6
- if current_user
.gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2{ data: { testid: 'group-buttons' } }
- if Feature.enabled?(:vue_notification_dropdown, @group, default_enabled: :yaml)
- if @notification_setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mr-3 gl-mt-3 gl-vertical-align-top' } }
- else
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn gl-button gl-sm-w-auto gl-w-full', dropdown_container_class: 'gl-mr-0 gl-px-2 gl-sm-w-auto gl-w-full', emails_disabled: emails_disabled
- if @notification_setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mr-3 gl-mt-3 gl-vertical-align-top' } }
- if can_create_subgroups
.gl-px-2.gl-sm-w-auto.gl-w-full
= link_to _("New subgroup"), new_group_path(parent_id: @group.id), class: "btn btn-success btn-md gl-button btn-success-secondary gl-mt-3 gl-sm-w-auto gl-w-full", data: { qa_selector: 'new_subgroup_button' }

View file

@ -9,11 +9,8 @@
= link_to group.name, group_path(group)
.table-section.section-30.text-right
- if Feature.enabled?(:vue_notification_dropdown, default_enabled: :yaml)
- if setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled, dropdown_items: notification_dropdown_items(setting).to_json, notification_level: setting.level, group_id: group.id, container_class: 'gl-mr-3', show_label: "true" } }
- else
= render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
- if setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(setting).to_json, notification_level: setting.level, group_id: group.id, container_class: 'gl-mr-3', show_label: "true" } }
.table-section.section-30
= form_for setting, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications gl-display-flex' } do |f|

View file

@ -8,8 +8,5 @@
= link_to_project(project)
.float-right
- if Feature.enabled?(:vue_notification_dropdown, default_enabled: :yaml)
- if setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled, dropdown_items: notification_dropdown_items(setting).to_json, notification_level: setting.level, project_id: project.id, container_class: 'gl-mr-3', show_label: "true" } }
- else
= render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
- if setting
.js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(setting).to_json, notification_level: setting.level, project_id: project.id, container_class: 'gl-mr-3', show_label: "true" } }

View file

@ -32,11 +32,8 @@
%br
.clearfix
.form-group.float-left.global-notification-setting
- if Feature.enabled?(:vue_notification_dropdown, default_enabled: :yaml)
- if @global_notification_setting
.js-vue-notification-dropdown{ data: { dropdown_items: notification_dropdown_items(@global_notification_setting).to_json, notification_level: @global_notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), show_label: 'true' } }
- else
= render 'shared/notifications/button', notification_setting: @global_notification_setting
- if @global_notification_setting
.js-vue-notification-dropdown{ data: { dropdown_items: notification_dropdown_items(@global_notification_setting).to_json, notification_level: @global_notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), show_label: 'true' } }
.clearfix

View file

@ -46,11 +46,8 @@
.project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
- if current_user
.d-inline-flex
- if Feature.enabled?(:vue_notification_dropdown, @project, default_enabled: :yaml)
- if @notification_setting
.js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, container_class: 'gl-mr-3 gl-mt-5 gl-vertical-align-top' } }
- else
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', dropdown_container_class: 'gl-mr-3', emails_disabled: emails_disabled
- if @notification_setting
.js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, container_class: 'gl-mr-3 gl-mt-5 gl-vertical-align-top' } }
.count-buttons.d-inline-flex
= render 'projects/buttons/star'

View file

@ -2,7 +2,7 @@
- release = @releases.find { |release| release.tag == tag.name }
- commit_status = @tag_pipeline_statuses[tag.name] unless @tag_pipeline_statuses.nil?
%li.flex-row.allow-wrap.js-tag-list
%li.flex-row.js-tag-list{ class: "gl-white-space-normal!" }
.row-main-content
= sprite_icon('tag')
= link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name'

View file

@ -1,37 +0,0 @@
- btn_class = local_assigns.fetch(:btn_class, '')
- emails_disabled = local_assigns.fetch(:emails_disabled, false)
- if notification_setting
- if emails_disabled
- button_title = notification_description(:owner_disabled)
- aria_label = button_title
- btn_class << " disabled"
- else
- button_title = _("Notification setting")
- aria_label = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }
.js-notification-dropdown.notification-dropdown.mr-md-2.home-panel-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
%button.dropdown-new.btn.btn-default.btn-icon.gl-button.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= sprite_icon("notifications", css_class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle.gl-display-flex.gl-align-items-center{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= sprite_icon('chevron-down')
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.btn-icon.gl-button.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
.float-left
= sprite_icon("notifications", css_class: "js-notification-loading")
= notification_title(notification_setting.level)
.float-right
= sprite_icon("chevron-down")
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting
= content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting

View file

@ -1,34 +0,0 @@
- hide_label = local_assigns.fetch(:hide_label, false)
.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), "aria-labelledby": "custom-notifications-title" }
.modal-dialog
.modal-content
.modal-header
%h4#custom-notifications-title.modal-title
#{ _('Custom notification events') }
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
.container-fluid
= form_for notification_setting, html: { class: "custom-notifications-form" } do |f|
= hidden_setting_source_input(notification_setting)
= hidden_field_tag("hide_label", true) if hide_label
.row
.col-lg-4
%h4.gl-mt-0= _('Notification events')
%p
- notification_link = link_to _('notification emails'), help_page_path('user/profile/notifications'), target: '_blank'
- paragraph = _('Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.') % { notification_link: notification_link.html_safe }
#{ paragraph.html_safe }
.col-lg-8
- notification_setting.email_events.each_with_index do |event, index|
- field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
.form-group
.form-check{ class: ("gl-mt-0" if index == 0) }
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event form-check-input", checked: notification_setting.public_send(event))
%label.form-check-label{ for: field_id }
%strong
= notification_event_name(event)
%span.spinner.is-loading.gl-vertical-align-middle.gl-display-none
= sprite_icon('check', css_class: 'is-done gl-display-none gl-vertical-align-middle gl-text-green-600')

View file

@ -1,35 +0,0 @@
- btn_class = local_assigns.fetch(:btn_class, '')
- dropdown_container_class = local_assigns.fetch(:dropdown_container_class, '')
- emails_disabled = local_assigns.fetch(:emails_disabled, false)
- if notification_setting
- if emails_disabled
- button_title = notification_description(:owner_disabled)
- btn_class << " disabled"
- else
- button_title = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }
.js-notification-dropdown.notification-dropdown.home-panel-action-button.gl-mt-3.dropdown.inline{ class: dropdown_container_class }
= form_for notification_setting, remote: true, html: { class: "notification-form no-label" } do |f|
= hidden_setting_source_input(notification_setting)
= hidden_field_tag "hide_label", true
= f.hidden_field :level, class: "notification_setting_level"
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
%button.dropdown-new.btn.gl-button.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
%button.btn.gl-button.btn-default.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" }
= sprite_icon("chevron-down", css_class: "icon mr-0")
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.gl-button.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
= sprite_icon("chevron-down", css_class: "icon")
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting
= content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting, hide_label: true

View file

@ -1,12 +0,0 @@
%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] }
- NotificationSetting.levels.each_key do |level|
- next if level == "custom"
- next if level == "global" && notification_setting.source.nil?
= notification_list_item(level, notification_setting)
%li.divider
%li
%a.update-notification{ href: "#", role: "button", class: ("is-active" if notification_setting.custom?), data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), notification_level: "custom", notification_title: "Custom" } }
%strong.dropdown-menu-inner-title= s_('NotificationSetting|Custom')
%span.dropdown-menu-inner-content= notification_description("custom")

View file

@ -0,0 +1,5 @@
---
title: Add Vue notifications dropdown component
merge_request: 54422
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Fix empty field in custom notification events modal
merge_request: 55313
author: Kev @KevSlashNull
type: fixed

View file

@ -1,8 +0,0 @@
---
name: vue_notification_dropdown
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52068
rollout_issue_url:
milestone: '13.8'
type: development
group: group::optimize
default_enabled: false

View file

@ -166,9 +166,6 @@ Rails.application.routes.draw do
end
end
# Notification settings
resources :notification_settings, only: [:create, :update]
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do
post :accept

View file

@ -5417,33 +5417,33 @@ Iteration ID wildcard values.
| Value | Description |
| ----- | ----------- |
| `ACCESSIBILITY` | |
| `API_FUZZING` | |
| `ARCHIVE` | |
| `BROWSER_PERFORMANCE` | |
| `CLUSTER_APPLICATIONS` | |
| `COBERTURA` | |
| `CODEQUALITY` | |
| `CONTAINER_SCANNING` | |
| `COVERAGE_FUZZING` | |
| `DAST` | |
| `DEPENDENCY_SCANNING` | |
| `DOTENV` | |
| `JUNIT` | |
| `LICENSE_MANAGEMENT` | |
| `LICENSE_SCANNING` | |
| `LOAD_PERFORMANCE` | |
| `LSIF` | |
| `METADATA` | |
| `METRICS` | |
| `METRICS_REFEREE` | |
| `NETWORK_REFEREE` | |
| `PERFORMANCE` | |
| `REQUIREMENTS` | |
| `SAST` | |
| `SECRET_DETECTION` | |
| `TERRAFORM` | |
| `TRACE` | |
| `ACCESSIBILITY` | ACCESSIBILITY job artifact file type. |
| `API_FUZZING` | API FUZZING job artifact file type. |
| `ARCHIVE` | ARCHIVE job artifact file type. |
| `BROWSER_PERFORMANCE` | BROWSER PERFORMANCE job artifact file type. |
| `CLUSTER_APPLICATIONS` | CLUSTER APPLICATIONS job artifact file type. |
| `COBERTURA` | COBERTURA job artifact file type. |
| `CODEQUALITY` | CODE QUALITY job artifact file type. |
| `CONTAINER_SCANNING` | CONTAINER SCANNING job artifact file type. |
| `COVERAGE_FUZZING` | COVERAGE FUZZING job artifact file type. |
| `DAST` | DAST job artifact file type. |
| `DEPENDENCY_SCANNING` | DEPENDENCY SCANNING job artifact file type. |
| `DOTENV` | DOTENV job artifact file type. |
| `JUNIT` | JUNIT job artifact file type. |
| `LICENSE_MANAGEMENT` | LICENSE MANAGEMENT job artifact file type. |
| `LICENSE_SCANNING` | LICENSE SCANNING job artifact file type. |
| `LOAD_PERFORMANCE` | LOAD PERFORMANCE job artifact file type. |
| `LSIF` | LSIF job artifact file type. |
| `METADATA` | METADATA job artifact file type. |
| `METRICS` | METRICS job artifact file type. |
| `METRICS_REFEREE` | METRICS REFEREE job artifact file type. |
| `NETWORK_REFEREE` | NETWORK REFEREE job artifact file type. |
| `PERFORMANCE` | PERFORMANCE job artifact file type. |
| `REQUIREMENTS` | REQUIREMENTS job artifact file type. |
| `SAST` | SAST job artifact file type. |
| `SECRET_DETECTION` | SECRET DETECTION job artifact file type. |
| `TERRAFORM` | TERRAFORM job artifact file type. |
| `TRACE` | TRACE job artifact file type. |
### ListLimitMetric

View file

@ -752,7 +752,7 @@ alt_usage_data(999)
### Adding counters to build new metrics
When adding the results of two counters, use the `add` usage data method that
When adding the results of two counters, use the `add` usage data method that
handles fallback values and exceptions. It also generates a valid [SQL export](#exporting-usage-ping-sql-queries-and-definitions).
Example usage:
@ -869,7 +869,7 @@ Ensure you comply with the [Changelog entries guide](changelog.md).
### 9. Ask for a Product Intelligence Review
On GitLab.com, we have DangerBot setup to monitor Product Intelligence related files and DangerBot recommends a Product Intelligence review. Mention `@gitlab-org/growth/product_intelligence/engineers` in your MR for a review.
On GitLab.com, we have DangerBot setup to monitor Product Intelligence related files and DangerBot recommends a [Product Intelligence review](usage_ping/product_intelligence_review.md). Mention `@gitlab-org/growth/product_intelligence/engineers` in your MR for a review.
### 10. Verify your metric

View file

@ -35,14 +35,14 @@ Each metric is defined in a separate YAML file consisting of a number of fields:
| `value_type` | yes | `string`; one of `string`, `number`, `boolean`. |
| `status` | yes | `string`; status of the metric, may be set to `data_available`, `planned`, `in_progress`, `implemented`, `not_used`, `deprecated` |
| `time_frame` | yes | `string`; may be set to a value like `7d`, `28d`, `all`, `none`. |
| `data_source` | yes | `string`: may be set to a value like `database`, `redis`, `redis_hll`, `prometheus`, `ruby`. |
| `distribution` | yes | The [distribution](https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/#definitions) where the metric applies. |
| `tier` | yes | The [tier]( https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/) where the metric applies. |
| `data_source` | yes | `string`; may be set to a value like `database`, `redis`, `redis_hll`, `prometheus`, `ruby`. |
| `distribution` | yes | `array`; may be set to one of `ce, ee` or `ee`. The [distribution](https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/#definitions) where the tracked feature is available. |
| `tier` | yes | `array`; may be set to one of `free, premium, ultimate`, `premium, ultimate` or `ultimate`. The [tier]( https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/) where the tracked feature is available. |
| `milestone` | no | The milestone when the metric is introduced. |
| `milestone_removed` | no | The milestone when the metric is removed. |
| `introduced_by_url` | no | The URL to the Merge Request that introduced the metric. |
### Example metric definition
### Example YAML metric definition
The linked [`uuid`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/license/uuid.yml)
YAML file includes an example metric definition, where the `uuid` metric is the GitLab
@ -93,3 +93,9 @@ To create a metric definition used in EE, add the `--ee` flag.
bundle exec rails generate gitlab:usage_metric_definition counts.issues --ee --dir=7d
create ee/config/metrics/counts_7d/issues.yml
```
## Metrics added dynamic to Usage Ping payload
The [Redis HLL metrics](../usage_ping.md#known-events-are-added-automatically-in-usage-data-payload) are added automatically to Usage Ping payload.
A YAML metric definition is required for each metric.

View file

@ -0,0 +1,80 @@
---
stage: Growth
group: Product Intelligence
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Product Intelligence review guidelines
This page includes introductory material for a
[Product Intelligence](https://about.gitlab.com/handbook/engineering/development/growth/product-intelligence/)
review, and is specific to Product Intelligence reviews. For broader advice and
general best practices for code reviews, refer to our [code review guide](../code_review.md).
## Resources for Product Intelligence reviewers
- [Usage Ping Guide](../usage_ping.md)
- [Snowplow Guide](../snowplow.md)
- [Metrics Dictionary](metrics_dictionary.md)
## Review process
We recommend a Product Intelligence review when an application update touches
Product Intelligence files.
- Changes that touch `usage_data*` files.
- Changes to the Metrics Dictionary including files in:
- [`config/metrics`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/config/metrics).
- [`ee/config/metrics`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/config/metrics).
- [`dictionary.md`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/usage_ping/dictionary.md).
- [`schema.json`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/schema.json).
- Changes to `tracking` files.
- Changes to Product Intelligence tooling. For example,
[`Gitlab::UsageMetricDefinitionGenerator`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/generators/gitlab/usage_metric_definition_generator.rb)
### Roles and process
The merge request **author** should:
- Decide whether a Product Intelligence review is needed.
- If a Product Intelligence review is needed, add the labels
`~product intelligence` and `~product intelligence::review pending`.
- Assign an
[engineer](https://gitlab.com/groups/gitlab-org/growth/product-intelligence/engineers/-/group_members?with_inherited_permissions=exclude) from the Product Intelligence team for a review.
- Set the correct attributes in YAML metrics:
- `product_section`, `product_stage`, `product_group`, `product_category`
- Provide a clear description of the metric.
- Update the
[Metrics Dictionary](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/usage_ping/dictionary.md) if it is needed.
- Add a changelog [according to guidelines](../changelog.md).
The Product Intelligence **reviewer** should:
- Perform a first-pass review on the merge request and suggest improvements to the author.
- Approve the MR, and relabel the MR with `~"product intelligence::approved"`.
## Review workload distribution
[Danger bot](../dangerbot.md) adds the list of Product Intelligence changed files
and pings the
[`@gitlab-org/growth/product-intelligence/engineers`](https://gitlab.com/groups/gitlab-org/growth/product-intelligence/engineers/-/group_members?with_inherited_permissions=exclude) group for merge requests
that are not drafts.
Any of the Product Intelligence engineers can be assigned for the Product Intelligence review.
### How to review for Product Intelligence
- Check the [metrics location](../usage_ping.md#1-naming-and-placing-the-metrics) in
the Usage Ping JSON payload.
- Add `~database` label and ask for [database review](../database_review.md) for
metrics that are based on Database.
- For tracking using Redis HLL (HyperLogLog):
- Check the Redis slot.
- Check if a [feature flag is needed](../usage_ping.md#recommendations).
- Metrics YAML definitions:
- Check the metric `description`.
- Check the metrics `key_path`.
- Check the `product_section`, `product_stage`, `product_group`, `product_category`.
Read the [stages file](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml).
- Check the file location. Consider the time frame, and if the file should be under `ee`.
- Check the tiers.

View file

@ -976,6 +976,9 @@ msgstr ""
msgid "'%{name}' Value Stream deleted"
msgstr ""
msgid "'%{name}' Value Stream saved"
msgstr ""
msgid "'%{name}' stage already exists"
msgstr ""
@ -8707,7 +8710,7 @@ msgstr ""
msgid "CreateValueStreamForm|'%{name}' Value Stream created"
msgstr ""
msgid "CreateValueStreamForm|'%{name}' Value Stream edited"
msgid "CreateValueStreamForm|'%{name}' Value Stream saved"
msgstr ""
msgid "CreateValueStreamForm|Add another stage"
@ -8986,9 +8989,6 @@ msgstr ""
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart} notification emails%{notificationLinkEnd}."
msgstr ""
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr ""
msgid "Custom project templates"
msgstr ""
@ -20636,9 +20636,6 @@ msgstr ""
msgid "Notification events"
msgstr ""
msgid "Notification setting"
msgstr ""
msgid "Notification setting - %{notification_title}"
msgstr ""
@ -20723,9 +20720,6 @@ msgstr ""
msgid "NotificationLevel|Watch"
msgstr ""
msgid "NotificationSetting|Custom"
msgstr ""
msgid "Notifications"
msgstr ""
@ -25971,6 +25965,9 @@ msgstr ""
msgid "Save Changes"
msgstr ""
msgid "Save Value Stream"
msgstr ""
msgid "Save application"
msgstr ""
@ -30042,9 +30039,6 @@ msgstr ""
msgid "There was an error saving your changes."
msgstr ""
msgid "There was an error saving your notification settings."
msgstr ""
msgid "There was an error subscribing to this label."
msgstr ""
@ -35820,9 +35814,6 @@ msgstr ""
msgid "not found"
msgstr ""
msgid "notification emails"
msgstr ""
msgid "nounSeries|%{firstItem} and %{lastItem}"
msgstr ""

View file

@ -1,202 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe NotificationSettingsController do
let(:project) { create(:project) }
let(:group) { create(:group, :internal) }
let(:user) { create(:user) }
before do
project.add_developer(user)
end
describe '#create' do
context 'when not authorized' do
it 'redirects to sign in page' do
post :create,
params: {
project_id: project.id,
notification_setting: { level: :participating }
}
expect(response).to redirect_to(new_user_session_path)
end
end
context 'when authorized' do
let(:notification_setting) { user.notification_settings_for(source) }
let(:custom_events) do
events = {}
NotificationSetting.email_events(source).each do |event|
events[event.to_s] = true
end
events
end
before do
sign_in(user)
end
context 'for projects' do
let(:source) { project }
it 'creates notification setting' do
post :create,
params: {
project_id: project.id,
notification_setting: { level: :participating }
}
expect(response).to have_gitlab_http_status(:ok)
expect(notification_setting.level).to eq("participating")
expect(notification_setting.user_id).to eq(user.id)
expect(notification_setting.source_id).to eq(project.id)
expect(notification_setting.source_type).to eq("Project")
end
context 'with custom settings' do
it 'creates notification setting' do
post :create,
params: {
project_id: project.id,
notification_setting: { level: :custom }.merge(custom_events)
}
expect(response).to have_gitlab_http_status(:ok)
expect(notification_setting.level).to eq("custom")
custom_events.each do |event, value|
expect(notification_setting.event_enabled?(event)).to eq(value)
end
end
end
end
context 'for groups' do
let(:source) { group }
it 'creates notification setting' do
post :create,
params: {
namespace_id: group.id,
notification_setting: { level: :watch }
}
expect(response).to have_gitlab_http_status(:ok)
expect(notification_setting.level).to eq("watch")
expect(notification_setting.user_id).to eq(user.id)
expect(notification_setting.source_id).to eq(group.id)
expect(notification_setting.source_type).to eq("Namespace")
end
context 'with custom settings' do
it 'creates notification setting' do
post :create,
params: {
namespace_id: group.id,
notification_setting: { level: :custom }.merge(custom_events)
}
expect(response).to have_gitlab_http_status(:ok)
expect(notification_setting.level).to eq("custom")
custom_events.each do |event, value|
expect(notification_setting.event_enabled?(event)).to eq(value)
end
end
end
end
end
context 'not authorized' do
let(:private_project) { create(:project, :private) }
before do
sign_in(user)
end
it 'returns 404' do
post :create,
params: {
project_id: private_project.id,
notification_setting: { level: :participating }
}
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe '#update' do
let(:notification_setting) { user.global_notification_setting }
context 'when not authorized' do
it 'redirects to sign in page' do
put :update,
params: {
id: notification_setting,
notification_setting: { level: :participating }
}
expect(response).to redirect_to(new_user_session_path)
end
end
context 'when authorized' do
before do
sign_in(user)
end
it 'returns success' do
put :update,
params: {
id: notification_setting,
notification_setting: { level: :participating }
}
expect(response).to have_gitlab_http_status(:ok)
end
context 'and setting custom notification setting' do
let(:custom_events) do
events = {}
notification_setting.email_events.each do |event|
events[event] = "true"
end
end
it 'returns success' do
put :update,
params: {
id: notification_setting,
notification_setting: { level: :participating, events: custom_events }
}
expect(response).to have_gitlab_http_status(:ok)
end
end
end
context 'not authorized' do
let(:other_user) { create(:user) }
before do
sign_in(other_user)
end
it 'returns 404' do
put :update,
params: {
id: notification_setting,
notification_setting: { level: :participating }
}
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end

View file

@ -163,7 +163,6 @@ RSpec.describe 'Group show page' do
let!(:project) { create(:project, namespace: group) }
before do
stub_feature_flags(vue_notification_dropdown: false)
group.add_maintainer(maintainer)
sign_in(maintainer)
end
@ -171,14 +170,14 @@ RSpec.describe 'Group show page' do
it 'is enabled by default' do
visit path
expect(page).to have_selector('.notifications-btn:not(.disabled)', visible: true)
expect(page).to have_selector('[data-testid="notification-button"]:not(.disabled)')
end
it 'is disabled if emails are disabled' do
group.update_attribute(:emails_disabled, true)
visit path
expect(page).to have_selector('.notifications-btn.disabled', visible: true)
expect(page).to have_selector('[data-testid="notification-button"].disabled')
end
end

View file

@ -7,7 +7,6 @@ RSpec.describe 'User visits the notifications tab', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(vue_notification_dropdown: false)
project.add_maintainer(user)
sign_in(user)
visit(profile_notifications_path)
@ -16,17 +15,17 @@ RSpec.describe 'User visits the notifications tab', :js do
it 'changes the project notifications setting' do
expect(page).to have_content('Notifications')
first('#notifications-button').click
click_link('On mention')
first('[data-testid="notification-button"]').click
click_button('On mention')
expect(page).to have_selector('#notifications-button', text: 'On mention')
expect(page).to have_selector('[data-testid="notification-button"]', text: 'On mention')
end
context 'when project emails are disabled' do
let(:project) { create(:project, emails_disabled: true) }
it 'notification button is disabled' do
expect(page).to have_selector('.notifications-btn.disabled', visible: true)
expect(page).to have_selector('[data-testid="notification-button"].disabled')
end
end
end

View file

@ -6,38 +6,36 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
let(:project) { create(:project, :public, :repository) }
before do
stub_feature_flags(vue_notification_dropdown: false)
sign_in(project.owner)
end
def click_notifications_button
first('.notifications-btn').click
first('[data-testid="notification-button"]').click
end
it 'changes the notification setting' do
visit project_path(project)
click_notifications_button
click_link 'On mention'
click_button 'On mention'
page.within('.notification-dropdown') do
expect(page).not_to have_css('.gl-spinner')
end
wait_for_requests
click_notifications_button
expect(find('.update-notification.is-active')).to have_content('On mention')
expect(page).to have_css('.notifications-icon[data-testid="notifications-icon"]')
page.within first('[data-testid="notification-button"]') do
expect(page.find('.gl-new-dropdown-item.is-active')).to have_content('On mention')
expect(page).to have_css('[data-testid="notifications-icon"]')
end
end
it 'changes the notification setting to disabled' do
visit project_path(project)
click_notifications_button
click_link 'Disabled'
click_button 'Disabled'
page.within('.notification-dropdown') do
expect(page).not_to have_css('.gl-spinner')
page.within first('[data-testid="notification-button"]') do
expect(page).to have_css('[data-testid="notifications-off-icon"]')
end
expect(page).to have_css('.notifications-icon[data-testid="notifications-off-icon"]')
end
context 'custom notification settings' do
@ -65,11 +63,13 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
it 'shows notification settings checkbox' do
visit project_path(project)
click_notifications_button
page.find('a[data-notification-level="custom"]').click
click_button 'Custom'
page.within('.custom-notifications-form') do
wait_for_requests
page.within('#custom-notifications-modal') do
email_events.each do |event_name|
expect(page).to have_selector("input[name='notification_setting[#{event_name}]']")
expect(page).to have_selector("[data-testid='notification-setting-#{event_name}']")
end
end
end
@ -80,7 +80,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
it 'is disabled' do
visit project_path(project)
expect(page).to have_selector('.notifications-btn.disabled', visible: true)
expect(page).to have_selector('[data-testid="notification-button"].disabled', visible: true)
end
end
end

View file

@ -162,7 +162,7 @@ describe('NotificationsDropdown', () => {
initialNotificationLevel: level,
});
const tooltipElement = findByTestId('notificationButton');
const tooltipElement = findByTestId('notification-button');
const tooltip = getBinding(tooltipElement.element, 'gl-tooltip');
expect(tooltip.value.title).toBe(`${tooltipTitlePrefix} - ${title}`);

View file

@ -15,7 +15,7 @@ module CycleAnalyticsHelpers
end
def toggle_dropdown(field)
page.within("[data-testid='#{field}']") do
page.within("[data-testid*='#{field}']") do
find('.dropdown-toggle').click
wait_for_requests
@ -26,7 +26,7 @@ module CycleAnalyticsHelpers
def select_dropdown_option_by_value(name, value, elem = '.dropdown-item')
toggle_dropdown name
page.find("[data-testid='#{name}'] .dropdown-menu").find("#{elem}[value='#{value}']").click
page.find("[data-testid*='#{name}'] .dropdown-menu").find("#{elem}[value='#{value}']").click
end
def create_commit_referencing_issue(issue, branch_name: generate(:branch))

View file

@ -9,7 +9,6 @@ RSpec.describe 'projects/_home_panel' do
let(:project) { create(:project) }
before do
stub_feature_flags(vue_notification_dropdown: false)
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
@ -25,11 +24,10 @@ RSpec.describe 'projects/_home_panel' do
assign(:notification_setting, notification_settings)
end
it 'makes it possible to set notification level' do
it 'renders Vue app root' do
render
expect(view).to render_template('shared/notifications/_new_button')
expect(rendered).to have_selector('.notification-dropdown')
expect(rendered).to have_selector('.js-vue-notification-dropdown')
end
end
@ -40,10 +38,10 @@ RSpec.describe 'projects/_home_panel' do
assign(:notification_setting, nil)
end
it 'is not possible to set notification level' do
it 'does not render Vue app root' do
render
expect(rendered).not_to have_selector('.notification_dropdown')
expect(rendered).not_to have_selector('.js-vue-notification-dropdown')
end
end
end