diff options
Diffstat (limited to 'app/helpers')
52 files changed, 910 insertions, 481 deletions
diff --git a/app/helpers/analytics/unique_visits_helper.rb b/app/helpers/analytics/unique_visits_helper.rb new file mode 100644 index 00000000000..ded7f54e44e --- /dev/null +++ b/app/helpers/analytics/unique_visits_helper.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Analytics + module UniqueVisitsHelper + extend ActiveSupport::Concern + + def visitor_id + return cookies[:visitor_id] if cookies[:visitor_id].present? + return unless current_user + + uuid = SecureRandom.uuid + cookies[:visitor_id] = { value: uuid, expires: 24.months } + uuid + end + + def track_visit(target_id) + return unless Feature.enabled?(:track_unique_visits) + return unless Gitlab::CurrentSettings.usage_ping_enabled? + return unless visitor_id + + Gitlab::Analytics::UniqueVisits.new.track_visit(visitor_id, target_id) + end + + class_methods do + def track_unique_visits(controller_actions, target_id:) + after_action only: controller_actions, if: -> { request.format.html? && request.headers['DNT'] != '1' } do + track_visit(target_id) + end + end + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bdfdf5a69b3..e8bd5ad9b9b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -335,6 +335,15 @@ module ApplicationHelper } end + def page_startup_api_calls + @api_startup_calls + end + + def add_page_startup_api_call(api_path, options: {}) + @api_startup_calls ||= {} + @api_startup_calls[api_path] = options + end + def autocomplete_data_sources(object, noteable_type) return {} unless object && noteable_type diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index e709d15a946..aa118a9bc45 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -190,6 +190,7 @@ module ApplicationSettingsHelper :container_expiration_policies_enable_historic_entries, :container_registry_token_expire_delay, :default_artifacts_expire_in, + :default_branch_name, :default_branch_protection, :default_ci_config_path, :default_group_visibility, @@ -244,6 +245,7 @@ module ApplicationSettingsHelper :metrics_method_call_threshold, :minimum_password_length, :mirror_available, + :notify_on_unknown_sign_in, :pages_domain_verification_enabled, :password_authentication_enabled_for_web, :password_authentication_enabled_for_git, @@ -319,7 +321,13 @@ module ApplicationSettingsHelper :email_restrictions_enabled, :email_restrictions, :issues_create_limit, - :raw_blob_request_limit + :raw_blob_request_limit, + :project_import_limit, + :project_export_limit, + :project_download_export_limit, + :group_import_limit, + :group_export_limit, + :group_download_export_limit ] end diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index 0f14680607e..c27f5d4ebce 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -22,4 +22,8 @@ module AutoDevopsHelper s_('CICD|instance enabled') end end + + def auto_devops_settings_path(project) + project_settings_ci_cd_path(project, anchor: 'autodevops-settings') + end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 69fe3303840..f4238e7711a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -52,13 +52,12 @@ module BlobHelper edit_button_tag(blob, common_classes, _('Edit'), - Feature.enabled?(:web_ide_default) ? ide_edit_path(project, ref, path) : edit_blob_path(project, ref, path, options), + edit_blob_path(project, ref, path, options), project, ref) end def ide_edit_button(project = @project, ref = @ref, path = @path, blob:) - return if Feature.enabled?(:web_ide_default) return unless blob edit_button_tag(blob, diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb deleted file mode 100644 index 2def3488184..00000000000 --- a/app/helpers/builds_helper.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module BuildsHelper - def build_summary(build, skip: false) - if build.has_trace? - if skip - link_to _("View job log"), pipeline_job_url(build.pipeline, build) - else - build.trace.html(last_lines: 10).html_safe - end - else - _("No job log") - end - end - - def sidebar_build_class(build, current_build) - build_class = [] - build_class << 'active' if build.id === current_build.id - build_class << 'retried' if build.retried? - build_class.join(' ') - end - - def javascript_build_options - { - page_path: project_job_path(@project, @build), - build_status: @build.status, - build_stage: @build.stage, - log_state: '' - } - end - - def build_failed_issue_options - { - title: _("Job Failed #%{build_id}") % { build_id: @build.id }, - description: project_job_url(@project, @build) - } - end -end diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb new file mode 100644 index 00000000000..bfdb830f2c3 --- /dev/null +++ b/app/helpers/ci/builds_helper.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Ci + module BuildsHelper + def build_summary(build, skip: false) + if build.has_trace? + if skip + link_to _('View job log'), pipeline_job_url(build.pipeline, build) + else + build.trace.html(last_lines: 10).html_safe + end + else + _('No job log') + end + end + + def sidebar_build_class(build, current_build) + build_class = [] + build_class << 'active' if build.id === current_build.id + build_class << 'retried' if build.retried? + build_class.join(' ') + end + + def javascript_build_options + { + page_path: project_job_path(@project, @build), + build_status: @build.status, + build_stage: @build.stage, + log_state: '' + } + end + + def build_failed_issue_options + { + title: _("Job Failed #%{build_id}") % { build_id: @build.id }, + description: project_job_url(@project, @build) + } + end + end +end diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb new file mode 100644 index 00000000000..0344413b849 --- /dev/null +++ b/app/helpers/ci/jobs_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Ci + module JobsHelper + def jobs_data + { + "endpoint" => project_job_path(@project, @build, format: :json), + "project_path" => @project.full_path, + "deployment_help_url" => help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting'), + "runner_help_url" => help_page_path('ci/runners/README.html', anchor: 'set-maximum-job-timeout-for-a-runner'), + "runner_settings_url" => project_runners_path(@build.project, anchor: 'js-runners-settings'), + "variables_settings_url" => project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'), + "page_path" => project_job_path(@project, @build), + "build_status" => @build.status, + "build_stage" => @build.stage, + "log_state" => '', + "build_options" => javascript_build_options + } + end + end +end + +Ci::JobsHelper.prepend_if_ee('::EE::Ci::JobsHelper') diff --git a/app/helpers/ci/pipeline_schedules_helper.rb b/app/helpers/ci/pipeline_schedules_helper.rb new file mode 100644 index 00000000000..20e5c90a60e --- /dev/null +++ b/app/helpers/ci/pipeline_schedules_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Ci + module PipelineSchedulesHelper + def timezone_data + ActiveSupport::TimeZone.all.map do |timezone| + { + name: timezone.name, + offset: timezone.now.utc_offset, + identifier: timezone.tzinfo.identifier + } + end + end + end +end diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb new file mode 100644 index 00000000000..8cdb28b2874 --- /dev/null +++ b/app/helpers/ci/runners_helper.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Ci + module RunnersHelper + def runner_status_icon(runner) + status = runner.status + case status + when :not_connected + content_tag :i, nil, + class: "fa fa-warning", + title: "New runner. Has not connected yet" + + when :online, :offline, :paused + content_tag :i, nil, + class: "fa fa-circle runner-status-#{status}", + title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago" + end + end + + def runner_link(runner) + display_name = truncate(runner.display_name, length: 15) + id = "\##{runner.id}" + + if current_user && current_user.admin + link_to admin_runner_path(runner) do + display_name + id + end + else + display_name + id + end + end + + # Due to inability of performing sorting of runners by cached "contacted_at" values we have to show uncached values if sorting by "contacted_asc" is requested. + # Please refer to the following issue for more details: https://gitlab.com/gitlab-org/gitlab-foss/issues/55920 + def runner_contacted_at(runner) + if params[:sort] == 'contacted_asc' + runner.uncached_contacted_at + else + runner.contacted_at + end + end + end +end + +Ci::RunnersHelper.prepend_if_ee('EE::Ci::RunnersHelper') diff --git a/app/helpers/ci/status_helper.rb b/app/helpers/ci/status_helper.rb new file mode 100644 index 00000000000..bca49324a19 --- /dev/null +++ b/app/helpers/ci/status_helper.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +## +# DEPRECATED +# +# These helpers are deprecated in favor of detailed CI/CD statuses. +# +# See 'detailed_status?` method and `Gitlab::Ci::Status` module. +# +module Ci + module StatusHelper + def ci_label_for_status(status) + if detailed_status?(status) + return status.label + end + + label = case status + when 'success' + 'passed' + when 'success-with-warnings' + 'passed with warnings' + when 'manual' + 'waiting for manual action' + when 'scheduled' + 'waiting for delayed job' + else + status + end + translation = "CiStatusLabel|#{label}" + s_(translation) + end + + def ci_text_for_status(status) + if detailed_status?(status) + return status.text + end + + case status + when 'success' + s_('CiStatusText|passed') + when 'success-with-warnings' + s_('CiStatusText|passed') + when 'manual' + s_('CiStatusText|blocked') + when 'scheduled' + s_('CiStatusText|delayed') + else + # All states are already being translated inside the detailed statuses: + # :running => Gitlab::Ci::Status::Running + # :skipped => Gitlab::Ci::Status::Skipped + # :failed => Gitlab::Ci::Status::Failed + # :success => Gitlab::Ci::Status::Success + # :canceled => Gitlab::Ci::Status::Canceled + # The following states are customized above: + # :manual => Gitlab::Ci::Status::Manual + status_translation = "CiStatusText|#{status}" + s_(status_translation) + end + end + + def ci_status_for_statuseable(subject) + status = subject.try(:status) || 'not found' + status.humanize + end + + # rubocop:disable Metrics/CyclomaticComplexity + def ci_icon_for_status(status, size: 16) + if detailed_status?(status) + return sprite_icon(status.icon, size: size) + end + + icon_name = + case status + when 'success' + 'status_success' + when 'success-with-warnings' + 'status_warning' + when 'failed' + 'status_failed' + when 'pending' + 'status_pending' + when 'waiting_for_resource' + 'status_pending' + when 'preparing' + 'status_preparing' + when 'running' + 'status_running' + when 'play' + 'play' + when 'created' + 'status_created' + when 'skipped' + 'status_skipped' + when 'manual' + 'status_manual' + when 'scheduled' + 'status_scheduled' + else + 'status_canceled' + end + + sprite_icon(icon_name, size: size) + end + # rubocop:enable Metrics/CyclomaticComplexity + + def ci_icon_class_for_status(status) + group = detailed_status?(status) ? status.group : status.dasherize + + "ci-status-icon-#{group}" + end + + def pipeline_status_cache_key(pipeline_status) + "pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}" + end + + def render_commit_status(commit, status, ref: nil, tooltip_placement: 'left') + project = commit.project + path = pipelines_project_commit_path(project, commit, ref: ref) + + render_status_with_link( + status, + path, + tooltip_placement: tooltip_placement, + icon_size: 24) + end + + def render_status_with_link(status, path = nil, type: _('pipeline'), tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) + klass = "ci-status-link #{ci_icon_class_for_status(status)} d-inline-flex #{cssclass}" + title = "#{type.titleize}: #{ci_label_for_status(status)}" + data = { toggle: 'tooltip', placement: tooltip_placement, container: container } + + if path + link_to ci_icon_for_status(status, size: icon_size), path, + class: klass, title: title, data: data + else + content_tag :span, ci_icon_for_status(status, size: icon_size), + class: klass, title: title, data: data + end + end + + def detailed_status?(status) + status.respond_to?(:text) && + status.respond_to?(:group) && + status.respond_to?(:label) && + status.respond_to?(:icon) + end + end +end diff --git a/app/helpers/ci/variables_helper.rb b/app/helpers/ci/variables_helper.rb new file mode 100644 index 00000000000..b20390d58e9 --- /dev/null +++ b/app/helpers/ci/variables_helper.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Ci + module VariablesHelper + def ci_variable_protected_by_default? + Gitlab::CurrentSettings.current_application_settings.protected_ci_variables + end + + def create_deploy_token_path(entity, opts = {}) + if entity.is_a?(::Group) + create_deploy_token_group_settings_repository_path(entity, opts) + else + # TODO: change this path to 'create_deploy_token_project_settings_ci_cd_path' + # See MR comment for more detail: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27059#note_311585356 + create_deploy_token_project_settings_repository_path(entity, opts) + end + end + + def revoke_deploy_token_path(entity, token) + if entity.is_a?(::Group) + revoke_group_deploy_token_path(entity, token) + else + revoke_project_deploy_token_path(entity, token) + end + end + + def ci_variable_protected?(variable, only_key_value) + if variable && !only_key_value + variable.protected + else + ci_variable_protected_by_default? + end + end + + def ci_variable_masked?(variable, only_key_value) + if variable && !only_key_value + variable.masked + else + false + end + end + + def ci_variable_type_options + [ + %w(Variable env_var), + %w(File file) + ] + end + + def ci_variable_maskable_regex + Ci::Maskable::REGEX.inspect.sub('\\A', '^').sub('\\z', '$').sub(/^\//, '').sub(/\/[a-z]*$/, '').gsub('\/', '/') + end + end +end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb deleted file mode 100644 index 80d1b7e7edb..00000000000 --- a/app/helpers/ci_status_helper.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -## -# DEPRECATED -# -# These helpers are deprecated in favor of detailed CI/CD statuses. -# -# See 'detailed_status?` method and `Gitlab::Ci::Status` module. -# -module CiStatusHelper - def ci_label_for_status(status) - if detailed_status?(status) - return status.label - end - - label = case status - when 'success' - 'passed' - when 'success-with-warnings' - 'passed with warnings' - when 'manual' - 'waiting for manual action' - when 'scheduled' - 'waiting for delayed job' - else - status - end - translation = "CiStatusLabel|#{label}" - s_(translation) - end - - def ci_text_for_status(status) - if detailed_status?(status) - return status.text - end - - case status - when 'success' - s_('CiStatusText|passed') - when 'success-with-warnings' - s_('CiStatusText|passed') - when 'manual' - s_('CiStatusText|blocked') - when 'scheduled' - s_('CiStatusText|delayed') - else - # All states are already being translated inside the detailed statuses: - # :running => Gitlab::Ci::Status::Running - # :skipped => Gitlab::Ci::Status::Skipped - # :failed => Gitlab::Ci::Status::Failed - # :success => Gitlab::Ci::Status::Success - # :canceled => Gitlab::Ci::Status::Canceled - # The following states are customized above: - # :manual => Gitlab::Ci::Status::Manual - status_translation = "CiStatusText|#{status}" - s_(status_translation) - end - end - - def ci_status_for_statuseable(subject) - status = subject.try(:status) || 'not found' - status.humanize - end - - # rubocop:disable Metrics/CyclomaticComplexity - def ci_icon_for_status(status, size: 16) - if detailed_status?(status) - return sprite_icon(status.icon, size: size) - end - - icon_name = - case status - when 'success' - 'status_success' - when 'success-with-warnings' - 'status_warning' - when 'failed' - 'status_failed' - when 'pending' - 'status_pending' - when 'waiting_for_resource' - 'status_pending' - when 'preparing' - 'status_preparing' - when 'running' - 'status_running' - when 'play' - 'play' - when 'created' - 'status_created' - when 'skipped' - 'status_skipped' - when 'manual' - 'status_manual' - when 'scheduled' - 'status_scheduled' - else - 'status_canceled' - end - - sprite_icon(icon_name, size: size) - end - # rubocop:enable Metrics/CyclomaticComplexity - - def ci_icon_class_for_status(status) - group = detailed_status?(status) ? status.group : status.dasherize - - "ci-status-icon-#{group}" - end - - def pipeline_status_cache_key(pipeline_status) - "pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}" - end - - def render_commit_status(commit, status, ref: nil, tooltip_placement: 'left') - project = commit.project - path = pipelines_project_commit_path(project, commit, ref: ref) - - render_status_with_link( - status, - path, - tooltip_placement: tooltip_placement, - icon_size: 24) - end - - def render_status_with_link(status, path = nil, type: _('pipeline'), tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) - klass = "ci-status-link #{ci_icon_class_for_status(status)} d-inline-flex #{cssclass}" - title = "#{type.titleize}: #{ci_label_for_status(status)}" - data = { toggle: 'tooltip', placement: tooltip_placement, container: container } - - if path - link_to ci_icon_for_status(status, size: icon_size), path, - class: klass, title: title, data: data - else - content_tag :span, ci_icon_for_status(status, size: icon_size), - class: klass, title: title, data: data - end - end - - def detailed_status?(status) - status.respond_to?(:text) && - status.respond_to?(:group) && - status.respond_to?(:label) && - status.respond_to?(:icon) - end -end diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb deleted file mode 100644 index cd0718c1b82..00000000000 --- a/app/helpers/ci_variables_helper.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -module CiVariablesHelper - def ci_variable_protected_by_default? - Gitlab::CurrentSettings.current_application_settings.protected_ci_variables - end - - def create_deploy_token_path(entity, opts = {}) - if entity.is_a?(Group) - create_deploy_token_group_settings_repository_path(entity, opts) - else - # TODO: change this path to 'create_deploy_token_project_settings_ci_cd_path' - # See MR comment for more detail: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27059#note_311585356 - create_deploy_token_project_settings_repository_path(entity, opts) - end - end - - def revoke_deploy_token_path(entity, token) - if entity.is_a?(Group) - revoke_group_deploy_token_path(entity, token) - else - revoke_project_deploy_token_path(entity, token) - end - end - - def ci_variable_protected?(variable, only_key_value) - if variable && !only_key_value - variable.protected - else - ci_variable_protected_by_default? - end - end - - def ci_variable_masked?(variable, only_key_value) - if variable && !only_key_value - variable.masked - else - false - end - end - - def ci_variable_type_options - [ - %w(Variable env_var), - %w(File file) - ] - end - - def ci_variable_maskable_regex - Ci::Maskable::REGEX.inspect.sub('\\A', '^').sub('\\z', '$').sub(/^\//, '').sub(/\/[a-z]*$/, '').gsub('\/', '/') - end -end diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 1204f882707..c85d2a68f14 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true module ClustersHelper - # EE overrides this - def has_multiple_clusters? - false - end - def create_new_cluster_label(provider: nil) case provider when 'aws' @@ -19,6 +14,7 @@ module ClustersHelper def js_clusters_list_data(path = nil) { + ancestor_help_path: help_page_path('user/group/clusters/index', anchor: 'cluster-precedence'), endpoint: path, img_tags: { aws: { path: image_path('illustrations/logos/amazon_eks.svg'), text: s_('ClusterIntegration|Amazon EKS') }, @@ -95,5 +91,3 @@ module ClustersHelper can?(user, :admin_cluster, cluster) end end - -ClustersHelper.prepend_if_ee('EE::ClustersHelper') diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 2a0c2e73dd6..f8490d79427 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -79,7 +79,7 @@ module CommitsHelper # Returns a link formatted as a commit tag link def commit_tag_link(url, text) link_to(url, class: 'badge badge-gray ref-name') do - sprite_icon('tag', size: 12, css_class: 'append-right-5 vertical-align-middle') + "#{text}" + sprite_icon('tag', size: 12, css_class: 'gl-mr-2 vertical-align-middle') + "#{text}" end end @@ -181,15 +181,11 @@ module CommitsHelper end def view_file_button(commit_sha, diff_new_path, project, replaced: false) + path = project_blob_path(project, tree_join(commit_sha, diff_new_path)) title = replaced ? _('View replaced file @ ') : _('View file @ ') - link_to( - project_blob_path(project, - tree_join(commit_sha, diff_new_path)), - class: 'btn view-file js-view-file' - ) do - raw(title) + content_tag(:span, Commit.truncate_sha(commit_sha), - class: 'commit-sha') + link_to(path, class: 'btn') do + raw(title) + content_tag(:span, truncate_sha(commit_sha), class: 'commit-sha') end end diff --git a/app/helpers/cookies_helper.rb b/app/helpers/cookies_helper.rb index 3a7e9987190..938379818de 100644 --- a/app/helpers/cookies_helper.rb +++ b/app/helpers/cookies_helper.rb @@ -1,9 +1,19 @@ # frozen_string_literal: true module CookiesHelper - def set_secure_cookie(key, value, httponly: false, permanent: false) - cookie_jar = permanent ? cookies.permanent : cookies + COOKIE_TYPE_PERMANENT = :permanent + COOKIE_TYPE_ENCRYPTED = :encrypted - cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly } + def set_secure_cookie(key, value, httponly: false, expires: nil, type: nil) + cookie_jar = case type + when COOKIE_TYPE_PERMANENT + cookies.permanent + when COOKIE_TYPE_ENCRYPTED + cookies.encrypted + else + cookies + end + + cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly, expires: expires } end end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index b38feb0fb6c..7bf3795d73a 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -41,7 +41,7 @@ module DashboardHelper if doc_href.present? link_to_doc = link_to(sprite_icon('question', size: 16), doc_href, - class: 'prepend-left-5', title: _('Documentation'), + class: 'gl-ml-2', title: _('Documentation'), target: '_blank', rel: 'noopener noreferrer') concat(link_to_doc) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 4c3c4931387..3b25de521d0 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -135,8 +135,7 @@ module DiffHelper def diff_file_html_data(project, diff_file_path, diff_commit_id) { - blob_diff_path: project_blob_diff_path(project, - tree_join(diff_commit_id, diff_file_path)), + blob_diff_path: project_blob_diff_path(project, tree_join(diff_commit_id, diff_file_path)), view: diff_view } end @@ -175,6 +174,10 @@ module DiffHelper end end + def apply_diff_view_cookie! + set_secure_cookie(:diff_view, params.delete(:view), type: CookiesHelper::COOKIE_TYPE_PERMANENT) if params[:view].present? + end + private def diff_btn(title, name, selected) diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 64c5fae7d96..772a5f79a4d 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -15,7 +15,10 @@ module DropdownsHelper dropdown_output = dropdown_toggle_link(toggle_text, data_attr, options) end - dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.key?(:dropdown_class)}") do + content_tag_options = { class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.key?(:dropdown_class)}" } + content_tag_options[:data] = { qa_selector: "#{options[:dropdown_qa_selector]}" } if options[:dropdown_qa_selector] + + dropdown_output << content_tag(:div, content_tag_options) do output = [] if options.key?(:title) diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 41a255434af..b522a9dfb4f 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -24,7 +24,7 @@ module EnvironmentsHelper def metrics_data(project, environment) metrics_data = {} metrics_data.merge!(project_metrics_data(project)) if project - metrics_data.merge!(environment_metrics_data(environment)) if environment + metrics_data.merge!(environment_metrics_data(environment, project)) if environment metrics_data.merge!(project_and_environment_metrics_data(project, environment)) if project && environment metrics_data.merge!(static_metrics_data) @@ -36,7 +36,8 @@ module EnvironmentsHelper "environment-name": environment.name, "environments-path": project_environments_path(project, format: :json), "environment-id": environment.id, - "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack') + "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'), + "clusters-path": project_clusters_path(project, format: :json) } end @@ -65,11 +66,11 @@ module EnvironmentsHelper } end - def environment_metrics_data(environment) + def environment_metrics_data(environment, project = nil) return {} unless environment { - 'metrics-dashboard-base-path' => environment_metrics_path(environment), + 'metrics-dashboard-base-path' => metrics_dashboard_base_path(environment, project), 'current-environment-name' => environment.name, 'has-metrics' => "#{environment.has_metrics?}", 'prometheus-status' => "#{environment.prometheus_status}", @@ -77,6 +78,17 @@ module EnvironmentsHelper } end + def metrics_dashboard_base_path(environment, project) + # This is needed to support our transition from environment scoped metric paths to project scoped. + if project + path = project_metrics_dashboard_path(project) + + return path if request.path.include?(path) + end + + environment_metrics_path(environment) + end + def project_and_environment_metrics_data(project, environment) return {} unless project && environment @@ -84,14 +96,16 @@ module EnvironmentsHelper 'metrics-endpoint' => additional_metrics_project_environment_path(project, environment, format: :json), 'dashboard-endpoint' => metrics_dashboard_project_environment_path(project, environment, format: :json), 'deployments-endpoint' => project_environment_deployments_path(project, environment, format: :json), - 'alerts-endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json) - + 'alerts-endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json), + 'operations-settings-path' => project_settings_operations_path(project), + 'can-access-operations-settings' => can?(current_user, :admin_operations, project).to_s } end def static_metrics_data { 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'), + 'add-dashboard-documentation-path' => help_page_path('user/project/integrations/prometheus.md', anchor: 'adding-a-new-dashboard-to-your-project'), 'empty-getting-started-svg-path' => image_path('illustrations/monitoring/getting_started.svg'), 'empty-loading-svg-path' => image_path('illustrations/monitoring/loading.svg'), 'empty-no-data-svg-path' => image_path('illustrations/monitoring/no_data.svg'), diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index c1f343edd10..207230fd92e 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -29,7 +29,11 @@ module EventsHelper def event_action_name(event) target = if event.target_type - if event.note? + if event.design? || event.design_note? + 'design' + elsif event.wiki_page? + 'wiki page' + elsif event.note? event.note_target_type else event.target_type.titleize.downcase @@ -58,11 +62,28 @@ module EventsHelper end def event_filter_visible(feature_key) + return designs_visible? if feature_key == :designs return true unless @project @project.feature_available?(feature_key, current_user) end + def designs_visible? + if @project + design_activity_enabled?(@project) + elsif @group + design_activity_enabled?(@group) + elsif @projects + @projects.with_namespace.include_project_feature.any? { |p| design_activity_enabled?(p) } + else + true + end + end + + def design_activity_enabled?(project) + Ability.allowed?(current_user, :read_design_activity, project) + end + def comments_visible? event_filter_visible(:repository) || event_filter_visible(:merge_requests) || @@ -94,6 +115,12 @@ module EventsHelper elsif event.milestone? words << "##{event.target_iid}" if event.target_iid words << "in" + elsif event.design? + words << event.design.to_reference + words << "in" + elsif event.wiki_page? + words << event.target_title + words << "in" elsif event.target prefix = if event.merge_request? @@ -180,10 +207,19 @@ module EventsHelper def event_wiki_title_html(event) capture do - concat content_tag(:span, _('wiki page'), class: "event-target-type append-right-4") + concat content_tag(:span, _('wiki page'), class: "event-target-type gl-mr-2") concat link_to(event.target_title, event_wiki_page_target_url(event), title: event.target_title, - class: 'has-tooltip event-target-link append-right-4') + class: 'has-tooltip event-target-link gl-mr-2') + end + end + + def event_design_title_html(event) + capture do + concat content_tag(:span, _('design'), class: "event-target-type gl-mr-2") + concat link_to(event.design.reference_link_text, design_url(event.design), + title: event.target_title, + class: 'has-tooltip event-design event-target-link gl-mr-2') end end @@ -194,8 +230,8 @@ module EventsHelper def event_note_title_html(event) if event.note_target capture do - concat content_tag(:span, event.note_target_type, class: "event-target-type append-right-4") - concat link_to(event.note_target_reference, event_note_target_url(event), title: event.target_title, class: 'has-tooltip event-target-link append-right-4') + concat content_tag(:span, event.note_target_type, class: "event-target-type gl-mr-2") + concat link_to(event.note_target_reference, event_note_target_url(event), title: event.target_title, class: 'has-tooltip event-target-link gl-mr-2') end else content_tag(:strong, '(deleted)') @@ -214,6 +250,18 @@ module EventsHelper sprite_icon(icon_name, size: size) if icon_name end + DESIGN_ICONS = { + 'created' => 'upload', + 'updated' => 'pencil', + 'destroyed' => ICON_NAMES_BY_EVENT_TYPE['destroyed'], + 'archived' => 'archive' + }.freeze + + def design_event_icon(action, size: 24) + icon_name = DESIGN_ICONS[action] + sprite_icon(icon_name, size: size) if icon_name + end + def icon_for_profile_event(event) if current_path?('users#show') content_tag :div, class: "system-note-image #{event.action_name.parameterize}-icon" do @@ -228,7 +276,9 @@ module EventsHelper def inline_event_icon(event) unless current_path?('users#show') - content_tag :span, class: "system-note-image-inline d-none d-sm-flex append-right-4 #{event.action_name.parameterize}-icon align-self-center" do + content_tag :span, class: "system-note-image-inline d-none d-sm-flex gl-mr-2 #{event.action_name.parameterize}-icon align-self-center" do + next design_event_icon(event.action, size: 14) if event.design? + icon_for_event(event.action_name, size: 14) end end @@ -244,7 +294,7 @@ module EventsHelper private - def design_url(design, opts) + def design_url(design, opts = {}) designs_project_issue_url( design.project, design.issue, diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb index 483b350b99b..38a4f7f1b4b 100644 --- a/app/helpers/export_helper.rb +++ b/app/helpers/export_helper.rb @@ -6,7 +6,7 @@ module ExportHelper [ _('Project and wiki repositories'), _('Project uploads'), - _('Project configuration, including services'), + _('Project configuration, excluding integrations'), _('Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities'), _('LFS objects'), _('Issue Boards'), diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 8a9380f4771..04f34f5a3ae 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -271,6 +271,36 @@ module GitlabRoutingHelper end end + def gitlab_raw_snippet_blob_url(snippet, path, ref = nil) + params = { + snippet_id: snippet, + ref: ref || snippet.repository.root_ref, + path: path + } + + if snippet.is_a?(ProjectSnippet) + project_snippet_blob_raw_url(snippet.project, params) + else + snippet_blob_raw_url(params) + end + end + + def gitlab_raw_snippet_blob_path(blob, ref = nil) + snippet = blob.container + + params = { + snippet_id: snippet, + ref: ref || blob.repository.root_ref, + path: blob.path + } + + if snippet.is_a?(ProjectSnippet) + project_snippet_blob_raw_path(snippet.project, params) + else + snippet_blob_raw_path(params) + end + end + def gitlab_snippet_notes_path(snippet, *args) new_args = snippet_query_params(snippet, *args) snippet_notes_path(snippet, *new_args) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index a6c3c97a873..61c9bd74451 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -176,6 +176,10 @@ module GroupsHelper links << :settings end + if can?(current_user, :read_wiki, @group) + links << :wiki + end + links end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 8a32d3c8a3f..add15cc0d12 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -28,10 +28,12 @@ module IconsHelper end def sprite_icon_path - # SVG Sprites currently don't work across domains, so in the case of a CDN - # we have to set the current path deliberately to prevent addition of asset_host - sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host - ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url) + @sprite_icon_path ||= begin + # SVG Sprites currently don't work across domains, so in the case of a CDN + # we have to set the current path deliberately to prevent addition of asset_host + sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host + ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url) + end end def sprite_file_icons_path @@ -53,6 +55,15 @@ module IconsHelper content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes.join(' ')) end + def loading_icon(container: false, color: 'orange', size: 'sm', css_class: nil) + css_classes = ['gl-spinner', "gl-spinner-#{color}", "gl-spinner-#{size}"] + css_classes << "#{css_class}" unless css_class.blank? + + spinner = content_tag(:span, "", { class: css_classes.join(' '), aria: { label: _('Loading') } }) + + container == true ? content_tag(:div, spinner, { class: 'gl-spinner-container' }) : spinner + end + def external_snippet_icon(name) content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}") end diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index d6145493ba6..93f5ca7258d 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -9,10 +9,12 @@ module IdeHelper "pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'), "promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'), "ci-help-page-path" => help_page_path('ci/quick_start/README'), - "web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'), + "web-ide-help-page-path" => help_page_path('user/project/web_ide/index.md'), "clientside-preview-enabled": Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?.to_s, "render-whitespace-in-code": current_user.render_whitespace_in_code.to_s, "codesandbox-bundler-url": Gitlab::CurrentSettings.web_ide_clientside_preview_bundler_url } end end + +::IdeHelper.prepend_if_ee('::EE::IdeHelper') diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 9122ad5b35a..1ee67211ab0 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -19,7 +19,11 @@ module ImportHelper end def provider_project_link_url(provider_url, full_path) - Gitlab::Utils.append_path(provider_url, full_path) + if Gitlab::Utils.parse_url(full_path)&.absolute? + full_path + else + Gitlab::Utils.append_path(provider_url, full_path) + end end def import_will_timeout_message(_ci_cd_only) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index a848c814742..dccb89eec79 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -367,15 +367,6 @@ module IssuablesHelper end end - def issuable_close_reopen_button_method(issuable) - case issuable - when Issue - '' - when MergeRequest - 'put' - end - end - def issuable_author_is_current_user(issuable) issuable.author == current_user end @@ -394,6 +385,14 @@ module IssuablesHelper end end + def issuable_squash_option?(issuable, project) + if issuable.persisted? + issuable.squash + else + project.squash_enabled_by_default? + end + end + private def sidebar_gutter_collapsed? diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 244b97c7196..61fe075303c 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -41,7 +41,7 @@ module IssuesHelper end def confidential_icon(issue) - icon('eye-slash') if issue.confidential? + sprite_icon('eye-slash', size: 16, css_class: 'gl-vertical-align-text-bottom') if issue.confidential? end def award_user_list(awards, current_user, limit: 10) @@ -132,7 +132,10 @@ module IssuesHelper end def show_moved_service_desk_issue_warning?(issue) - false + return false unless issue.moved_from + return false unless issue.from_service_desk? + + issue.moved_from.project.service_desk_enabled? && !issue.project.service_desk_enabled? end end diff --git a/app/helpers/jobs_helper.rb b/app/helpers/jobs_helper.rb deleted file mode 100644 index 46edba261dd..00000000000 --- a/app/helpers/jobs_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module JobsHelper - def jobs_data - { - "endpoint" => project_job_path(@project, @build, format: :json), - "project_path" => @project.full_path, - "deployment_help_url" => help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting-failed-deployment-jobs'), - "runner_help_url" => help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'), - "runner_settings_url" => project_runners_path(@build.project, anchor: 'js-runners-settings'), - "variables_settings_url" => project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'), - "page_path" => project_job_path(@project, @build), - "build_status" => @build.status, - "build_stage" => @build.stage, - "log_state" => '', - "build_options" => javascript_build_options - } - end -end diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 7ab2b33de8c..ed8931fe0f2 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -244,7 +244,6 @@ module MarkupHelper content_tag :button, type: 'button', class: 'toolbar-btn js-md has-tooltip', - tabindex: -1, data: data, title: options[:title], aria: { label: options[:title] } do diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 31995c27fac..d66f67fbb60 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -48,6 +48,14 @@ module MembersHelper "#{request.path}?#{options.to_param}" end + def member_path(member) + if member.is_a?(GroupMember) + group_group_member_path(member.source, member) + else + project_project_member_path(member.source, member) + end + end + private def source_text(member) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 7940ec1162b..caf39741543 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -118,7 +118,7 @@ module MergeRequestsHelper auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS, should_remove_source_branch: true, sha: merge_request.diff_head_sha, - squash: merge_request.squash + squash: merge_request.squash_on_merge? } end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index b9f8d81bc4e..81451e398f2 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -56,45 +56,6 @@ module NamespacesHelper namespaces_options(selected, options) end - def namespace_storage_alert(namespace) - return {} if current_user.nil? - - payload = Namespaces::CheckStorageSizeService.new(namespace, current_user).execute.payload - - return {} if payload.empty? - - alert_level = payload[:alert_level] - root_namespace = payload[:root_namespace] - - return {} if cookies["hide_storage_limit_alert_#{root_namespace.id}_#{alert_level}"] == 'true' - - payload - end - - def namespace_storage_alert_style(alert_level) - if alert_level == :error || alert_level == :alert - 'danger' - else - alert_level.to_s - end - end - - def namespace_storage_alert_icon(alert_level) - if alert_level == :error || alert_level == :alert - 'error' - elsif alert_level == :info - 'information-o' - else - alert_level.to_s - end - end - - def namespace_storage_usage_link(namespace) - # The usage quota page is only available in EE. This will be changed in - # the future, see https://gitlab.com/gitlab-org/gitlab/-/issues/220042. - nil - end - private # Many importers create a temporary Group, so use the real diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 9ea0b9cb584..d849ed9d076 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -27,7 +27,7 @@ module NavHelper end elsif current_path?('jobs#show') %w[page-gutter build-sidebar right-sidebar-expanded] - elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access', 'destroy') + elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access', 'destroy', 'diff') %w[page-gutter wiki-sidebar right-sidebar-expanded] else [] diff --git a/app/helpers/notify_helper.rb b/app/helpers/notify_helper.rb new file mode 100644 index 00000000000..fb68029928c --- /dev/null +++ b/app/helpers/notify_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module NotifyHelper + def merge_request_reference_link(entity, *args) + link_to(entity.to_reference, merge_request_url(entity, *args)) + end + + def issue_reference_link(entity, *args) + link_to(entity.to_reference, issue_url(entity, *args)) + end +end diff --git a/app/helpers/onboarding_experiment_helper.rb b/app/helpers/onboarding_experiment_helper.rb deleted file mode 100644 index 138fc60479d..00000000000 --- a/app/helpers/onboarding_experiment_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module OnboardingExperimentHelper - def allow_access_to_onboarding? - ::Gitlab.dev_env_or_com? && Feature.enabled?(:user_onboarding) - end -end - -OnboardingExperimentHelper.prepend_if_ee('EE::OnboardingExperimentHelper') diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb new file mode 100644 index 00000000000..3444773fe88 --- /dev/null +++ b/app/helpers/operations_helper.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module OperationsHelper + include Gitlab::Utils::StrongMemoize + + def prometheus_service + strong_memoize(:prometheus_service) do + @project.find_or_initialize_service(::PrometheusService.to_param) + end + end + + def alerts_service + strong_memoize(:alerts_service) do + @project.find_or_initialize_service(::AlertsService.to_param) + end + end + + def alerts_settings_data(disabled: false) + { + 'prometheus_activated' => prometheus_service.manual_configuration?.to_s, + 'activated' => alerts_service.activated?.to_s, + 'prometheus_form_path' => scoped_integration_path(prometheus_service), + 'form_path' => scoped_integration_path(alerts_service), + 'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(@project), + 'prometheus_authorization_key' => @project.alerting_setting&.token, + 'prometheus_api_url' => prometheus_service.api_url, + 'authorization_key' => alerts_service.token, + 'prometheus_url' => notify_project_prometheus_alerts_url(@project, format: :json), + 'url' => alerts_service.url, + 'alerts_setup_url' => help_page_path('user/project/integrations/generic_alerts.md', anchor: 'setting-up-generic-alerts'), + 'alerts_usage_url' => project_alert_management_index_path(@project), + 'disabled' => disabled.to_s + } + end + + def operations_settings_data + setting = project_incident_management_setting + templates = setting.available_issue_templates.map { |t| { key: t.key, name: t.name } } + + { + operations_settings_endpoint: project_settings_operations_path(@project), + templates: templates.to_json, + create_issue: setting.create_issue.to_s, + issue_template_key: setting.issue_template_key.to_s, + send_email: setting.send_email.to_s, + pagerduty_active: setting.pagerduty_active.to_s, + pagerduty_token: setting.pagerduty_token.to_s, + pagerduty_webhook_url: project_incidents_pagerduty_url(@project, token: setting.pagerduty_token), + pagerduty_reset_key_path: reset_pagerduty_token_project_settings_operations_path(@project) + } + end +end + +OperationsHelper.prepend_if_ee('EE::OperationsHelper') diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb deleted file mode 100644 index 0e166106b32..00000000000 --- a/app/helpers/pipeline_schedules_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module PipelineSchedulesHelper - def timezone_data - ActiveSupport::TimeZone.all.map do |timezone| - { - name: timezone.name, - offset: timezone.now.utc_offset, - identifier: timezone.tzinfo.identifier - } - end - end -end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 7a0462e1b2c..271359fcfd1 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -70,7 +70,10 @@ module PreferencesHelper end def language_choices - Gitlab::I18n::AVAILABLE_LANGUAGES.map(&:reverse).sort + options_for_select( + Gitlab::I18n::AVAILABLE_LANGUAGES.map(&:reverse).sort, + current_user.preferred_language + ) end private diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb index bc585899591..d6e8e738a1c 100644 --- a/app/helpers/projects/alert_management_helper.rb +++ b/app/helpers/projects/alert_management_helper.rb @@ -4,10 +4,11 @@ module Projects::AlertManagementHelper def alert_management_data(current_user, project) { 'project-path' => project.full_path, - 'enable-alert-management-path' => edit_project_service_path(project, AlertsService), + 'enable-alert-management-path' => project_settings_operations_path(project, anchor: 'js-alert-management-settings'), + 'populating-alerts-help-url' => help_page_url('user/project/operations/alert_management.html', anchor: 'enable-alert-management'), 'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'), - 'user-can-enable-alert-management' => can?(current_user, :admin_project, project).to_s, - 'alert-management-enabled' => (!!project.alerts_service_activated?).to_s + 'user-can-enable-alert-management' => can?(current_user, :admin_operations, project).to_s, + 'alert-management-enabled' => alert_management_enabled?(project).to_s } end @@ -15,7 +16,16 @@ module Projects::AlertManagementHelper { 'alert-id' => alert_id, 'project-path' => project.full_path, + 'project-id' => project.id, 'project-issues-path' => project_issues_path(project) } end + + private + + def alert_management_enabled?(project) + !!(project.alerts_service_activated? || project.prometheus_service_active?) + end end + +Projects::AlertManagementHelper.prepend_if_ee('EE::Projects::AlertManagementHelper') diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index bda9a69d71f..840e3ef9daa 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -180,7 +180,7 @@ module ProjectsHelper end def link_to_autodeploy_doc - link_to _('About auto deploy'), help_page_path('autodevops/index.md#auto-deploy'), target: '_blank' + link_to _('About auto deploy'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank' end def autodeploy_flash_notice(branch_name) @@ -384,9 +384,12 @@ module ProjectsHelper end def project_license_name(project) - project.repository.license&.name + key = "project:#{project.id}:license_name" + + Gitlab::SafeRequestStore.fetch(key) { project.repository.license&.name } rescue GRPC::Unavailable, GRPC::DeadlineExceeded, Gitlab::Git::CommandError => e Gitlab::ErrorTracking.track_exception(e) + Gitlab::SafeRequestStore[key] = nil nil end @@ -397,7 +400,7 @@ module ProjectsHelper nav_tabs = [:home] unless project.empty_repo? - nav_tabs << [:files, :commits, :network, :graphs, :forks] if can?(current_user, :download_code, project) + nav_tabs += [:files, :commits, :network, :graphs, :forks] if can?(current_user, :download_code, project) nav_tabs << :releases if can?(current_user, :read_release, project) end @@ -418,30 +421,30 @@ module ProjectsHelper nav_tabs << :operations end - if can?(current_user, :read_cycle_analytics, project) - nav_tabs << :cycle_analytics - end - tab_ability_map.each do |tab, ability| if can?(current_user, ability, project) nav_tabs << tab end end - nav_tabs << external_nav_tabs(project) + apply_external_nav_tabs(nav_tabs, project) - nav_tabs.flatten + nav_tabs end - def external_nav_tabs(project) - [].tap do |tabs| - tabs << :external_issue_tracker if project.external_issue_tracker - tabs << :external_wiki if project.external_wiki + def apply_external_nav_tabs(nav_tabs, project) + nav_tabs << :external_issue_tracker if project.external_issue_tracker + nav_tabs << :external_wiki if project.external_wiki + + if project.has_confluence? + nav_tabs.delete(:wiki) + nav_tabs << :confluence end end def tab_ability_map { + cycle_analytics: :read_cycle_analytics, environments: :read_environment, metrics_dashboards: :metrics_dashboard, milestones: :read_milestone, @@ -565,7 +568,7 @@ module ProjectsHelper end def project_child_container_class(view_path) - view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}" + view_path == "projects/issues/issues" ? "gl-mt-3" : "project-show-#{view_path}" end def project_issues(project) @@ -729,10 +732,6 @@ module ProjectsHelper !project.repository.gitlab_ci_yml end - def vue_file_list_enabled? - Feature.enabled?(:vue_file_list, @project, default_enabled: true) - end - def native_code_navigation_enabled?(project) Feature.enabled?(:code_navigation, project, default_enabled: true) end diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb index 1238567a4ed..a3d944c64cc 100644 --- a/app/helpers/releases_helper.rb +++ b/app/helpers/releases_helper.rb @@ -18,21 +18,40 @@ module ReleasesHelper illustration_path: illustration, documentation_path: help_page }.tap do |data| - data[:new_release_path] = new_project_tag_path(@project) if can?(current_user, :create_release, @project) + if can?(current_user, :create_release, @project) + data[:new_release_path] = if Feature.enabled?(:new_release_page, @project) + new_project_release_path(@project) + else + new_project_tag_path(@project) + end + end end end def data_for_edit_release_page + new_edit_pages_shared_data.merge( + tag_name: @release.tag, + releases_page_path: project_releases_path(@project, anchor: @release.tag) + ) + end + + def data_for_new_release_page + new_edit_pages_shared_data.merge( + default_branch: @project.default_branch + ) + end + + private + + def new_edit_pages_shared_data { project_id: @project.id, - tag_name: @release.tag, markdown_preview_path: preview_markdown_path(@project), markdown_docs_path: help_page_path('user/markdown'), - releases_page_path: project_releases_path(@project, anchor: @release.tag), update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'), release_assets_docs_path: help_page(anchor: 'release-assets'), manage_milestones_path: project_milestones_path(@project), - new_milestone_path: new_project_milestone_url(@project) + new_milestone_path: new_project_milestone_path(@project) } end end diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb deleted file mode 100644 index d871aaa9c86..00000000000 --- a/app/helpers/runners_helper.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module RunnersHelper - def runner_status_icon(runner) - status = runner.status - case status - when :not_connected - content_tag :i, nil, - class: "fa fa-warning", - title: "New runner. Has not connected yet" - - when :online, :offline, :paused - content_tag :i, nil, - class: "fa fa-circle runner-status-#{status}", - title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago" - end - end - - def runner_link(runner) - display_name = truncate(runner.display_name, length: 15) - id = "\##{runner.id}" - - if current_user && current_user.admin - link_to admin_runner_path(runner) do - display_name + id - end - else - display_name + id - end - end - - # Due to inability of performing sorting of runners by cached "contacted_at" values we have to show uncached values if sorting by "contacted_asc" is requested. - # Please refer to the following issue for more details: https://gitlab.com/gitlab-org/gitlab-foss/issues/55920 - def runner_contacted_at(runner) - if params[:sort] == 'contacted_asc' - runner.uncached_contacted_at - else - runner.contacted_at - end - end -end - -RunnersHelper.prepend_if_ee('EE::RunnersHelper') diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 4e3b6aad8cc..1b9876b9a6a 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -3,6 +3,28 @@ module SearchHelper SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze + def search_autocomplete_opts(term) + return unless current_user + + resources_results = [ + groups_autocomplete(term), + projects_autocomplete(term) + ].flatten + + search_pattern = Regexp.new(Regexp.escape(term), "i") + + generic_results = project_autocomplete + default_autocomplete + help_autocomplete + generic_results.concat(default_autocomplete_admin) if current_user.admin? + generic_results.select! { |result| result[:label] =~ search_pattern } + + [ + resources_results, + generic_results + ].flatten.uniq do |item| + item[:label] + end + end + def search_entries_info(collection, scope, term) return if collection.to_a.empty? @@ -62,7 +84,7 @@ module SearchHelper }).html_safe end - # Overriden in EE + # Overridden in EE def search_blob_title(project, path) path end @@ -73,6 +95,91 @@ module SearchHelper private + # Autocomplete results for various settings pages + def default_autocomplete + [ + { category: "Settings", label: _("User settings"), url: profile_path }, + { category: "Settings", label: _("SSH Keys"), url: profile_keys_path }, + { category: "Settings", label: _("Dashboard"), url: root_path } + ] + end + + # Autocomplete results for settings pages, for admins + def default_autocomplete_admin + [ + { category: "Settings", label: _("Admin Section"), url: admin_root_path } + ] + end + + # Autocomplete results for internal help pages + def help_autocomplete + [ + { category: "Help", label: _("API Help"), url: help_page_path("api/README") }, + { category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") }, + { category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") }, + { category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") }, + { category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") }, + { category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/README") }, + { category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") }, + { category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") }, + { category: "Help", label: _("Workflow Help"), url: help_page_path("workflow/README") } + ] + end + + # Autocomplete results for the current project, if it's defined + def project_autocomplete + if @project && @project.repository.root_ref + ref = @ref || @project.repository.root_ref + + [ + { category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) }, + { category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) }, + { category: "In this project", label: _("Network"), url: project_network_path(@project, ref) }, + { category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) }, + { category: "In this project", label: _("Issues"), url: project_issues_path(@project) }, + { category: "In this project", label: _("Merge Requests"), url: project_merge_requests_path(@project) }, + { category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) }, + { category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) }, + { category: "In this project", label: _("Members"), url: project_project_members_path(@project) }, + { category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) } + ] + else + [] + end + end + + # Autocomplete results for the current user's groups + # rubocop: disable CodeReuse/ActiveRecord + def groups_autocomplete(term, limit = 5) + current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group| + { + category: "Groups", + id: group.id, + label: "#{search_result_sanitize(group.full_name)}", + url: group_path(group), + avatar_url: group.avatar_url || '' + } + end + end + # rubocop: enable CodeReuse/ActiveRecord + + # Autocomplete results for the current user's projects + # rubocop: disable CodeReuse/ActiveRecord + def projects_autocomplete(term, limit = 5) + current_user.authorized_projects.order_id_desc.search_by_title(term) + .sorted_by_stars_desc.non_archived.limit(limit).map do |p| + { + category: "Projects", + id: p.id, + value: "#{search_result_sanitize(p.name)}", + label: "#{search_result_sanitize(p.full_name)}", + url: project_path(p), + avatar_url: p.avatar_url || '' + } + end + end + # rubocop: enable CodeReuse/ActiveRecord + def search_result_sanitize(str) Sanitize.clean(str) end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index fe839b92ba6..1f9cce80bed 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -4,25 +4,29 @@ module ServicesHelper def service_event_description(event) case event when "push", "push_events" - "Event will be triggered by a push to the repository" + s_("ProjectService|Event will be triggered by a push to the repository") when "tag_push", "tag_push_events" - "Event will be triggered when a new tag is pushed to the repository" + s_("ProjectService|Event will be triggered when a new tag is pushed to the repository") when "note", "note_events" - "Event will be triggered when someone adds a comment" + s_("ProjectService|Event will be triggered when someone adds a comment") when "confidential_note", "confidential_note_events" - "Event will be triggered when someone adds a comment on a confidential issue" + s_("ProjectService|Event will be triggered when someone adds a comment on a confidential issue") when "issue", "issue_events" - "Event will be triggered when an issue is created/updated/closed" - when "confidential_issue", "confidential_issues_events" - "Event will be triggered when a confidential issue is created/updated/closed" + s_("ProjectService|Event will be triggered when an issue is created/updated/closed") + when "confidential_issue", "confidential_issue_events" + s_("ProjectService|Event will be triggered when a confidential issue is created/updated/closed") when "merge_request", "merge_request_events" - "Event will be triggered when a merge request is created/updated/merged" + s_("ProjectService|Event will be triggered when a merge request is created/updated/merged") when "pipeline", "pipeline_events" - "Event will be triggered when a pipeline status changes" + s_("ProjectService|Event will be triggered when a pipeline status changes") when "wiki_page", "wiki_page_events" - "Event will be triggered when a wiki page is created/updated" + s_("ProjectService|Event will be triggered when a wiki page is created/updated") when "commit", "commit_events" - "Event will be triggered when a commit is created/updated" + s_("ProjectService|Event will be triggered when a commit is created/updated") + when "deployment" + s_("ProjectService|Event will be triggered when a deployment finishes") + when "alert" + s_("ProjectService|Event will be triggered when a new, unique alert is recorded") end end @@ -44,15 +48,8 @@ module ServicesHelper end end - def event_action_description(action) - case action - when "comment" - s_("ProjectService|Comment will be posted on each event") - end - end - - def service_save_button - button_tag(class: 'btn btn-success', type: 'submit', data: { qa_selector: 'save_changes_button' }) do + def service_save_button(disabled: false) + button_tag(class: 'btn btn-success', type: 'submit', disabled: disabled, data: { qa_selector: 'save_changes_button' }) do icon('spinner spin', class: 'hidden js-btn-spinner') + content_tag(:span, 'Save changes', class: 'js-btn-label') end @@ -90,7 +87,7 @@ module ServicesHelper def scoped_test_integration_path(integration) if @project.present? - test_project_settings_integration_path(@project, integration) + test_project_service_path(@project, integration) elsif @group.present? test_group_settings_integration_path(@group, integration) else @@ -99,25 +96,45 @@ module ServicesHelper end def integration_form_refactor? - Feature.enabled?(:integration_form_refactor, @project) + Feature.enabled?(:integration_form_refactor, @project, default_enabled: true) end - def trigger_events_for_service + def integration_form_data(integration) + { + id: integration.id, + show_active: integration.show_active_box?.to_s, + activated: (integration.active || integration.new_record?).to_s, + type: integration.to_param, + merge_request_events: integration.merge_requests_events.to_s, + commit_events: integration.commit_events.to_s, + enable_comments: integration.comment_on_event_enabled.to_s, + comment_detail: integration.comment_detail, + trigger_events: trigger_events_for_service(integration), + fields: fields_for_service(integration), + inherit_from_id: integration.inherit_from_id + } + end + + def trigger_events_for_service(integration) return [] unless integration_form_refactor? - ServiceEventSerializer.new(service: @service).represent(@service.configurable_events).to_json + ServiceEventSerializer.new(service: integration).represent(integration.configurable_events).to_json end - def fields_for_service + def fields_for_service(integration) return [] unless integration_form_refactor? - ServiceFieldSerializer.new(service: @service).represent(@service.global_fields).to_json + ServiceFieldSerializer.new(service: integration).represent(integration.global_fields).to_json end - def show_service_trigger_events? - return false if @service.is_a?(JiraService) || integration_form_refactor? + def show_service_trigger_events?(integration) + return false if integration.is_a?(JiraService) || integration_form_refactor? + + integration.configurable_events.present? + end - @service.configurable_events.present? + def project_jira_issues_integration? + false end extend self diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb index ce810433a3a..13bf9c92d52 100644 --- a/app/helpers/storage_helper.rb +++ b/app/helpers/storage_helper.rb @@ -14,9 +14,10 @@ module StorageHelper counter_repositories: storage_counter(statistics.repository_size), counter_wikis: storage_counter(statistics.wiki_size), counter_build_artifacts: storage_counter(statistics.build_artifacts_size), - counter_lfs_objects: storage_counter(statistics.lfs_objects_size) + counter_lfs_objects: storage_counter(statistics.lfs_objects_size), + counter_snippets: storage_counter(statistics.snippets_size) } - _("Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / LFS: %{counter_lfs_objects}") % counters + _("Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / LFS: %{counter_lfs_objects} / Snippets: %{counter_snippets}") % counters end end diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index 7baa615d36f..6ea6a33ba5e 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -31,7 +31,9 @@ module SystemNoteHelper 'designs_added' => 'doc-image', 'designs_modified' => 'doc-image', 'designs_removed' => 'doc-image', - 'designs_discussion_added' => 'doc-image' + 'designs_discussion_added' => 'doc-image', + 'status' => 'status', + 'alert_issue_added' => 'issues' }.freeze def system_note_icon_name(note) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 2b4f2f11d1e..b9a6cab07a8 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -22,6 +22,7 @@ module TodosHelper when Todo::APPROVAL_REQUIRED then "set #{todo_action_subject(todo)} as an approver for" when Todo::UNMERGEABLE then 'Could not merge' when Todo::DIRECTLY_ADDRESSED then "directly addressed #{todo_action_subject(todo)} on" + when Todo::MERGE_TRAIN_REMOVED then "Removed from Merge Train:" end end @@ -97,11 +98,13 @@ module TodosHelper 'mr' when Issue 'issue' + when AlertManagement::Alert + 'alert' end content_tag(:span, nil, class: 'target-status') do - content_tag(:span, nil, class: "status-box status-box-#{type}-#{todo.target.state.dasherize}") do - todo.target.state.capitalize + content_tag(:span, nil, class: "status-box status-box-#{type}-#{todo.target.state.to_s.dasherize}") do + todo.target.state.to_s.capitalize end end end @@ -195,6 +198,10 @@ module TodosHelper "· #{content}".html_safe end + def todo_author_display?(todo) + !todo.build_failed? && !todo.unmergeable? + end + private def todos_design_path(todo, path_options) @@ -214,7 +221,14 @@ module TodosHelper end def show_todo_state?(todo) - (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state) + case todo.target + when MergeRequest, Issue + %w(closed merged).include?(todo.target.state) + when AlertManagement::Alert + %i(resolved).include?(todo.target.state) + else + false + end end def todo_group_options diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 4dc00581703..90a5b6da4c7 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -191,8 +191,10 @@ module TreeHelper def vue_file_list_data(project, ref) { + can_push_code: current_user&.can?(:push_code, project) && "true", project_path: project.full_path, project_short_path: project.path, + fork_path: current_user&.fork_of(project)&.full_path, ref: ref, escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref), full_name: project.name_with_namespace diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 3c983606b73..cf2d2d178e1 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -3,6 +3,30 @@ module WikiHelper include API::Helpers::RelatedResourcesHelpers + def wiki_page_title(page, action = nil) + titles = [_('Wiki')] + + if page.persisted? + titles << page.human_title + breadcrumb_title(page.human_title) + wiki_breadcrumb_dropdown_links(page.slug) + end + + titles << action if action + page_title(*titles.reverse) + add_to_breadcrumbs(_('Wiki'), wiki_path(page.wiki)) + end + + def link_to_wiki_page(page, **options) + link_to page.human_title, wiki_page_path(page.wiki, page), **options + end + + def wiki_sidebar_toggle_button + content_tag :button, class: 'btn btn-default sidebar-toggle js-sidebar-wiki-toggle', role: 'button', type: 'button' do + sprite_icon('chevron-double-lg-left') + end + end + # Produces a pure text breadcrumb for a given page. # # page_slug - The slug of a WikiPage object. @@ -71,10 +95,13 @@ module WikiHelper def wiki_empty_state_messages(wiki) case wiki.container when Project + writable_body = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on.") + writable_body += s_("WikiEmpty| Have a Confluence wiki already? Use that instead.") if show_enable_confluence_integration?(wiki.container) + { writable: { title: s_('WikiEmpty|The wiki lets you write documentation for your project'), - body: s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on.") + body: writable_body }, issuable: { title: s_('WikiEmpty|This project has no wiki pages'), @@ -104,4 +131,19 @@ module WikiHelper raise NotImplementedError, "Unknown wiki container type #{wiki.container.class.name}" end end + + def wiki_page_tracking_context(page) + { + 'wiki-format' => page.format, + 'wiki-title-size' => page.title.bytesize, + 'wiki-content-size' => page.raw_content.bytesize, + 'wiki-directory-nest-level' => page.path.scan('/').count + } + end + + def show_enable_confluence_integration?(container) + container.is_a?(Project) && + current_user&.can?(:admin_project, container) && + !container.has_confluence? + end end |