diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-15 15:09:01 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-15 15:09:01 +0300 |
commit | 01d2d6c8695d03440d68b262806f709b57da63b4 (patch) | |
tree | 2c01be0f6bc018df88062107b42d4b9a6289f8de /app | |
parent | 32e53ae7d739e9457ef81ba4a441a3acb4446240 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |