Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-07-15 15:09:01 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-15 15:09:01 +0300
commit01d2d6c8695d03440d68b262806f709b57da63b4 (patch)
tree2c01be0f6bc018df88062107b42d4b9a6289f8de /app
parent32e53ae7d739e9457ef81ba4a441a3acb4446240 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue2
-rw-r--r--app/assets/javascripts/smart_interval.js29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue29
-rw-r--r--app/controllers/members/mailgun/permanent_failures_controller.rb65
-rw-r--r--app/finders/container_repositories_finder.rb2
-rw-r--r--app/helpers/packages_helper.rb2
-rw-r--r--app/helpers/sidebars_helper.rb13
-rw-r--r--app/mailers/emails/members.rb6
-rw-r--r--app/models/ci/build_dependencies.rb14
-rw-r--r--app/models/error_tracking/error.rb11
-rw-r--r--app/models/project.rb2
-rw-r--r--app/services/ci/after_requeue_job_service.rb24
-rw-r--r--app/services/error_tracking/collect_error_service.rb38
-rw-r--r--app/services/members/mailgun.rb8
-rw-r--r--app/services/members/mailgun/process_webhook_service.rb39
-rw-r--r--app/views/admin/application_settings/_mailgun.html.haml2
-rw-r--r--app/views/clusters/clusters/_multiple_clusters_message.html.haml2
-rw-r--r--app/views/groups/_delete_project_button.html.haml1
-rw-r--r--app/views/groups/_project_badges.html.haml2
-rw-r--r--app/views/groups/projects.html.haml5
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml179
-rw-r--r--app/views/layouts/nav/sidebar/_group_menus.html.haml166
-rw-r--r--app/views/layouts/nav/sidebar/_group_scope_menu.html.haml6
-rw-r--r--app/views/shared/nav/_sidebar.html.haml9
24 files changed, 440 insertions, 216 deletions
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index fde134f1440..11e6b4577e0 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -221,7 +221,7 @@ export default {
}
if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
- options.push([visibilityOptions.PUBLIC, PAGE_FEATURE_ACCESS_LEVEL]);
+ options.push([30, PAGE_FEATURE_ACCESS_LEVEL]);
}
}
return options;
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 15d04dadb15..6d77952f24e 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -3,6 +3,35 @@ import $ from 'jquery';
/**
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
+ *
+ * This component has two intervals:
+ *
+ * - current interval - when the page is visible - defined by `startingInterval`, `maxInterval`, and `incrementByFactorOf`
+ * - Example:
+ * - `startingInterval: 10000`, `maxInterval: 240000`, `incrementByFactorOf: 2`
+ * - results in `10s, 20s, 40s, 80s, ..., 240s`, it stops increasing at `240s` and keeps this interval indefinitely.
+ * - hidden interval - when the page is not visible
+ *
+ * Visibility transitions:
+ *
+ * - `visible -> not visible`
+ * - `document.addEventListener('visibilitychange', () => ...)`
+ *
+ * > This event fires with a visibilityState of hidden when a user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser, or, on mobile, switches from the browser to a different app.
+ *
+ * Source [Document: visibilitychange event - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event)
+ *
+ * - `window.addEventListener('blur', () => ...)` - every time user clicks somewhere else then in the browser page
+ * - `not visible -> visible`
+ * - `document.addEventListener('visibilitychange', () => ...)` same as the transition `visible -> not visible`
+ * - `window.addEventListener('focus', () => ...)`
+ *
+ * The combination of these two listeners can result in an unexpected resumption of polling:
+ *
+ * - switch to a different window (causes `blur`)
+ * - switch to a different desktop (causes `visibilitychange` (not visible))
+ * - switch back to the original desktop (causes `visibilitychange` (visible))
+ * - *now the polling happens even in window that user doesn't work in*
*/
export default class SmartInterval {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 963f1cf324f..5177eab790b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,6 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__, __ } from '~/locale';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
@@ -8,7 +8,6 @@ import modalEventHub from '~/projects/commit/event_hub';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import eventHub from '../../event_hub';
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
-import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetMerged',
@@ -17,7 +16,7 @@ export default {
},
components: {
MrWidgetAuthorTime,
- statusIcon,
+ GlIcon,
ClipboardButton,
GlLoadingIcon,
GlButton,
@@ -116,7 +115,7 @@ export default {
</script>
<template>
<div class="mr-widget-body media">
- <status-icon status="success" />
+ <gl-icon name="merge" :size="24" class="gl-text-blue-500 gl-mr-3 gl-mt-1" />
<div class="media-body">
<div class="space-children">
<mr-widget-author-time
@@ -131,7 +130,6 @@ export default {
:title="revertTitle"
size="small"
category="secondary"
- variant="warning"
data-qa-selector="revert_button"
@click="openRevertModal"
>
@@ -144,7 +142,6 @@ export default {
:title="revertTitle"
size="small"
category="secondary"
- variant="warning"
data-method="post"
>
{{ revertLabel }}
@@ -169,6 +166,15 @@ export default {
>
{{ cherryPickLabel }}
</gl-button>
+ <gl-button
+ v-if="shouldShowRemoveSourceBranch"
+ :disabled="isMakingRequest"
+ size="small"
+ class="js-remove-branch-button"
+ @click="removeSourceBranch"
+ >
+ {{ s__('mrWidget|Delete source branch') }}
+ </gl-button>
</div>
<section class="mr-info-list" data-qa-selector="merged_status_content">
<p>
@@ -196,17 +202,6 @@ export default {
<p v-if="mr.sourceBranchRemoved">
{{ s__('mrWidget|The source branch has been deleted') }}
</p>
- <p v-if="shouldShowRemoveSourceBranch" class="space-children">
- <span>{{ s__('mrWidget|You can delete the source branch now') }}</span>
- <gl-button
- :disabled="isMakingRequest"
- size="small"
- class="js-remove-branch-button"
- @click="removeSourceBranch"
- >
- {{ s__('mrWidget|Delete source branch') }}
- </gl-button>
- </p>
<p v-if="shouldShowSourceBranchRemoving">
<gl-loading-icon size="sm" :inline="true" />
<span> {{ s__('mrWidget|The source branch is being deleted') }} </span>
diff --git a/app/controllers/members/mailgun/permanent_failures_controller.rb b/app/controllers/members/mailgun/permanent_failures_controller.rb
new file mode 100644
index 00000000000..685faa34694
--- /dev/null
+++ b/app/controllers/members/mailgun/permanent_failures_controller.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Members
+ module Mailgun
+ class PermanentFailuresController < ApplicationController
+ respond_to :json
+
+ skip_before_action :authenticate_user!
+ skip_before_action :verify_authenticity_token
+
+ before_action :ensure_feature_enabled!
+ before_action :authenticate_signature!
+ before_action :validate_invite_email!
+
+ feature_category :authentication_and_authorization
+
+ def create
+ webhook_processor.execute
+
+ head :ok
+ end
+
+ private
+
+ def ensure_feature_enabled!
+ render_406 unless Gitlab::CurrentSettings.mailgun_events_enabled?
+ end
+
+ def authenticate_signature!
+ access_denied! unless valid_signature?
+ end
+
+ def valid_signature?
+ return false if Gitlab::CurrentSettings.mailgun_signing_key.blank?
+
+ # per this guide: https://documentation.mailgun.com/en/latest/user_manual.html#webhooks
+ digest = OpenSSL::Digest.new('SHA256')
+ data = [params.dig(:signature, :timestamp), params.dig(:signature, :token)].join
+
+ hmac_digest = OpenSSL::HMAC.hexdigest(digest, Gitlab::CurrentSettings.mailgun_signing_key, data)
+
+ ActiveSupport::SecurityUtils.secure_compare(params.dig(:signature, :signature), hmac_digest)
+ end
+
+ def validate_invite_email!
+ # permanent_failures webhook does not provide a way to filter failures, so we'll get them all on this endpoint
+ # and we only care about our invite_emails
+ render_406 unless payload[:tags]&.include?(::Members::Mailgun::INVITE_EMAIL_TAG)
+ end
+
+ def webhook_processor
+ ::Members::Mailgun::ProcessWebhookService.new(payload)
+ end
+
+ def payload
+ @payload ||= params.permit!['event-data']
+ end
+
+ def render_406
+ # failure to stop retries per https://documentation.mailgun.com/en/latest/user_manual.html#webhooks
+ head :not_acceptable
+ end
+ end
+ end
+end
diff --git a/app/finders/container_repositories_finder.rb b/app/finders/container_repositories_finder.rb
index 14e4d6799d8..1f6fa9aa1cc 100644
--- a/app/finders/container_repositories_finder.rb
+++ b/app/finders/container_repositories_finder.rb
@@ -25,8 +25,6 @@ class ContainerRepositoriesFinder
end
def project_repositories
- return unless @subject.container_registry_enabled
-
@subject.container_repositories
end
diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb
index 02995267dc2..50984415aa5 100644
--- a/app/helpers/packages_helper.rb
+++ b/app/helpers/packages_helper.rb
@@ -57,7 +57,7 @@ module PackagesHelper
def show_cleanup_policy_on_alert(project)
Gitlab.com? &&
Gitlab.config.registry.enabled &&
- project.container_registry_enabled &&
+ project.feature_available?(:container_registry, current_user) &&
!Gitlab::CurrentSettings.container_expiration_policies_enable_historic_entries &&
Feature.enabled?(:container_expiration_policies_historic_entry, project) &&
project.container_expiration_policy.nil? &&
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 9e3a6a60d75..77af6e37099 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -26,6 +26,12 @@ module SidebarsHelper
Sidebars::Projects::Context.new(**context_data)
end
+ def group_sidebar_context(group, user)
+ context_data = group_sidebar_context_data(group, user)
+
+ Sidebars::Groups::Context.new(**context_data)
+ end
+
private
def sidebar_attributes_for_object(object)
@@ -89,6 +95,13 @@ module SidebarsHelper
show_cluster_hint: show_gke_cluster_integration_callout?(project)
}
end
+
+ def group_sidebar_context_data(group, user)
+ {
+ current_user: user,
+ container: group
+ }
+ end
end
SidebarsHelper.prepend_mod_with('SidebarsHelper')
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index d1870065845..738794a94e7 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -150,10 +150,10 @@ module Emails
end
def invite_email_headers
- if Gitlab.dev_env_or_com?
+ if Gitlab::CurrentSettings.mailgun_events_enabled?
{
- 'X-Mailgun-Tag' => 'invite_email',
- 'X-Mailgun-Variables' => { 'invite_token' => @token }.to_json
+ 'X-Mailgun-Tag' => ::Members::Mailgun::INVITE_EMAIL_TAG,
+ 'X-Mailgun-Variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => @token }.to_json
}
else
{}
diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb
index d39e0411a79..c4a04d42a1e 100644
--- a/app/models/ci/build_dependencies.rb
+++ b/app/models/ci/build_dependencies.rb
@@ -37,12 +37,20 @@ module Ci
next [] unless processable.pipeline_id # we don't have any dependency when creating the pipeline
deps = model_class.where(pipeline_id: processable.pipeline_id).latest
- deps = from_previous_stages(deps)
- deps = from_needs(deps)
+ deps = find_dependencies(processable, deps)
+
from_dependencies(deps).to_a
end
end
+ def find_dependencies(processable, deps)
+ if processable.scheduling_type_dag?
+ from_needs(deps)
+ else
+ from_previous_stages(deps)
+ end
+ end
+
# Dependencies from the same parent-pipeline hierarchy excluding
# the current job's pipeline
def cross_pipeline
@@ -125,8 +133,6 @@ module Ci
end
def from_needs(scope)
- return scope unless processable.scheduling_type_dag?
-
needs_names = processable.needs.artifacts.select(:name)
scope.where(name: needs_names)
end
diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb
index 6b5ba462b94..012dcc4418f 100644
--- a/app/models/error_tracking/error.rb
+++ b/app/models/error_tracking/error.rb
@@ -9,4 +9,15 @@ class ErrorTracking::Error < ApplicationRecord
validates :name, presence: true
validates :description, presence: true
validates :actor, presence: true
+
+ def self.report_error(name:, description:, actor:, platform:, timestamp:)
+ safe_find_or_create_by(
+ name: name,
+ description: description,
+ actor: actor,
+ platform: platform
+ ) do |error|
+ error.update!(last_seen_at: timestamp)
+ end
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 21d5b083476..6873c5f8236 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -377,6 +377,8 @@ class Project < ApplicationRecord
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
+ has_many :error_tracking_errors, inverse_of: :project, class_name: 'ErrorTracking::Error'
+
has_many :timelogs
accepts_nested_attributes_for :variables, allow_destroy: true
diff --git a/app/services/ci/after_requeue_job_service.rb b/app/services/ci/after_requeue_job_service.rb
index 2b611c857c7..b422e57baad 100644
--- a/app/services/ci/after_requeue_job_service.rb
+++ b/app/services/ci/after_requeue_job_service.rb
@@ -10,8 +10,16 @@ module Ci
private
def process_subsequent_jobs(processable)
- processable.pipeline.processables.skipped.after_stage(processable.stage_idx).find_each do |processable|
- process(processable)
+ if Feature.enabled?(:ci_same_stage_job_needs, processable.project, default_enabled: :yaml)
+ (stage_dependent_jobs(processable) | needs_dependent_jobs(processable))
+ .each do |processable|
+ process(processable)
+ end
+ else
+ skipped_jobs(processable).after_stage(processable.stage_idx)
+ .find_each do |job|
+ process(job)
+ end
end
end
@@ -24,5 +32,17 @@ module Ci
processable.process(current_user)
end
end
+
+ def skipped_jobs(processable)
+ processable.pipeline.processables.skipped
+ end
+
+ def stage_dependent_jobs(processable)
+ skipped_jobs(processable).scheduling_type_stage.after_stage(processable.stage_idx)
+ end
+
+ def needs_dependent_jobs(processable)
+ skipped_jobs(processable).scheduling_type_dag.with_needs([processable.name])
+ end
end
end
diff --git a/app/services/error_tracking/collect_error_service.rb b/app/services/error_tracking/collect_error_service.rb
new file mode 100644
index 00000000000..bc1f238d81f
--- /dev/null
+++ b/app/services/error_tracking/collect_error_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class CollectErrorService < ::BaseService
+ def execute
+ # Error is a way to group events based on common data like name or cause
+ # of exception. We need to keep a sane balance here between taking too little
+ # and too much data into group logic.
+ error = project.error_tracking_errors.report_error(
+ name: exception['type'], # Example: ActionView::MissingTemplate
+ description: exception['value'], # Example: Missing template posts/show in...
+ actor: event['transaction'], # Example: PostsController#show
+ platform: event['platform'], # Example: ruby
+ timestamp: event['timestamp']
+ )
+
+ # The payload field contains all the data on error including stacktrace in jsonb.
+ # Together with occured_at these are 2 main attributes that we need to save here.
+ error.events.create!(
+ environment: event['environment'],
+ description: exception['type'],
+ level: event['level'],
+ occurred_at: event['timestamp'],
+ payload: event
+ )
+ end
+
+ private
+
+ def event
+ params[:event]
+ end
+
+ def exception
+ event['exception']['values'].first
+ end
+ end
+end
diff --git a/app/services/members/mailgun.rb b/app/services/members/mailgun.rb
new file mode 100644
index 00000000000..43fb5a14ef1
--- /dev/null
+++ b/app/services/members/mailgun.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Members
+ module Mailgun
+ INVITE_EMAIL_TAG = 'invite_email'
+ INVITE_EMAIL_TOKEN_KEY = :invite_token
+ end
+end
diff --git a/app/services/members/mailgun/process_webhook_service.rb b/app/services/members/mailgun/process_webhook_service.rb
new file mode 100644
index 00000000000..e359a83ad42
--- /dev/null
+++ b/app/services/members/mailgun/process_webhook_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Members
+ module Mailgun
+ class ProcessWebhookService
+ ProcessWebhookServiceError = Class.new(StandardError)
+
+ def initialize(payload)
+ @payload = payload
+ end
+
+ def execute
+ @member = Member.find_by_invite_token(invite_token)
+ update_member_and_log if member
+ rescue ProcessWebhookServiceError => e
+ Gitlab::ErrorTracking.track_exception(e)
+ end
+
+ private
+
+ attr_reader :payload, :member
+
+ def update_member_and_log
+ log_update_event if member.update(invite_email_success: false)
+ end
+
+ def log_update_event
+ Gitlab::AppLogger.info "UPDATED MEMBER INVITE_EMAIL_SUCCESS: member_id: #{member.id}"
+ end
+
+ def invite_token
+ # may want to validate schema in some way using ::JSONSchemer.schema(SCHEMA_PATH).valid?(message) if this
+ # gets more complex
+ payload.dig('user-variables', ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY) ||
+ raise(ProcessWebhookServiceError, "Failed to receive #{::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY} in user-variables: #{payload}")
+ end
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_mailgun.html.haml b/app/views/admin/application_settings/_mailgun.html.haml
index 6204f7df5dc..40b4d5cac6d 100644
--- a/app/views/admin/application_settings/_mailgun.html.haml
+++ b/app/views/admin/application_settings/_mailgun.html.haml
@@ -1,5 +1,3 @@
-- return unless Feature.enabled?(:mailgun_events_receiver)
-
- expanded = integration_expanded?('mailgun_')
%section.settings.as-mailgun.no-animate#js-mailgun-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/clusters/clusters/_multiple_clusters_message.html.haml b/app/views/clusters/clusters/_multiple_clusters_message.html.haml
index da3e128ba32..f235435d907 100644
--- a/app/views/clusters/clusters/_multiple_clusters_message.html.haml
+++ b/app/views/clusters/clusters/_multiple_clusters_message.html.haml
@@ -1,4 +1,4 @@
-- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'using-multiple-kubernetes-clusters')
+- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'use-multiple-kubernetes-clusters')
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- help_link_end = '</a>'.html_safe
diff --git a/app/views/groups/_delete_project_button.html.haml b/app/views/groups/_delete_project_button.html.haml
new file mode 100644
index 00000000000..54a99319418
--- /dev/null
+++ b/app/views/groups/_delete_project_button.html.haml
@@ -0,0 +1 @@
+= link_to _('Delete'), project, data: { confirm: remove_project_message(project) }, method: :delete, class: "btn gl-button btn-danger"
diff --git a/app/views/groups/_project_badges.html.haml b/app/views/groups/_project_badges.html.haml
new file mode 100644
index 00000000000..1f7895e216c
--- /dev/null
+++ b/app/views/groups/_project_badges.html.haml
@@ -0,0 +1,2 @@
+- if project.archived
+ %span.badge.badge-warning.badge-pill.gl-badge.md= _('archived')
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 9d595d19779..9dbf60b119c 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -15,13 +15,12 @@
.controls
= link_to _('Members'), project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button"
= link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button"
- = link_to _('Delete'), project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn gl-button btn-danger"
+ = render 'delete_project_button', project: project
.stats
%span.badge.badge-pill
= storage_counter(project.statistics&.storage_size)
- - if project.archived
- %span.badge.badge-warning archived
+ = render 'project_badges', project: project
.title
= link_to(project_path(project)) do
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 014f3cf7241..980730bc3be 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,176 +1,3 @@
-- issues_count = cached_issuables_count(@group, type: :issues)
-- merge_requests_count = cached_issuables_count(@group, type: :merge_requests)
-- aside_title = @group.subgroup? ? _('Subgroup navigation') : _('Group navigation')
-
-%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(@group), 'aria-label': aside_title }
- .nav-sidebar-inner-scroll
- %ul.sidebar-top-level-items.qa-group-sidebar
- = nav_link(path: ['groups#show', 'groups#details'], html_options: { class: 'context-header' }) do
- = link_to group_path(@group), title: @group.name, data: { qa_selector: 'group_scope_link' } do
- %span{ class: ['avatar-container', 'rect-avatar', 'group-avatar' , 's32'] }
- = group_icon(@group, class: ['avatar', 'avatar-tile', 's32'])
- %span.sidebar-context-title
- = @group.name
- = render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group
-
- - if group_sidebar_link?(:overview)
- - paths = group_overview_nav_link_paths
- = nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do
- = link_to activity_group_path(@group), class: 'has-sub-items', data: { qa_selector: 'group_information_link' } do
- .nav-icon-container
- = sprite_icon('group')
- %span.nav-item-name
- = group_information_title(@group)
-
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_information_submenu'} }
- = nav_link(path: paths, html_options: { class: "fly-out-top-item" } ) do
- = link_to activity_group_path(@group) do
- %strong.fly-out-top-item-name
- = group_information_title(@group)
- %li.divider.fly-out-top-item
-
- - if group_sidebar_link?(:activity)
- = nav_link(path: 'groups#activity') do
- = link_to activity_group_path(@group), title: _('Activity') do
- %span
- = _('Activity')
-
- - if group_sidebar_link?(:labels)
- = nav_link(path: 'labels#index') do
- = link_to group_labels_path(@group), title: _('Labels') do
- %span
- = _('Labels')
-
- - if group_sidebar_link?(:group_members)
- = nav_link(path: 'group_members#index') do
- = link_to group_group_members_path(@group), title: _('Members'), data: { qa_selector: 'group_members_item' } do
- %span
- = _('Members')
-
- = render_if_exists "layouts/nav/ee/epic_link", group: @group
-
- - if group_sidebar_link?(:issues)
- = nav_link(path: group_issues_sub_menu_items, unless: -> { current_path?('issues_analytics#show') }) do
- = link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' }, class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('issues')
- %span.nav-item-name
- = _('Issues')
- %span.badge.badge-pill.count= issues_count
-
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} }
- = nav_link(path: group_issues_sub_menu_items, html_options: { class: "fly-out-top-item" } ) do
- = link_to issues_group_path(@group) do
- %strong.fly-out-top-item-name
- = _('Issues')
- %span.badge.badge-pill.count.issue_counter.fly-out-badge= issues_count
-
- %li.divider.fly-out-top-item
- = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
- = link_to issues_group_path(@group), title: _('List') do
- %span
- = _('List')
-
- - if group_sidebar_link?(:boards)
- = nav_link(path: ['boards#index', 'boards#show']) do
- = link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do
- %span
- = boards_link_text
-
- - if group_sidebar_link?(:milestones)
- = nav_link(path: 'milestones#index') do
- = link_to group_milestones_path(@group), title: _('Milestones'), data: { qa_selector: 'group_milestones_link' } do
- %span
- = _('Milestones')
-
- = render_if_exists 'layouts/nav/sidebar/group_iterations_link'
-
- - if group_sidebar_link?(:merge_requests)
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group) do
- .nav-icon-container
- = sprite_icon('git-merge')
- %span.nav-item-name
- = _('Merge requests')
- %span.badge.badge-pill.count= merge_requests_count
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
- = link_to merge_requests_group_path(@group) do
- %strong.fly-out-top-item-name
- = _('Merge requests')
- %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= merge_requests_count
-
- = render_if_exists "layouts/nav/ee/security_link" # EE-specific
-
- = render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific
-
- - if group_sidebar_link?(:kubernetes)
- = nav_link(controller: [:clusters]) do
- = link_to group_clusters_path(@group) do
- .nav-icon-container
- = sprite_icon('cloud-gear')
- %span.nav-item-name
- = _('Kubernetes')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do
- = link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
- %strong.fly-out-top-item-name
- = _('Kubernetes')
-
- = render 'groups/sidebar/packages'
-
- = render 'layouts/nav/sidebar/analytics_links', links: group_analytics_navbar_links(@group, current_user)
-
- - if group_sidebar_link?(:wiki)
- = render 'layouts/nav/sidebar/wiki_link', wiki_url: @group.wiki.web_url
-
- - if group_sidebar_link?(:settings)
- = nav_link(path: group_settings_nav_link_paths) do
- = link_to edit_group_path(@group), class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('settings')
- %span.nav-item-name{ data: { qa_selector: 'group_settings' } }
- = _('Settings')
- %ul.sidebar-sub-level-items.qa-group-sidebar-submenu{ data: { testid: 'group-settings-menu' } }
- = nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show groups/applications#index], html_options: { class: "fly-out-top-item" } ) do
- = link_to edit_group_path(@group) do
- %strong.fly-out-top-item-name
- = _('Settings')
- %li.divider.fly-out-top-item
- = nav_link(path: 'groups#edit') do
- = link_to edit_group_path(@group), title: _('General'), data: { qa_selector: 'general_settings_link' } do
- %span
- = _('General')
-
- = nav_link(controller: :integrations) do
- = link_to group_settings_integrations_path(@group), title: _('Integrations') do
- %span
- = _('Integrations')
-
- = nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: _('Projects') do
- %span
- = _('Projects')
-
- = nav_link(controller: :repository) do
- = link_to group_settings_repository_path(@group), title: _('Repository') do
- %span
- = _('Repository')
-
- = nav_link(controller: [:ci_cd, 'groups/runners']) do
- = link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do
- %span
- = _('CI/CD')
-
- = nav_link(controller: :applications) do
- = link_to group_settings_applications_path(@group), title: _('Applications') do
- %span
- = _('Applications')
-
- = render 'groups/sidebar/packages_settings'
-
- = render_if_exists "groups/ee/settings_nav"
-
- = render_if_exists "groups/ee/administration_nav"
-
- = render 'shared/sidebar_toggle_button'
+-# We're migration the group sidebar to a logical model based structure. If you need to update
+-# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_group_menus.html.haml.
+= render partial: 'shared/nav/sidebar', object: Sidebars::Groups::Panel.new(group_sidebar_context(@group, current_user))
diff --git a/app/views/layouts/nav/sidebar/_group_menus.html.haml b/app/views/layouts/nav/sidebar/_group_menus.html.haml
new file mode 100644
index 00000000000..5738c8becd5
--- /dev/null
+++ b/app/views/layouts/nav/sidebar/_group_menus.html.haml
@@ -0,0 +1,166 @@
+- issues_count = cached_issuables_count(@group, type: :issues)
+- merge_requests_count = cached_issuables_count(@group, type: :merge_requests)
+
+= render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group
+
+- if group_sidebar_link?(:overview)
+ - paths = group_overview_nav_link_paths
+ = nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do
+ = link_to activity_group_path(@group), class: 'has-sub-items', data: { qa_selector: 'group_information_link' } do
+ .nav-icon-container
+ = sprite_icon('group')
+ %span.nav-item-name
+ = group_information_title(@group)
+
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_information_submenu'} }
+ = nav_link(path: paths, html_options: { class: "fly-out-top-item" } ) do
+ = link_to activity_group_path(@group) do
+ %strong.fly-out-top-item-name
+ = group_information_title(@group)
+ %li.divider.fly-out-top-item
+
+ - if group_sidebar_link?(:activity)
+ = nav_link(path: 'groups#activity') do
+ = link_to activity_group_path(@group), title: _('Activity') do
+ %span
+ = _('Activity')
+
+ - if group_sidebar_link?(:labels)
+ = nav_link(path: 'labels#index') do
+ = link_to group_labels_path(@group), title: _('Labels') do
+ %span
+ = _('Labels')
+
+ - if group_sidebar_link?(:group_members)
+ = nav_link(path: 'group_members#index') do
+ = link_to group_group_members_path(@group), title: _('Members'), data: { qa_selector: 'group_members_item' } do
+ %span
+ = _('Members')
+
+= render_if_exists "layouts/nav/ee/epic_link", group: @group
+
+- if group_sidebar_link?(:issues)
+ = nav_link(path: group_issues_sub_menu_items, unless: -> { current_path?('issues_analytics#show') }) do
+ = link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' }, class: 'has-sub-items' do
+ .nav-icon-container
+ = sprite_icon('issues')
+ %span.nav-item-name
+ = _('Issues')
+ %span.badge.badge-pill.count= issues_count
+
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} }
+ = nav_link(path: group_issues_sub_menu_items, html_options: { class: "fly-out-top-item" } ) do
+ = link_to issues_group_path(@group) do
+ %strong.fly-out-top-item-name
+ = _('Issues')
+ %span.badge.badge-pill.count.issue_counter.fly-out-badge= issues_count
+
+ %li.divider.fly-out-top-item
+ = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
+ = link_to issues_group_path(@group), title: _('List') do
+ %span
+ = _('List')
+
+ - if group_sidebar_link?(:boards)
+ = nav_link(path: ['boards#index', 'boards#show']) do
+ = link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do
+ %span
+ = boards_link_text
+
+ - if group_sidebar_link?(:milestones)
+ = nav_link(path: 'milestones#index') do
+ = link_to group_milestones_path(@group), title: _('Milestones'), data: { qa_selector: 'group_milestones_link' } do
+ %span
+ = _('Milestones')
+
+ = render_if_exists 'layouts/nav/sidebar/group_iterations_link'
+
+- if group_sidebar_link?(:merge_requests)
+ = nav_link(path: 'groups#merge_requests') do
+ = link_to merge_requests_group_path(@group) do
+ .nav-icon-container
+ = sprite_icon('git-merge')
+ %span.nav-item-name
+ = _('Merge requests')
+ %span.badge.badge-pill.count= merge_requests_count
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
+ = link_to merge_requests_group_path(@group) do
+ %strong.fly-out-top-item-name
+ = _('Merge requests')
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= merge_requests_count
+
+= render_if_exists "layouts/nav/ee/security_link" # EE-specific
+
+= render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific
+
+- if group_sidebar_link?(:kubernetes)
+ = nav_link(controller: [:clusters]) do
+ = link_to group_clusters_path(@group) do
+ .nav-icon-container
+ = sprite_icon('cloud-gear')
+ %span.nav-item-name
+ = _('Kubernetes')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do
+ = link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
+ %strong.fly-out-top-item-name
+ = _('Kubernetes')
+
+= render 'groups/sidebar/packages'
+
+= render 'layouts/nav/sidebar/analytics_links', links: group_analytics_navbar_links(@group, current_user)
+
+- if group_sidebar_link?(:wiki)
+ = render 'layouts/nav/sidebar/wiki_link', wiki_url: @group.wiki.web_url
+
+- if group_sidebar_link?(:settings)
+ = nav_link(path: group_settings_nav_link_paths) do
+ = link_to edit_group_path(@group), class: 'has-sub-items' do
+ .nav-icon-container
+ = sprite_icon('settings')
+ %span.nav-item-name{ data: { qa_selector: 'group_settings' } }
+ = _('Settings')
+ %ul.sidebar-sub-level-items{ data: { testid: 'group-settings-menu', qa_selector: 'group_sidebar_submenu' } }
+ = nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show groups/applications#index], html_options: { class: "fly-out-top-item" } ) do
+ = link_to edit_group_path(@group) do
+ %strong.fly-out-top-item-name
+ = _('Settings')
+ %li.divider.fly-out-top-item
+ = nav_link(path: 'groups#edit') do
+ = link_to edit_group_path(@group), title: _('General'), data: { qa_selector: 'general_settings_link' } do
+ %span
+ = _('General')
+
+ = nav_link(controller: :integrations) do
+ = link_to group_settings_integrations_path(@group), title: _('Integrations') do
+ %span
+ = _('Integrations')
+
+ = nav_link(path: 'groups#projects') do
+ = link_to projects_group_path(@group), title: _('Projects') do
+ %span
+ = _('Projects')
+
+ = nav_link(controller: :repository) do
+ = link_to group_settings_repository_path(@group), title: _('Repository') do
+ %span
+ = _('Repository')
+
+ = nav_link(controller: [:ci_cd, 'groups/runners']) do
+ = link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do
+ %span
+ = _('CI/CD')
+
+ = nav_link(controller: :applications) do
+ = link_to group_settings_applications_path(@group), title: _('Applications') do
+ %span
+ = _('Applications')
+
+ = render 'groups/sidebar/packages_settings'
+
+ = render_if_exists "groups/ee/settings_nav"
+
+= render_if_exists "groups/ee/administration_nav"
+
+= render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_group_scope_menu.html.haml b/app/views/layouts/nav/sidebar/_group_scope_menu.html.haml
new file mode 100644
index 00000000000..57c0663f3ae
--- /dev/null
+++ b/app/views/layouts/nav/sidebar/_group_scope_menu.html.haml
@@ -0,0 +1,6 @@
+= nav_link(path: ['groups#show', 'groups#details'], html_options: { class: 'context-header' }) do
+ = link_to group_path(@group), title: @group.name, data: { qa_selector: 'group_scope_link' } do
+ %span{ class: ['avatar-container', 'rect-avatar', 'group-avatar' , 's32'] }
+ = group_icon(@group, class: ['avatar', 'avatar-tile', 's32'])
+ %span.sidebar-context-title
+ = @group.name
diff --git a/app/views/shared/nav/_sidebar.html.haml b/app/views/shared/nav/_sidebar.html.haml
index a52c2f8dd4b..915352996d9 100644
--- a/app/views/shared/nav/_sidebar.html.haml
+++ b/app/views/shared/nav/_sidebar.html.haml
@@ -1,13 +1,14 @@
%aside.nav-sidebar{ class: ('sidebar-collapsed-desktop' if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(sidebar.container), 'aria-label': sidebar.aria_label }
.nav-sidebar-inner-scroll
- - if sidebar.render_raw_scope_menu_partial
- = render sidebar.render_raw_scope_menu_partial
-
%ul.sidebar-top-level-items{ data: { qa_selector: sidebar_qa_selector(sidebar.container) } }
- - if sidebar.scope_menu
+ - if sidebar.render_raw_scope_menu_partial
+ = render sidebar.render_raw_scope_menu_partial
+ - elsif sidebar.scope_menu
= render partial: 'shared/nav/scope_menu', object: sidebar.scope_menu
+
- if sidebar.renderable_menus.any?
= render partial: 'shared/nav/sidebar_menu', collection: sidebar.renderable_menus
+
- if sidebar.render_raw_menus_partial
= render sidebar.render_raw_menus_partial