diff --git a/.gitattributes b/.gitattributes index ec47d175c55..8d21784ed11 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ VERSION merge=ours Dangerfile gitlab-language=ruby *.pdf filter=lfs diff=lfs merge=lfs -text +*.rb diff=ruby diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 57c0e014a03..78b5f3fb88b 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -119,7 +119,6 @@ linters: - 'app/views/invites/show.html.haml' - 'app/views/jira_connect/subscriptions/index.html.haml' - 'app/views/layouts/_mailer.html.haml' - - 'app/views/layouts/experiment_mailer.html.haml' - 'app/views/layouts/header/_default.html.haml' - 'app/views/layouts/header/_new_dropdown.haml' - 'app/views/layouts/jira_connect.html.haml' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 21599a74a56..2c103a9998c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1298,7 +1298,6 @@ Graphql/IDType: - 'app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb' - 'app/graphql/resolvers/snippets_resolver.rb' - 'app/graphql/resolvers/user_merge_requests_resolver.rb' - - 'app/graphql/resolvers/user_resolver.rb' # Offense count: 86 # Cop supports --auto-correct. @@ -1328,7 +1327,6 @@ FactoryBot/InlineAssociation: - 'spec/factories/notes.rb' - 'spec/factories/packages.rb' - 'spec/factories/packages/package_file.rb' - - 'spec/factories/prometheus_alert.rb' - 'spec/factories/resource_label_events.rb' - 'spec/factories/resource_milestone_event.rb' - 'spec/factories/resource_state_event.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7805ebc228e..6ebe641f7d7 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -9bb23d81e20244cb4f9635295ae4615e8ff12f8c +60aaf7cdd17b4efdf4dab23eb83aa86fe596a55b diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index a492b95d1d9..430a8c38387 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -10,7 +10,7 @@ function showTooltip(target, title) { setTimeout(() => { target.setAttribute('title', originalTitle); fixTitle(target); - }, 300); + }, 100); }; target.setAttribute('title', title); diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js index cfc6dc61f9f..8adbc2a8168 100644 --- a/app/assets/javascripts/pages/users/index.js +++ b/app/assets/javascripts/pages/users/index.js @@ -4,11 +4,6 @@ import UserCallout from '~/user_callout'; import UserTabs from './user_tabs'; function initUserProfile(action) { - // place profile avatars to top - $('.profile-groups-avatars').tooltip({ - placement: 'top', - }); - // eslint-disable-next-line no-new new UserTabs({ parentEl: '.user-profile', action }); diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 33a20d41bde..b9f268629fb 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -56,6 +56,9 @@ export default { // Note: Realtime is only available on issues right now, future support for MR wil be built later. return this.glFeatures.realTimeIssueSidebar && this.issuableType === 'issue'; }, + relativeUrlRoot() { + return gon.relative_url_root ?? ''; + }, }, created() { this.removeAssignee = this.store.removeAssignee.bind(this.store); @@ -119,7 +122,7 @@ export default { /> .btn:last-child { + float: none; +} + // Pipelines list // Should affect pipelines table components rendered by: // - app/assets/javascripts/commit/pipelines/pipelines_bundle.js diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index d7b4db3840e..a086ea1d6e4 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -347,3 +347,20 @@ } } } + +@include media-breakpoint-down(md) { + .content-list { + &.builds-content-list { + width: 100%; + overflow: auto; + } + } +} + +[data-page='admin:jobs:index'] { + .admin-builds-table { + td:last-child { + min-width: 120px; + } + } +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 2df43b861b2..06911cbab0f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,11 +1,17 @@ -@include media-breakpoint-down(md) { - .content-list { - &.builds-content-list { - width: 100%; - overflow: auto; - } - } -} +/** + * !! NOTE: Do not add more code in this file: + * + * https://gitlab.com/gitlab-org/gitlab/-/issues/267602 + * + * For new pipeline CSS please consider: + * + * For pipelines tables and lists: + * - `app/assets/stylesheets/page_bundles/pipelines.scss` + * + * For individual pipelines and mini-pipelines: + * - `app/assets/stylesheets/page_bundles/pipeline.scss` + * +**/ .ci-table { .avatar { @@ -80,39 +86,3 @@ color: $gl-text-color; } } - -[data-page='admin:jobs:index'] { - .admin-builds-table { - td:last-child { - min-width: 120px; - } - } -} - -.pipelines-container .top-area .nav-controls > .btn:last-child { - float: none; -} - -.progress-bar.bg-primary { - background-color: $blue-500 !important; -} - -.pipeline-stage-pill { - width: 10rem; -} - -.pipeline-job-pill { - width: 8rem; -} - -.stage-rounded { - border-radius: 2rem; -} - -.stage-left-rounded { - border-radius: 2rem 0 0 2rem; -} - -.stage-right-rounded { - border-radius: 0 2rem 2rem 0; -} diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index c7b8486d1c9..26fc1c11f6d 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -15,13 +15,11 @@ class InvitesController < ApplicationController feature_category :authentication_and_authorization def show - track_new_user_invite_experiment('opened') accept if skip_invitation_prompt? end def accept if member.accept_invite!(current_user) - track_new_user_invite_experiment('accepted') track_invitation_reminders_experiment('accepted') redirect_to invite_details[:path], notice: _("You have been granted %{member_human_access} access to %{title} %{name}.") % { member_human_access: member.human_access, title: invite_details[:title], name: invite_details[:name] } @@ -110,25 +108,13 @@ class InvitesController < ApplicationController end end - def track_new_user_invite_experiment(action) - return unless params[:new_user_invite] - - property = params[:new_user_invite] == 'experiment' ? 'experiment_group' : 'control_group' - - track_experiment(:invite_email, action, property) - end - def track_invitation_reminders_experiment(action) return unless Gitlab::Experimentation.enabled?(:invitation_reminders) property = Gitlab::Experimentation.enabled_for_attribute?(:invitation_reminders, member.invite_email) ? 'experimental_group' : 'control_group' - track_experiment(:invitation_reminders, action, property) - end - - def track_experiment(experiment_key, action, property) Gitlab::Tracking.event( - Gitlab::Experimentation.experiment(experiment_key).tracking_category, + Gitlab::Experimentation.experiment(:invitation_reminders).tracking_category, action, property: property, label: Digest::MD5.hexdigest(member.to_global_id.to_s) diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb index a34cecba491..06c1f0cb42d 100644 --- a/app/graphql/resolvers/user_resolver.rb +++ b/app/graphql/resolvers/user_resolver.rb @@ -6,7 +6,7 @@ module Resolvers type Types::UserType, null: true - argument :id, GraphQL::ID_TYPE, + argument :id, Types::GlobalIDType[User], required: false, description: 'ID of the User' diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 57e4c7df440..0b5a8dfdc24 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -51,34 +51,20 @@ module Emails return unless member_exists? - subject_line = subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}") - - if member.invite_to_unknown_user? && Feature.enabled?(:invite_email_experiment) - subject_line = subject("#{member.created_by.name} invited you to join GitLab") if member.created_by - @invite_url_params = { new_user_invite: 'experiment' } - - member_email_with_layout( - to: member.invite_email, - subject: subject_line, - template: 'member_invited_email_experiment', - layout: 'experiment_mailer' - ) - - Gitlab::Tracking.event(Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category], 'sent', property: 'experiment_group') - else - @invite_url_params = member.invite_to_unknown_user? ? { new_user_invite: 'control' } : {} - - member_email_with_layout( - to: member.invite_email, - subject: subject_line - ) - - if member.invite_to_unknown_user? - Gitlab::Tracking.event(Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category], 'sent', property: 'control_group') + subject_line = + if member.created_by + subject(s_("MemberInviteEmail|%{member_name} invited you to join GitLab") % { member_name: member.created_by.name }) + else + subject(s_("MemberInviteEmail|Invitation to join the %{project_or_group} %{project_or_group_name}") % { project_or_group: member_source.human_name, project_or_group_name: member_source.model_name.singular }) end - end - if member.invite_to_unknown_user? && Gitlab::Experimentation.enabled?(:invitation_reminders) + member_email_with_layout( + to: member.invite_email, + subject: subject_line, + layout: 'unknown_user_mailer' + ) + + if Gitlab::Experimentation.enabled?(:invitation_reminders) Gitlab::Tracking.event( Gitlab::Experimentation.experiment(:invitation_reminders).tracking_category, 'sent', @@ -105,7 +91,7 @@ module Emails subject_line = subjects[reminder_index] % { inviter: member.created_by.name } member_email_with_layout( - layout: 'experiment_mailer', + layout: 'unknown_user_mailer', to: member.invite_email, subject: subject(subject_line) ) @@ -162,15 +148,10 @@ module Emails @member_source_type.classify.constantize end - def member_email_with_layout(to:, subject:, template: nil, layout: 'mailer') + def member_email_with_layout(to:, subject:, layout: 'mailer') mail(to: to, subject: subject) do |format| - if template - format.html { render template, layout: layout } - format.text { render template, layout: layout } - else - format.html { render layout: layout } - format.text { render layout: layout } - end + format.html { render layout: layout } + format.text { render layout: layout } end end end diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb index 71d09830d56..61cc15a522e 100644 --- a/app/models/alert_management/alert.rb +++ b/app/models/alert_management/alert.rb @@ -38,12 +38,16 @@ module AlertManagement sha_attribute :fingerprint + TITLE_MAX_LENGTH = 200 + DESCRIPTION_MAX_LENGTH = 1_000 + SERVICE_MAX_LENGTH = 100 + TOOL_MAX_LENGTH = 100 HOSTS_MAX_LENGTH = 255 - validates :title, length: { maximum: 200 }, presence: true - validates :description, length: { maximum: 1_000 } - validates :service, length: { maximum: 100 } - validates :monitoring_tool, length: { maximum: 100 } + validates :title, length: { maximum: TITLE_MAX_LENGTH }, presence: true + validates :description, length: { maximum: DESCRIPTION_MAX_LENGTH } + validates :service, length: { maximum: SERVICE_MAX_LENGTH } + validates :monitoring_tool, length: { maximum: TOOL_MAX_LENGTH } validates :project, presence: true validates :events, presence: true validates :severity, presence: true @@ -54,7 +58,7 @@ module AlertManagement conditions: -> { not_resolved }, message: -> (object, data) { _('Cannot have multiple unresolved alerts') } }, unless: :resolved? - validate :hosts_length + validate :hosts_format enum severity: { critical: 0, @@ -251,10 +255,11 @@ module AlertManagement Gitlab::DataBuilder::Alert.build(self) end - def hosts_length + def hosts_format return unless hosts errors.add(:hosts, "hosts array is over #{HOSTS_MAX_LENGTH} chars") if hosts.join.length > HOSTS_MAX_LENGTH + errors.add(:hosts, "hosts array cannot be nested") if hosts.flatten != hosts end end end diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 0cf5ef93e96..6fb387ecca3 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -2,7 +2,7 @@ .todo-avatar = author_avatar(todo, size: 40) - .todo-item.todo-block.align-self-center + .todo-item.todo-block.align-self-center{ data: { qa_selector: "todo_item_container" } } .todo-title - if todo_author_display?(todo) = todo_target_state_pill(todo) @@ -13,7 +13,7 @@ - else (removed) - %span.title-item.action-name + %span.title-item.action-name{ data: { qa_selector: "todo_action_name_content" } } = todo_action_name(todo) %span.title-item.todo-label.todo-target-link @@ -22,7 +22,7 @@ - else = _("(removed)") - %span.title-item.todo-target-title + %span.title-item.todo-target-title{ data: { qa_selector: "todo_target_title_content" } } = todo_target_title(todo) %span.title-item.todo-project.todo-label diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 44d968ae26d..56506370ee0 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -83,7 +83,7 @@ .todos-list-container.js-todos-all - if @todos.any? - .js-todos-list-container + .js-todos-list-container{ data: { qa_selector: "todos_list_container" } } .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } } .card.card-without-border.card-without-margin %ul.content-list.todos-list diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml index 37143799132..1492fea7fb2 100644 --- a/app/views/invites/show.html.haml +++ b/app/views/invites/show.html.haml @@ -25,5 +25,5 @@ - if !member? .actions - = link_to _("Accept invitation"), accept_invite_url(@token, new_user_invite: params[:new_user_invite]), method: :post, class: "btn gl-button btn-success" + = link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn gl-button btn-success" = link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn gl-button btn-danger gl-ml-3" diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml index 24b8138078d..74d05be7f95 100644 --- a/app/views/layouts/_mailer.html.haml +++ b/app/views/layouts/_mailer.html.haml @@ -34,13 +34,7 @@ = render_if_exists 'layouts/mailer/additional_text' - %tr.footer - %td - %img{ alt: "GitLab", height: "33", width: "90", src: image_url('mailers/gitlab_footer_logo.gif') } - %div - - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, class: 'mng-notif-link') - - help_link = link_to(_("Help"), help_url, class: 'help-link') - = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link } + = yield :footer = yield :additional_footer %tr diff --git a/app/views/layouts/experiment_mailer.html.haml b/app/views/layouts/experiment_mailer.html.haml deleted file mode 100644 index 5a342c400d6..00000000000 --- a/app/views/layouts/experiment_mailer.html.haml +++ /dev/null @@ -1,48 +0,0 @@ - -%html{ lang: "en" } - %head - %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ - %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ - %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ - %title= message.subject - - -# Avoid premailer processing of client-specific styles (@media tag not supported) - -# We need to inline the contents here because mail clients (e.g. iOS Mail, Outlook) - -# do not support linked stylesheets. - %style{ type: 'text/css', 'data-premailer': 'ignore' } - = asset_to_string('mailer_client_specific.css').html_safe - - = stylesheet_link_tag 'mailer.css' - %body - %table#body{ border: "0", cellpadding: "0", cellspacing: "0" } - %tbody - %tr.line - %td - %tr.header - %td - = html_header_message - = header_logo - %tr - %td - %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0" } - %tbody - %tr - %td.wrapper-cell{ style: "padding: 0" } - %table.content{ border: "0", cellpadding: "0", cellspacing: "0" } - %tbody - = yield - - = render_if_exists 'layouts/mailer/additional_text' - - %tr.footer - %td{ style: "padding: 24px 0" } - %img{ alt: "GitLab", height: "33", width: "90", src: image_url('mailers/gitlab_footer_logo.gif') } - %p{ style: "color: #949ba5; max-width: 640px; margin: 0 auto; text-align: left; font-size: 12px;" } - GitLab is a complete DevOps platform, delivered as a single application, fundamentally changing the way - %br - Development, Security, and Ops teams collaborate. - - = yield :additional_footer - %tr - %td.footer-message - = html_footer_message diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 28dcbce7183..c2eb6b68024 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -1 +1,10 @@ += content_for :footer do + %tr.footer + %td + %img.footer-logo{ alt: "GitLab", src: image_url('mailers/gitlab_footer_logo.gif') } + %div + - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, class: 'mng-notif-link') + - help_link = link_to(_("Help"), help_url, class: 'help-link') + = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link } + = render 'layouts/mailer' diff --git a/app/views/layouts/unknown_user_mailer.html.haml b/app/views/layouts/unknown_user_mailer.html.haml new file mode 100644 index 00000000000..2eb7b400604 --- /dev/null +++ b/app/views/layouts/unknown_user_mailer.html.haml @@ -0,0 +1,8 @@ += content_for :footer do + %tr.footer + %td.gitlab-info + %img.footer-logo{ alt: "GitLab", src: image_url('mailers/gitlab_footer_logo.gif') } + %p.gitlab-info-text + = html_escape(_("GitLab is a complete DevOps platform, delivered as a single application, fundamentally changing the way%{br_tag}Development, Security, and Ops teams collaborate")) % { br_tag: '
'.html_safe } + += render 'layouts/mailer' diff --git a/app/views/layouts/unknown_user_mailer.text.erb b/app/views/layouts/unknown_user_mailer.text.erb new file mode 100644 index 00000000000..f3d8f13b7bf --- /dev/null +++ b/app/views/layouts/unknown_user_mailer.text.erb @@ -0,0 +1,9 @@ +<%= text_header_message %> + +<%= yield -%> + +-- <%# signature marker %> +<%= _("GitLab is a complete DevOps platform, delivered as a single application, fundamentally changing the way Development, Security, and Ops teams collaborate") %> +<%= render_if_exists 'layouts/mailer/additional_text' %> + +<%= text_footer_message %> diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml index 4fcd2936d25..5ff1e2393c9 100644 --- a/app/views/notify/member_invited_email.html.haml +++ b/app/views/notify/member_invited_email.html.haml @@ -1,16 +1,12 @@ +- placeholders = { strong_start: ''.html_safe, strong_end: ''.html_safe, project_or_group_name: member_source.human_name, project_or_group: member_source.model_name.singular, br_tag: '
'.html_safe, role: member.human_access.downcase } %tr %td.text-content + %h2.invite-header + = s_('InviteEmail|You are invited!') %p - You have been invited - if member.created_by - by - = link_to member.created_by.name, user_url(member.created_by) - to join the - = link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token), class: :highlight - #{member_source.model_name.singular} as #{content_tag :span, member.human_access, class: :highlight}. - - %p - = link_to 'Accept invitation', invite_url(@token, @invite_url_params) - or - = link_to 'decline', decline_invite_url(@token) - + = html_escape(s_("InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders.merge({ inviter: (link_to member.created_by.name, user_url(member.created_by)).html_safe }) + - else + = html_escape(s_("InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders + %p.invite-actions + = link_to s_('InviteEmail|Join now'), invite_url(@token), class: 'invite-btn-join' diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb index e6e6a685f92..e58dfc10810 100644 --- a/app/views/notify/member_invited_email.text.erb +++ b/app/views/notify/member_invited_email.text.erb @@ -1,4 +1,9 @@ -You have been invited <%= "by #{sanitize_name(member.created_by.name)} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>. +<% placeholders = { project_or_group_name: member_source.human_name, project_or_group: member_source.model_name.singular, role: member.human_access.downcase } %> -Accept invitation: <%= invite_url(@token, @invite_url_params) %> -Decline invitation: <%= decline_invite_url(@token) %> +<% if member.created_by %> +<%= s_('InviteEmail|%{inviter} invited you to join the %{project_or_group_name} %{project_or_group} as a %{role}') % placeholders.merge({ inviter: sanitize_name(member.created_by.name) }) %> +<% else %> +<%= s_('InviteEmail|You have been invited to join the %{project_or_group_name} %{project_or_group} as a %{role}') % placeholders %> +<% end %> + +<%= s_('InviteEmail|Join now') %>: <%= invite_url(@token) %> diff --git a/app/views/notify/member_invited_email_experiment.html.haml b/app/views/notify/member_invited_email_experiment.html.haml deleted file mode 100644 index 5cfb6acee05..00000000000 --- a/app/views/notify/member_invited_email_experiment.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%tr - %td.text-content - %h2.invite-header - = s_('InviteEmail|You are invited!') - %p - - if member.created_by - = html_escape(s_("InviteEmail|%{inviter} invited you")) % { inviter: (link_to member.created_by.name, user_url(member.created_by)).html_safe } - = html_escape(s_("InviteEmail|to join the %{strong_start}%{project_or_group_name}%{strong_end}")) % { strong_start: ''.html_safe, strong_end: ''.html_safe, project_or_group_name: member_source.human_name } - %br - = s_("InviteEmail|%{project_or_group} as a %{role}") % { project_or_group: member_source.model_name.singular, role: member.human_access.downcase } - %p.invite-actions - = link_to s_('InviteEmail|Join now'), invite_url(@token, @invite_url_params), class: 'invite-btn-join' diff --git a/app/views/notify/member_invited_email_experiment.text.erb b/app/views/notify/member_invited_email_experiment.text.erb deleted file mode 100644 index 6843cea4df7..00000000000 --- a/app/views/notify/member_invited_email_experiment.text.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% project_and_role = s_('InviteEmail|to join the %{project_or_group_name} %{project_or_group} as a %{role}') \ - % { project_or_group_name: member_source.human_name, project_or_group: member_source.model_name.singular, role: member.human_access.downcase } %> - -<% if member.created_by %> -<%= s_('InviteEmail|%{inviter} invited you') % { inviter: sanitize_name(member.created_by.name) } %> <%= project_and_role %> -<% else %> -<%= s_('InviteEmail|You have been invited') %> <%= project_and_role %> -<% end %> - -Join now: <%= invite_url(@token, @invite_url_params) %> diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml deleted file mode 100644 index 6d7a52c7688..00000000000 --- a/app/views/users/_groups.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -.clearfix - - groups.each do |group| - = link_to group, class: 'profile-groups-avatars inline', title: group.name do - .avatar-container.rect-avatar.s40 - = group_icon(group, class: 'avatar group-avatar s40') diff --git a/changelogs/unreleased/268144-mr-widget-copy-branch-name-button-tooltip-occasionally-shows-undef.yml b/changelogs/unreleased/268144-mr-widget-copy-branch-name-button-tooltip-occasionally-shows-undef.yml new file mode 100644 index 00000000000..412c62c7ce4 --- /dev/null +++ b/changelogs/unreleased/268144-mr-widget-copy-branch-name-button-tooltip-occasionally-shows-undef.yml @@ -0,0 +1,5 @@ +--- +title: Fix undefined tooltip text flashing on clipboard icon +merge_request: 45482 +author: +type: fixed diff --git a/changelogs/unreleased/nicolasdular-new-invite-mail-design.yml b/changelogs/unreleased/nicolasdular-new-invite-mail-design.yml new file mode 100644 index 00000000000..d4d8b4fbabd --- /dev/null +++ b/changelogs/unreleased/nicolasdular-new-invite-mail-design.yml @@ -0,0 +1,5 @@ +--- +title: New group and project invite mail design +merge_request: 44940 +author: +type: changed diff --git a/changelogs/unreleased/sy-truncate-alert-fields.yml b/changelogs/unreleased/sy-truncate-alert-fields.yml new file mode 100644 index 00000000000..e929cde65f9 --- /dev/null +++ b/changelogs/unreleased/sy-truncate-alert-fields.yml @@ -0,0 +1,5 @@ +--- +title: Truncate over-long alert fields instead of return error response +merge_request: 45099 +author: +type: changed diff --git a/changelogs/unreleased/update-cluster-applications-34.yml b/changelogs/unreleased/update-cluster-applications-34.yml new file mode 100644 index 00000000000..5a7a17c2fae --- /dev/null +++ b/changelogs/unreleased/update-cluster-applications-34.yml @@ -0,0 +1,5 @@ +--- +title: Bump cluster applications CI template +merge_request: 45472 +author: +type: other diff --git a/config/routes.rb b/config/routes.rb index 87d32e3d89a..ef718c7618a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -180,6 +180,7 @@ Rails.application.routes.draw do get 'jwks' => 'doorkeeper/openid_connect/discovery#keys' draw :snippets + draw :profile # Product analytics collector match '/collector/i', to: ProductAnalytics::CollectorApp.new, via: :all @@ -266,7 +267,6 @@ Rails.application.routes.draw do draw :uploads draw :explore draw :admin - draw :profile draw :dashboard draw :user draw :project @@ -274,13 +274,6 @@ Rails.application.routes.draw do # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/210024 scope as: 'deprecated' do draw :snippets - end - - # Serve profile routes under /-/ scope. - # To ensure an old unscoped routing is used for the UI we need to - # add prefix 'as' to the scope routing and place it below original routing. - # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/210024 - scope '-', as: :scoped do draw :profile end diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index a44f8f70311..57bd91bc75b 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -15681,7 +15681,7 @@ type Query { """ ID of the User """ - id: ID + id: UserID """ Username of the User diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 6914ba29c57..74aad595bcb 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -45328,7 +45328,7 @@ "description": "ID of the User", "type": { "kind": "SCALAR", - "name": "ID", + "name": "UserID", "ofType": null }, "defaultValue": null diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md index dfe22fc453e..45bc0f55095 100644 --- a/doc/api/templates/gitlab_ci_ymls.md +++ b/doc/api/templates/gitlab_ci_ymls.md @@ -135,7 +135,7 @@ Example response: ```json { "name": "Ruby", - "content": "# This file is a template, and might need editing before it works on your project.\n# Official language image. Look for the different tagged releases at:\n# https://hub.docker.com/r/library/ruby/tags/\nimage: \"ruby:2.5\"\n\n# Pick zero or more services to be used on all builds.\n# Only needed when using a docker container to run your tests in.\n# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service\nservices:\n - mysql:latest\n - redis:latest\n - postgres:latest\n\nvariables:\n POSTGRES_DB: database_name\n\n# Cache gems in between builds\ncache:\n paths:\n - vendor/ruby\n\n# This is a basic example for a gem or script which doesn't use\n# services such as redis or postgres\nbefore_script:\n - ruby -v # Print out ruby version for debugging\n # Uncomment next line if your rails app needs a JS runtime:\n # - apt-get update -q && apt-get install nodejs -yqq\n - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby\n\n# Optional - Delete if not using `rubocop`\nrubocop:\n script:\n - rubocop\n\nrspec:\n script:\n - rspec spec\n\nrails:\n variables:\n DATABASE_URL: \"postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB\"\n script:\n - rails db:migrate\n - rails db:seed\n - rails test\n\n# This deploy job uses a simple deploy flow to Heroku, other providers, e.g. AWS Elastic Beanstalk\n# are supported too: https://github.com/travis-ci/dpl\ndeploy:\n type: deploy\n environment: production\n script:\n - gem install dpl\n - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_PRODUCTION_KEY\n" + "content": "# This file is a template, and might need editing before it works on your project.\n# Official language image. Look for the different tagged releases at:\n# https://hub.docker.com/r/library/ruby/tags/\nimage: \"ruby:2.5\"\n\n# Pick zero or more services to be used on all builds.\n# Only needed when using a docker container to run your tests in.\n# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service\nservices:\n - mysql:latest\n - redis:latest\n - postgres:latest\n\nvariables:\n POSTGRES_DB: database_name\n\n# Cache gems in between builds\ncache:\n paths:\n - vendor/ruby\n\n# This is a basic example for a gem or script which doesn't use\n# services such as redis or postgres\nbefore_script:\n - ruby -v # Print out ruby version for debugging\n # Uncomment next line if your rails app needs a JS runtime:\n # - apt-get update -q && apt-get install nodejs -yqq\n - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby\n\n# Optional - Delete if not using `rubocop`\nrubocop:\n script:\n - rubocop\n\nrspec:\n script:\n - rspec spec\n\nrails:\n variables:\n DATABASE_URL: \"postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB\"\n script:\n - rails db:migrate\n - rails db:seed\n - rails test\n\n# This deploy job uses a simple deploy flow to Heroku, other providers, e.g. AWS Elastic Beanstalk\n# are supported too: https://github.com/travis-ci/dpl\ndeploy:\n type: deploy\n environment: production\n script:\n - gem install dpl\n - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_PRODUCTION_KEY\n" } ``` diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index 9e4d4112ae8..cd90da7adb1 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -205,7 +205,7 @@ spotbugs-sast: FAIL_NEVER: 1 ``` -### Custom rulesets +### Custom rulesets **(ULTIMATE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235382) in GitLab 13.5. diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md index bb10e9d7315..988c9297480 100644 --- a/doc/user/application_security/secret_detection/index.md +++ b/doc/user/application_security/secret_detection/index.md @@ -142,7 +142,7 @@ Secret Detection can be customized by defining available variables: | `SECRET_DETECTION_EXCLUDED_PATHS` | "" | Exclude vulnerabilities from output based on the paths. This is a comma-separated list of patterns. Patterns can be globs, or file or folder paths (for example, `doc,spec` ). Parent directories also match patterns. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225273) in GitLab 13.3. | | `SECRET_DETECTION_HISTORIC_SCAN` | false | Flag to enable a historic Gitleaks scan. | -### Custom rulesets +### Custom rulesets **(ULTIMATE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211387) in GitLab 13.5. diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb index 74e47e5226e..0fd593a3780 100644 --- a/lib/gitlab/alert_management/payload/base.rb +++ b/lib/gitlab/alert_management/payload/base.rb @@ -88,19 +88,19 @@ module Gitlab # AlertManagement::Alert directly for read operations. def alert_params { - description: description, + description: description&.truncate(::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH), ended_at: ends_at, environment: environment, fingerprint: gitlab_fingerprint, - hosts: Array(hosts), - monitoring_tool: monitoring_tool, + hosts: truncate_hosts(Array(hosts).flatten), + monitoring_tool: monitoring_tool&.truncate(::AlertManagement::Alert::TOOL_MAX_LENGTH), payload: payload, project_id: project.id, prometheus_alert: gitlab_alert, - service: service, + service: service&.truncate(::AlertManagement::Alert::SERVICE_MAX_LENGTH), severity: severity, started_at: starts_at, - title: title + title: title&.truncate(::AlertManagement::Alert::TITLE_MAX_LENGTH) }.transform_values(&:presence).compact end @@ -135,6 +135,18 @@ module Gitlab def plain_gitlab_fingerprint; end + def truncate_hosts(hosts) + return hosts if hosts.join.length <= ::AlertManagement::Alert::HOSTS_MAX_LENGTH + + hosts.inject([]) do |new_hosts, host| + remaining_length = ::AlertManagement::Alert::HOSTS_MAX_LENGTH - new_hosts.join.length + + break new_hosts unless remaining_length > 0 + + new_hosts << host.to_s.truncate(remaining_length, omission: '') + end + end + def value_for_paths(paths) target_path = paths.find { |path| payload&.dig(*path) } diff --git a/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml b/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml index e9301a2638d..538f96c4084 100644 --- a/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml @@ -4,7 +4,7 @@ image: "crystallang/crystal:latest" # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service +# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service # services: # - mysql:latest # - redis:latest diff --git a/lib/gitlab/ci/templates/Django.gitlab-ci.yml b/lib/gitlab/ci/templates/Django.gitlab-ci.yml index d35fcb0f807..c657c7e8eb1 100644 --- a/lib/gitlab/ci/templates/Django.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Django.gitlab-ci.yml @@ -4,7 +4,7 @@ image: python:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service +# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest - postgres:latest @@ -13,7 +13,7 @@ variables: POSTGRES_DB: database_name # This folder is cached between builds -# http://docs.gitlab.com/ce/ci/yaml/README.html#cache +# http://docs.gitlab.com/ee/ci/yaml/README.html#cache cache: paths: - ~/.cache/pip/ diff --git a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml index 4d4c6a64cd5..7271526ab1b 100644 --- a/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml @@ -2,7 +2,7 @@ image: elixir:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service +# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest - redis:latest diff --git a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml index 9bde04dff19..5d2c8024524 100644 --- a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml @@ -4,7 +4,7 @@ image: php:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service +# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest @@ -13,7 +13,7 @@ variables: MYSQL_ROOT_PASSWORD: secret # This folder is cached between builds -# http://docs.gitlab.com/ce/ci/yaml/README.html#cache +# http://docs.gitlab.com/ee/ci/yaml/README.html#cache cache: paths: - vendor/ diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml index 7050b41e045..a9638f564f3 100644 --- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml @@ -1,6 +1,6 @@ apply: stage: deploy - image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.29.0" + image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.33.0" environment: name: production variables: diff --git a/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml b/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml index b87178141a1..92379ded77c 100644 --- a/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml @@ -4,14 +4,14 @@ image: node:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service +# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest - redis:latest - postgres:latest # This folder is cached between builds -# http://docs.gitlab.com/ce/ci/yaml/README.html#cache +# http://docs.gitlab.com/ee/ci/yaml/README.html#cache cache: paths: - node_modules/ diff --git a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml index 25ea20e454f..84e8223e69b 100644 --- a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml @@ -19,7 +19,7 @@ before_script: - php composer.phar install # Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service -# See http://docs.gitlab.com/ce/ci/services/README.html for examples. +# See http://docs.gitlab.com/ee/ci/services/README.html for examples. services: - mysql:5.7 diff --git a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml index a683561a455..3a6eac63892 100644 --- a/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml @@ -1,7 +1,7 @@ image: node:latest # This folder is cached between builds -# http://docs.gitlab.com/ce/ci/yaml/README.html#cache +# http://docs.gitlab.com/ee/ci/yaml/README.html#cache cache: paths: - node_modules/ diff --git a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml index b3cad8b858a..275364afae4 100644 --- a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml @@ -4,7 +4,7 @@ image: "ruby:2.5" # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service +# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest - redis:latest diff --git a/lib/gitlab/ci/templates/Rust.gitlab-ci.yml b/lib/gitlab/ci/templates/Rust.gitlab-ci.yml index f35470367cc..94117a79d1c 100644 --- a/lib/gitlab/ci/templates/Rust.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Rust.gitlab-ci.yml @@ -4,7 +4,7 @@ image: "rust:latest" # Optional: Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service +# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service # services: # - mysql:latest # - redis:latest diff --git a/lib/gitlab/ci/trace/metrics.rb b/lib/gitlab/ci/trace/metrics.rb index cdc474d06dc..097436d84ea 100644 --- a/lib/gitlab/ci/trace/metrics.rb +++ b/lib/gitlab/ci/trace/metrics.rb @@ -59,7 +59,7 @@ module Gitlab strong_memoize(:finalize_histogram) do name = :gitlab_ci_trace_finalize_duration_seconds comment = 'Duration of build trace chunks migration to object storage' - buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 10.0, 30.0, 60.0, 300.0] + buckets = [0.1, 0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0, 60.0, 300.0] labels = {} ::Gitlab::Metrics.histogram(name, comment, labels, buckets) diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 9e10e7aea13..f808e30bd6e 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -14,10 +14,10 @@ module Gitlab def restore return true unless File.exist?(path_to_bundle) + ensure_repository_does_not_exist! + repository.create_from_bundle(path_to_bundle) rescue => e - Repositories::DestroyService.new(repository).execute - shared.error(e) false end @@ -25,6 +25,16 @@ module Gitlab private attr_accessor :repository, :path_to_bundle, :shared + + def ensure_repository_does_not_exist! + if repository.exists? + shared.logger.info( + message: %Q{Deleting existing "#{repository.path}" to re-import it.} + ) + + Repositories::DestroyService.new(repository).execute + end + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2eff8d1365f..9af7ada2e8f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12303,6 +12303,12 @@ msgstr "" msgid "GitLab for Slack" msgstr "" +msgid "GitLab is a complete DevOps platform, delivered as a single application, fundamentally changing the way Development, Security, and Ops teams collaborate" +msgstr "" + +msgid "GitLab is a complete DevOps platform, delivered as a single application, fundamentally changing the way%{br_tag}Development, Security, and Ops teams collaborate" +msgstr "" + msgid "GitLab is a single application for the entire software development lifecycle. From project planning and source code management to CI/CD, monitoring, and security." msgstr "" @@ -14389,25 +14395,22 @@ msgstr "" msgid "Invite teammates (optional)" msgstr "" -msgid "InviteEmail|%{inviter} invited you" +msgid "InviteEmail|%{inviter} invited you to join the %{project_or_group_name} %{project_or_group} as a %{role}" msgstr "" -msgid "InviteEmail|%{project_or_group} as a %{role}" +msgid "InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}" msgstr "" msgid "InviteEmail|Join now" msgstr "" +msgid "InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}" +msgstr "" + msgid "InviteEmail|You are invited!" msgstr "" -msgid "InviteEmail|You have been invited" -msgstr "" - -msgid "InviteEmail|to join the %{project_or_group_name} %{project_or_group} as a %{role}" -msgstr "" - -msgid "InviteEmail|to join the %{strong_start}%{project_or_group_name}%{strong_end}" +msgid "InviteEmail|You have been invited to join the %{project_or_group_name} %{project_or_group} as a %{role}" msgstr "" msgid "InviteMembersBanner|Collaborate with your team" @@ -16208,6 +16211,12 @@ msgstr "" msgid "Member since %{date}" msgstr "" +msgid "MemberInviteEmail|%{member_name} invited you to join GitLab" +msgstr "" + +msgid "MemberInviteEmail|Invitation to join the %{project_or_group} %{project_or_group_name}" +msgstr "" + msgid "Members" msgstr "" diff --git a/package.json b/package.json index eed43d4eb53..276b10f0819 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.171.0", - "@gitlab/ui": "21.33.0", + "@gitlab/ui": "21.34.1", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-3", "@rails/ujs": "^6.0.3-2", diff --git a/qa/qa.rb b/qa/qa.rb index 8564b8e04a7..f281a4b6ef4 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -18,6 +18,7 @@ module QA autoload :Project, 'qa/flow/project' autoload :Saml, 'qa/flow/saml' autoload :User, 'qa/flow/user' + autoload :MergeRequest, 'qa/flow/merge_request' end ## @@ -191,6 +192,7 @@ module QA autoload :Projects, 'qa/page/dashboard/projects' autoload :Groups, 'qa/page/dashboard/groups' autoload :Welcome, 'qa/page/dashboard/welcome' + autoload :Todos, 'qa/page/dashboard/todos' module Snippet autoload :New, 'qa/page/dashboard/snippet/new' diff --git a/qa/qa/flow/login.rb b/qa/qa/flow/login.rb index d4d5cc2dcfc..d23d8eaf097 100644 --- a/qa/qa/flow/login.rb +++ b/qa/qa/flow/login.rb @@ -23,6 +23,7 @@ module QA end def sign_in(as: nil, address: :gitlab, skip_page_validation: false) + Page::Main::Menu.perform(&:sign_out) if Page::Main::Menu.perform(&:signed_in?) Runtime::Browser.visit(address, Page::Main::Login) Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: as, skip_page_validation: skip_page_validation) } end diff --git a/qa/qa/flow/merge_request.rb b/qa/qa/flow/merge_request.rb new file mode 100644 index 00000000000..c26140000fe --- /dev/null +++ b/qa/qa/flow/merge_request.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module QA + module Flow + module MergeRequest + module_function + + def enable_merge_trains + Page::Project::Menu.perform(&:go_to_general_settings) + Page::Project::Settings::Main.perform(&:expand_merge_requests_settings) + Page::Project::Settings::MergeRequest.perform(&:enable_merge_train) + end + end + end +end diff --git a/qa/qa/page/dashboard/todos.rb b/qa/qa/page/dashboard/todos.rb new file mode 100644 index 00000000000..d8baadcf73d --- /dev/null +++ b/qa/qa/page/dashboard/todos.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module QA + module Page + module Dashboard + class Todos < Page::Base + include Page::Component::Snippet + + view 'app/views/dashboard/todos/index.html.haml' do + element :todos_list_container, required: true + end + + view 'app/views/dashboard/todos/_todo.html.haml' do + element :todo_item_container + element :todo_action_name_content + element :todo_target_title_content + end + + def has_todo_list? + has_element? :todo_item_container + end + + def has_latest_todo_item_with_content?(action, title) + within_element(:todos_list_container) do + within_element_by_index(:todo_item_container, 0) do + has_element?(:todo_action_name_content, text: action) && has_element?(:todo_target_title_content, text: title) + end + end + end + end + end + end +end diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index 75a972d2f95..2d13b942c31 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -9,13 +9,6 @@ RSpec.describe InvitesController, :snowplow do let(:project_members) { member.source.users } let(:md5_member_global_id) { Digest::MD5.hexdigest(member.to_global_id.to_s) } let(:params) { { id: raw_invite_token } } - let(:snowplow_event) do - { - category: 'Growth::Acquisition::Experiment::InviteEmail', - label: md5_member_global_id, - property: group_type - } - end shared_examples 'invalid token' do context 'when invite token is not valid' do @@ -94,38 +87,6 @@ RSpec.describe InvitesController, :snowplow do expect(flash[:notice]).to be_nil end - context 'when new_user_invite is not set' do - it 'does not track the user as experiment group' do - request - - expect_no_snowplow_event - end - end - - context 'when new_user_invite is experiment' do - let(:params) { { id: raw_invite_token, new_user_invite: 'experiment' } } - let(:group_type) { 'experiment_group' } - - it 'tracks the user as experiment group' do - request - - expect_snowplow_event(**snowplow_event.merge(action: 'opened')) - expect_snowplow_event(**snowplow_event.merge(action: 'accepted')) - end - end - - context 'when new_user_invite is control' do - let(:params) { { id: raw_invite_token, new_user_invite: 'control' } } - let(:group_type) { 'control_group' } - - it 'tracks the user as control group' do - request - - expect_snowplow_event(**snowplow_event.merge(action: 'opened')) - expect_snowplow_event(**snowplow_event.merge(action: 'accepted')) - end - end - it_behaves_like "tracks the 'accepted' event for the invitation reminders experiment" it_behaves_like 'invalid token' end @@ -158,36 +119,6 @@ RSpec.describe InvitesController, :snowplow do subject(:request) { post :accept, params: params } - context 'when new_user_invite is not set' do - it 'does not track an event' do - request - - expect_no_snowplow_event - end - end - - context 'when new_user_invite is experiment' do - let(:params) { { id: raw_invite_token, new_user_invite: 'experiment' } } - let(:group_type) { 'experiment_group' } - - it 'tracks the user as experiment group' do - request - - expect_snowplow_event(**snowplow_event.merge(action: 'accepted')) - end - end - - context 'when new_user_invite is control' do - let(:params) { { id: raw_invite_token, new_user_invite: 'control' } } - let(:group_type) { 'control_group' } - - it 'tracks the user as control group' do - request - - expect_snowplow_event(**snowplow_event.merge(action: 'accepted')) - end - end - it_behaves_like "tracks the 'accepted' event for the invitation reminders experiment" it_behaves_like 'invalid token' end diff --git a/spec/factories/prometheus_alert.rb b/spec/factories/prometheus_alert.rb index 18cf1a20e0d..ad3868c38ed 100644 --- a/spec/factories/prometheus_alert.rb +++ b/spec/factories/prometheus_alert.rb @@ -7,11 +7,11 @@ FactoryBot.define do threshold { 1 } environment do |alert| - build(:environment, project: alert.project) + association(:environment, project: alert.project) end prometheus_metric do |alert| - build(:prometheus_metric, project: alert.project) + association(:prometheus_metric, project: alert.project) end trait :with_runbook_url do diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb index e8caa2159a4..13ec12c50ea 100644 --- a/spec/features/profiles/account_spec.rb +++ b/spec/features/profiles/account_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'Profile > Account', :js do end it 'allows the user to disconnect when there is an existing identity' do - expect(page).to have_link('Disconnect Twitter', href: '/profile/account/unlink?provider=twitter') + expect(page).to have_link('Disconnect Twitter', href: '/-/profile/account/unlink?provider=twitter') end it 'shows active for a provider that is not allowed to unlink' do diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index dcb901bcf11..4ff3827b240 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -12,8 +12,6 @@ RSpec.describe 'Project navbar' do let_it_be(:project) { create(:project, :repository) } before do - stub_feature_flags(project_iterations: false) - insert_package_nav(_('Operations')) project.add_maintainer(user) diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb index e0f63bad05d..0c26e94e596 100644 --- a/spec/lib/gitlab/alert_management/payload/base_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb @@ -120,14 +120,107 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do end describe '#alert_params' do - before do - allow(parsed_payload).to receive(:title).and_return('title') - allow(parsed_payload).to receive(:description).and_return('description') - end - subject { parsed_payload.alert_params } - it { is_expected.to eq({ description: 'description', project_id: project.id, title: 'title' }) } + context 'with every key' do + let_it_be(:raw_payload) { { 'key' => 'value' } } + let_it_be(:stubs) do + { + description: 'description', + ends_at: Time.current, + environment: create(:environment, project: project), + gitlab_fingerprint: 'gitlab_fingerprint', + hosts: 'hosts', + monitoring_tool: 'monitoring_tool', + gitlab_alert: create(:prometheus_alert, project: project), + service: 'service', + severity: 'critical', + starts_at: Time.current, + title: 'title' + } + end + + let(:expected_result) do + { + description: stubs[:description], + ended_at: stubs[:ends_at], + environment: stubs[:environment], + fingerprint: stubs[:gitlab_fingerprint], + hosts: [stubs[:hosts]], + monitoring_tool: stubs[:monitoring_tool], + payload: raw_payload, + project_id: project.id, + prometheus_alert: stubs[:gitlab_alert], + service: stubs[:service], + severity: stubs[:severity], + started_at: stubs[:starts_at], + title: stubs[:title] + } + end + + before do + allow(parsed_payload).to receive_messages(stubs) + end + + it { is_expected.to eq(expected_result) } + + it 'can generate a valid new alert' do + expect(::AlertManagement::Alert.new(subject.except(:ended_at))).to be_valid + end + end + + context 'with too-long strings' do + let_it_be(:stubs) do + { + description: 'a' * (::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH + 1), + hosts: 'b' * (::AlertManagement::Alert::HOSTS_MAX_LENGTH + 1), + monitoring_tool: 'c' * (::AlertManagement::Alert::TOOL_MAX_LENGTH + 1), + service: 'd' * (::AlertManagement::Alert::SERVICE_MAX_LENGTH + 1), + title: 'e' * (::AlertManagement::Alert::TITLE_MAX_LENGTH + 1) + } + end + + before do + allow(parsed_payload).to receive_messages(stubs) + end + + it do + is_expected.to eq({ + description: stubs[:description].truncate(AlertManagement::Alert::DESCRIPTION_MAX_LENGTH), + hosts: ['b' * ::AlertManagement::Alert::HOSTS_MAX_LENGTH], + monitoring_tool: stubs[:monitoring_tool].truncate(::AlertManagement::Alert::TOOL_MAX_LENGTH), + service: stubs[:service].truncate(::AlertManagement::Alert::SERVICE_MAX_LENGTH), + project_id: project.id, + title: stubs[:title].truncate(::AlertManagement::Alert::TITLE_MAX_LENGTH) + }) + end + end + + context 'with too-long hosts array' do + let(:hosts) { %w(abc def ghij) } + let(:shortened_hosts) { %w(abc def ghi) } + + before do + stub_const('::AlertManagement::Alert::HOSTS_MAX_LENGTH', 9) + allow(parsed_payload).to receive(:hosts).and_return(hosts) + end + + it { is_expected.to eq(hosts: shortened_hosts, project_id: project.id) } + + context 'with host cut off between elements' do + let(:hosts) { %w(abcde fghij) } + let(:shortened_hosts) { %w(abcde fghi) } + + it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) } + end + + context 'with nested hosts' do + let(:hosts) { ['abc', ['de', 'f'], 'g', 'hij'] } # rubocop:disable Style/WordArray + let(:shortened_hosts) { %w(abc de f g hi) } + + it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) } + end + end end describe '#gitlab_fingerprint' do diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index ace4449042e..b32ae60fbcc 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -36,21 +36,20 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do expect(subject.restore).to be_truthy end - context 'when the repository creation fails' do - before do - allow_next_instance_of(Repositories::DestroyService) do |instance| + context 'when the repository already exists' do + it 'deletes the existing repository before importing' do + allow(project.repository).to receive(:exists?).and_return(true) + allow(project.repository).to receive(:path).and_return('repository_path') + + expect_next_instance_of(Repositories::DestroyService) do |instance| expect(instance).to receive(:execute).and_call_original end - end - it 'logs the error' do - allow(project.repository) - .to receive(:create_from_bundle) - .and_raise('9:CreateRepositoryFromBundle: target directory is non-empty') + expect(shared.logger).to receive(:info).with( + message: 'Deleting existing "repository_path" to re-import it.' + ) - expect(shared).to receive(:error).and_call_original - - expect(subject.restore).to be_falsey + expect(subject.restore).to be_truthy end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 8604939ead9..fa91bbc36b2 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -887,97 +887,31 @@ RSpec.describe Notify do subject { described_class.member_invited_email('project', project_member.id, project_member.invite_token) } - context 'when invite_email_experiment is disabled' do - before do - stub_feature_flags(invite_email_experiment: false) - end + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'appearance header and footer enabled' + it_behaves_like 'appearance header and footer not enabled' + it_behaves_like 'does not render a manage notifications link' - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" - it_behaves_like 'appearance header and footer enabled' - it_behaves_like 'appearance header and footer not enabled' + context 'when there is an inviter' do + it 'contains all the useful information' do + is_expected.to have_subject "#{inviter.name} invited you to join GitLab" + is_expected.to have_body_text project.full_name + is_expected.to have_body_text project_member.human_access.downcase + is_expected.to have_body_text project_member.invite_token + end + end + + context 'when there is no inviter' do + let(:inviter) { nil } it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{project.full_name} project" is_expected.to have_body_text project.full_name - is_expected.to have_body_text project_member.human_access + is_expected.to have_body_text project_member.human_access.downcase is_expected.to have_body_text project_member.invite_token end - - context 'when member is invited via an email address' do - it 'does add a param to the invite link' do - is_expected.to have_body_text 'new_user_invite=control' - end - - it 'tracks an event' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Acquisition::Experiment::InviteEmail', - 'sent', - property: 'control_group' - ) - - subject.deliver_now - end - end - - context 'when member is already a user' do - let(:project_member) { invite_to_project(project, inviter: maintainer, user: create(:user)) } - - it 'does not add a param to the invite link' do - is_expected.not_to have_body_text 'new_user_invite' - end - - it 'does not track an event' do - expect(Gitlab::Tracking).not_to receive(:event) - - subject.deliver_now - end - end - end - - context 'when invite_email_experiment is enabled' do - before do - stub_feature_flags(invite_email_experiment: true) - end - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" - - context 'when there is no inviter' do - let(:inviter) { nil } - - it 'contains all the useful information' do - is_expected.to have_subject "Invitation to join the #{project.full_name} project" - is_expected.to have_body_text project.full_name - is_expected.to have_body_text project_member.human_access.downcase - is_expected.to have_body_text project_member.invite_token - end - end - - context 'when there is an inviter' do - it 'contains all the useful information' do - is_expected.to have_subject "#{inviter.name} invited you to join GitLab" - is_expected.to have_body_text project.full_name - is_expected.to have_body_text project_member.human_access.downcase - is_expected.to have_body_text project_member.invite_token - end - end - - it 'adds a param to the invite link' do - is_expected.to have_body_text 'new_user_invite=experiment' - end - - it 'tracks an event' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Acquisition::Experiment::InviteEmail', - 'sent', - property: 'experiment_group' - ) - - subject.deliver_now - end end end @@ -1547,96 +1481,32 @@ RSpec.describe Notify do end end - context 'when invite_email_experiment is disabled' do - before do - stub_feature_flags(invite_email_experiment: false) - end + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'appearance header and footer enabled' + it_behaves_like 'appearance header and footer not enabled' + it_behaves_like 'it requires a group' + it_behaves_like 'does not render a manage notifications link' - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" - it_behaves_like 'appearance header and footer enabled' - it_behaves_like 'appearance header and footer not enabled' - it_behaves_like 'it requires a group' + context 'when there is an inviter' do + it 'contains all the useful information' do + is_expected.to have_subject "#{group_member.created_by.name} invited you to join GitLab" + is_expected.to have_body_text group.name + is_expected.to have_body_text group_member.human_access.downcase + is_expected.to have_body_text group_member.invite_token + end + end + + context 'when there is no inviter' do + let(:inviter) { nil } it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{group.name} group" is_expected.to have_body_text group.name - is_expected.to have_body_text group.web_url - is_expected.to have_body_text group_member.human_access + is_expected.to have_body_text group_member.human_access.downcase is_expected.to have_body_text group_member.invite_token end - - context 'when member is invited via an email address' do - it 'does add a param to the invite link' do - is_expected.to have_body_text 'new_user_invite=control' - end - - it 'tracks an event' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Acquisition::Experiment::InviteEmail', - 'sent', - property: 'control_group' - ) - - subject.deliver_now - end - end - - context 'when member is already a user' do - let(:group_member) { invite_to_group(group, inviter: owner, user: create(:user)) } - - it 'does not add a param to the invite link' do - is_expected.not_to have_body_text 'new_user_invite' - end - - it 'does not track an event' do - expect(Gitlab::Tracking).not_to receive(:event) - - subject.deliver_now - end - end - end - - context 'when invite_email_experiment is enabled' do - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" - it_behaves_like 'it requires a group' - - context 'when there is no inviter' do - let(:inviter) { nil } - - it 'contains all the useful information' do - is_expected.to have_subject "Invitation to join the #{group.name} group" - is_expected.to have_body_text group.name - is_expected.to have_body_text group_member.human_access.downcase - is_expected.to have_body_text group_member.invite_token - end - end - - context 'when there is an inviter' do - it 'contains all the useful information' do - is_expected.to have_subject "#{group_member.created_by.name} invited you to join GitLab" - is_expected.to have_body_text group.name - is_expected.to have_body_text group_member.human_access.downcase - is_expected.to have_body_text group_member.invite_token - end - end - - it 'does add a param to the invite link' do - is_expected.to have_body_text 'new_user_invite' - end - - it 'tracks an event' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Acquisition::Experiment::InviteEmail', - 'sent', - property: 'experiment_group' - ) - - subject.deliver_now - end end end diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb index c80697e1af3..b57062b5fc1 100644 --- a/spec/models/alert_management/alert_spec.rb +++ b/spec/models/alert_management/alert_spec.rb @@ -170,6 +170,12 @@ RSpec.describe AlertManagement::Alert do it { is_expected.to be_valid } end + + context 'nested array' do + let(:hosts) { ['111.111.111.111', ['111.111.111.111']] } + + it { is_expected.not_to be_valid } + end end end diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb index ad4ab26c198..ae0b8d6d7ac 100644 --- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb +++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb @@ -117,15 +117,19 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do end context 'when alert cannot be created' do + let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })} + before do - payload['annotations']['title'] = 'description' * 50 + allow(service).to receive(:alert).and_call_original + allow(service).to receive_message_chain(:alert, :save).and_return(false) + allow(service).to receive_message_chain(:alert, :errors).and_return(errors) end it 'writes a warning to the log' do expect(Gitlab::AppLogger).to receive(:warn).with( message: 'Unable to create AlertManagement::Alert', project_id: project.id, - alert_errors: { title: ["is too long (maximum is 200 characters)"] } + alert_errors: { hosts: ['hosts array is over 255 chars'] } ) execute diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index 7ce7b2161f6..0143bf693c7 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -273,3 +273,12 @@ RSpec.shared_examples 'no email is sent' do expect(subject.message).to be_a_kind_of(ActionMailer::Base::NullMail) end end + +RSpec.shared_examples 'does not render a manage notifications link' do + it do + aggregate_failures do + expect(subject).not_to have_body_text("Manage all notifications") + expect(subject).not_to have_body_text(profile_notifications_url) + end + end +end diff --git a/yarn.lock b/yarn.lock index 731828e3049..714035006e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -866,10 +866,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.171.0.tgz#abc3092bf804f0898301626130e0f3231834924a" integrity sha512-TPfdqIxQDda+0CQHhb9XdF50lmqDmADu6yT8R4oZi6BoUtWLdiHbyFt+RnVU6t7EmjIKicNAii7Ga+f2ljCfUA== -"@gitlab/ui@21.33.0": - version "21.33.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.33.0.tgz#83dd7e4d65557f7b82ce1f9d7d6e7a1c54cb3dbc" - integrity sha512-UBLTz5A1z1Usxw2yfjFNuQ1Ccgx7dYD3M+lDfewDYlU48s0PY7Hm/VXsd543XIuIf+4GEJ1b43kptEZI1wtrbQ== +"@gitlab/ui@21.34.1": + version "21.34.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.34.1.tgz#50c21bb751e88c12a5c7491d3c980a239a0dbbc3" + integrity sha512-S60A7vZcc9ZcXJrT6lCOUbzbPo+Yzx3HOaSIq335iee9DED5WMVCCYvbjm7f7Rn3CoTmDAVC4akNVTzhiuyQYQ== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0"