diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 0bda1b60623..4d8d5545a14 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -604,10 +604,6 @@ RSpec/EmptyLineAfterFinalLetItBe: - ee/spec/services/incident_management/incidents/upload_metric_service_spec.rb - ee/spec/services/incident_management/oncall_rotations/edit_service_spec.rb - ee/spec/services/merge_request_approval_settings/update_service_spec.rb - - ee/spec/services/merge_trains/check_status_service_spec.rb - - ee/spec/services/merge_trains/create_pipeline_service_spec.rb - - ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb - - ee/spec/services/merge_trains/refresh_service_spec.rb - ee/spec/services/personal_access_tokens/create_service_audit_log_spec.rb - ee/spec/services/personal_access_tokens/groups/update_lifetime_service_spec.rb - ee/spec/services/projects/after_rename_service_spec.rb @@ -779,9 +775,6 @@ RSpec/EmptyLineAfterFinalLetItBe: - spec/lib/gitlab/gitaly_client/operation_service_spec.rb - spec/lib/gitlab/gl_repository/repo_type_spec.rb - spec/lib/gitlab/group_search_results_spec.rb - - spec/lib/gitlab/hook_data/issue_builder_spec.rb - - spec/lib/gitlab/hook_data/merge_request_builder_spec.rb - - spec/lib/gitlab/hook_data/release_builder_spec.rb - spec/lib/gitlab/json_cache_spec.rb - spec/lib/gitlab/language_detection_spec.rb - spec/lib/gitlab/project_search_results_spec.rb @@ -1010,25 +1003,12 @@ RSpec/EmptyLineAfterFinalLetItBe: - spec/services/feature_flags/enable_service_spec.rb - spec/services/feature_flags/update_service_spec.rb - spec/services/git/branch_push_service_spec.rb - - spec/services/groups/auto_devops_service_spec.rb - - spec/services/groups/group_links/update_service_spec.rb - - spec/services/groups/transfer_service_spec.rb - - spec/services/groups/update_shared_runners_service_spec.rb - - spec/services/ide/base_config_service_spec.rb - - spec/services/ide/schemas_config_service_spec.rb - - spec/services/ide/terminal_config_service_spec.rb - spec/services/import/bitbucket_server_service_spec.rb - spec/services/incident_management/incidents/create_service_spec.rb - spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb - spec/services/incident_management/pager_duty/process_webhook_service_spec.rb - spec/services/integrations/test/project_service_spec.rb - spec/services/issuable/bulk_update_service_spec.rb - - spec/services/issues/build_service_spec.rb - - spec/services/issues/clone_service_spec.rb - - spec/services/issues/create_service_spec.rb - - spec/services/issues/export_csv_service_spec.rb - - spec/services/issues/move_service_spec.rb - - spec/services/issues/related_branches_service_spec.rb - spec/services/jira_connect/sync_service_spec.rb - spec/services/jira_import/start_import_service_spec.rb - spec/services/jira_import/users_importer_spec.rb diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js index ca019bc4178..66e8d982113 100644 --- a/app/assets/javascripts/branches/divergence_graph.js +++ b/app/assets/javascripts/branches/divergence_graph.js @@ -4,13 +4,13 @@ import axios from '../lib/utils/axios_utils'; import { __ } from '../locale'; import DivergenceGraph from './components/divergence_graph.vue'; -export function createGraphVueApp(el, data, maxCommits) { +export function createGraphVueApp(el, data, maxCommits, defaultBranch) { return new Vue({ el, render(h) { return h(DivergenceGraph, { props: { - defaultBranch: 'master', + defaultBranch, distance: data.distance ? parseInt(data.distance, 10) : null, aheadCount: parseInt(data.ahead, 10), behindCount: parseInt(data.behind, 10), @@ -21,7 +21,7 @@ export function createGraphVueApp(el, data, maxCommits) { }); } -export default (endpoint) => { +export default (endpoint, defaultBranch) => { const names = [...document.querySelectorAll('.js-branch-item')].map( ({ dataset }) => dataset.name, ); @@ -47,7 +47,7 @@ export default (endpoint) => { if (!el) return; - createGraphVueApp(el, val, maxCommits); + createGraphVueApp(el, val, maxCommits, defaultBranch); }); }) .catch(() => diff --git a/app/assets/javascripts/delete_label_modal.js b/app/assets/javascripts/delete_label_modal.js new file mode 100644 index 00000000000..cf7c9e7734f --- /dev/null +++ b/app/assets/javascripts/delete_label_modal.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue'; + +const mountDeleteLabelModal = (optionalProps) => + new Vue({ + render(h) { + return h(DeleteLabelModal, { + props: { + selector: '.js-delete-label-modal-button', + ...optionalProps, + }, + }); + }, + }).$mount(); + +export default (optionalProps = {}) => mountDeleteLabelModal(optionalProps); diff --git a/app/assets/javascripts/jira_connect/api.js b/app/assets/javascripts/jira_connect/api.js index d78aba0a3f7..8da2ca73f9a 100644 --- a/app/assets/javascripts/jira_connect/api.js +++ b/app/assets/javascripts/jira_connect/api.js @@ -39,11 +39,12 @@ export const removeSubscription = async (removePath) => { }); }; -export const fetchGroups = async (groupsPath, { page, perPage }) => { +export const fetchGroups = async (groupsPath, { page, perPage, search }) => { return axios.get(groupsPath, { params: { page, per_page: perPage, + search, }, }); }; diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/components/groups_list.vue index 69f2903388c..275ff820419 100644 --- a/app/assets/javascripts/jira_connect/components/groups_list.vue +++ b/app/assets/javascripts/jira_connect/components/groups_list.vue @@ -1,5 +1,5 @@ - + {{ errorMessage }} - - - - - {{ s__('Integrations|No available namespaces.') }} - - {{ - s__('Integrations|You must have owner or maintainer permissions to link namespaces.') - }} - - - - - + - - - - - + + + {{ s__('Integrations|No available namespaces.') }} + + {{ s__('Integrations|You must have owner or maintainer permissions to link namespaces.') }} + + + + + + + + + diff --git a/app/assets/javascripts/jira_connect/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/components/groups_list_item.vue index b8959a2a505..9c5722c44c7 100644 --- a/app/assets/javascripts/jira_connect/components/groups_list_item.vue +++ b/app/assets/javascripts/jira_connect/components/groups_list_item.vue @@ -21,6 +21,11 @@ export default { type: Object, required: true, }, + disabled: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -60,7 +65,7 @@ export default { - + @@ -83,11 +88,13 @@ export default { {{ __('Link') }} + {{ __('Link') }} + diff --git a/app/assets/javascripts/pages/groups/labels/index/index.js b/app/assets/javascripts/pages/groups/labels/index/index.js index 87d522d7654..95c2c7cd7d0 100644 --- a/app/assets/javascripts/pages/groups/labels/index/index.js +++ b/app/assets/javascripts/pages/groups/labels/index/index.js @@ -1,3 +1,5 @@ +import initDeleteLabelModal from '~/delete_label_modal'; import initLabels from '~/init_labels'; initLabels(); +initDeleteLabelModal(); diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index 52f9cbea370..45b1cfecc5a 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -5,5 +5,10 @@ import initDiverganceGraph from '~/branches/divergence_graph'; AjaxLoadingSpinner.init(); new DeleteModal(); // eslint-disable-line no-new -initDiverganceGraph(document.querySelector('.js-branch-list').dataset.divergingCountsEndpoint); + +const { divergingCountsEndpoint, defaultBranch } = document.querySelector( + '.js-branch-list', +).dataset; + +initDiverganceGraph(divergingCountsEndpoint, defaultBranch); BranchSortDropdown(); diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 9f782c07101..94ab0d64de4 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import initDeleteLabelModal from '~/delete_label_modal'; import initLabels from '~/init_labels'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import Translate from '~/vue_shared/translate'; @@ -9,6 +10,7 @@ Vue.use(Translate); const initLabelIndex = () => { initLabels(); + initDeleteLabelModal(); const onRequestFinished = ({ labelUrl, successful }) => { const button = document.querySelector( diff --git a/app/assets/javascripts/vue_shared/components/delete_label_modal.vue b/app/assets/javascripts/vue_shared/components/delete_label_modal.vue new file mode 100644 index 00000000000..1ff0938d086 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/delete_label_modal.vue @@ -0,0 +1,81 @@ + + + + + + + + {{ labelName }} + + + + + + {{ content }} + + + + {{ __('Cancel') }} + {{ __('Delete label') }} + + + diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss index eb2dd6e578e..346cdbb9a8d 100644 --- a/app/assets/stylesheets/page_bundles/jira_connect.scss +++ b/app/assets/stylesheets/page_bundles/jira_connect.scss @@ -4,7 +4,6 @@ @import 'bootstrap-vue/src/index'; @import '@gitlab/ui/src/scss/utilities'; -@import '@gitlab/ui/src/components/base/alert/alert'; // We should only import styles that we actually use. @import '@gitlab/ui/src/components/base/alert/alert'; @@ -16,8 +15,8 @@ @import '@gitlab/ui/src/components/base/loading_icon/loading_icon'; @import '@gitlab/ui/src/components/base/modal/modal'; @import '@gitlab/ui/src/components/base/pagination/pagination'; -@import '@gitlab/ui/src/components/base/tabs/tabs/tabs'; @import '@gitlab/ui/src/components/base/tooltip/tooltip'; +@import '@gitlab/ui/src/components/base/search_box_by_type/search_box_by_type'; $atlaskit-border-color: #dfe1e6; $header-height: 40px; diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb new file mode 100644 index 00000000000..521e0482759 --- /dev/null +++ b/app/graphql/resolvers/blobs_resolver.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Resolvers + class BlobsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::Tree::BlobType.connection_type, null: true + authorize :download_code + calls_gitaly! + + alias_method :repository, :object + + argument :paths, [GraphQL::STRING_TYPE], + required: true, + description: 'Array of desired blob paths.' + argument :ref, GraphQL::STRING_TYPE, + required: false, + default_value: nil, + description: 'The commit ref to get the blobs from. Default value is HEAD.' + + # We fetch blobs from Gitaly efficiently but it still scales O(N) with the + # number of paths being fetched, so apply a scaling limit to that. + def self.resolver_complexity(args, child_complexity:) + super + args.fetch(:paths, []).size + end + + def resolve(paths:, ref:) + authorize!(repository.container) + + return [] if repository.empty? + + ref ||= repository.root_ref + + repository.blobs_at(paths.map { |path| [ref, path] }) + end + end +end diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb index 31444b0c592..75f1ee478a8 100644 --- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -50,7 +50,8 @@ module ResolvesMergeRequests approved_by: [:approved_by_users], milestone: [:milestone], security_auto_fix: [:author], - head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }] + head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }], + timelogs: [:timelogs] } end end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index ac3bdda0f12..7a67f115abf 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -44,7 +44,8 @@ module Resolvers { alert_management_alert: [:alert_management_alert], labels: [:labels], - assignees: [:assignees] + assignees: [:assignees], + timelogs: [:timelogs] } end diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb index 8fca6b829c0..aa8808b15ac 100644 --- a/app/graphql/resolvers/project_pipeline_resolver.rb +++ b/app/graphql/resolvers/project_pipeline_resolver.rb @@ -31,7 +31,7 @@ module Resolvers end else BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args| - finder = ::Ci::PipelinesFinder.new(project, current_user, shas: shas) + finder = ::Ci::PipelinesFinder.new(project, current_user, sha: shas) finder.execute.each { |pipeline| loader.call(pipeline.sha.to_s, pipeline) } end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index f15ab69f2d4..34c824fe9fb 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -124,6 +124,9 @@ module Types field :create_note_email, GraphQL::STRING_TYPE, null: true, description: 'User specific email address for the issue.' + field :timelogs, Types::TimelogType.connection_type, null: false, + description: 'Timelogs on the issue.' + def author Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index b7a50c4931a..c8ccf9d8aff 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -186,6 +186,8 @@ module Types description: 'Selected auto merge strategy.' field :merge_user, Types::UserType, null: true, description: 'User who merged this merge request.' + field :timelogs, Types::TimelogType.connection_type, null: false, + description: 'Timelogs on the merge request.' def approved_by object.approved_by_users diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index e319a5f3124..fc835cdf642 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -14,5 +14,7 @@ module Types description: 'Indicates a corresponding Git repository exists on disk.' field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true, description: 'Tree of the repository.' + field :blobs, Types::Tree::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true, + description: 'Blobs contained within the repository' end end diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index 87187e97df4..3219620de71 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -37,4 +37,18 @@ module ProfilesHelper def user_status_set_to_busy?(status) status&.availability == availability_values[:busy] end + + # Overridden in EE::ProfilesHelper#ssh_key_expiration_tooltip + def ssh_key_expiration_tooltip(key) + return key.errors.full_messages.join(', ') if key.errors.full_messages.any? + + s_('Profiles|Key usable beyond expiration date.') if key.expired? + end + + # Overridden in EE::ProfilesHelper#ssh_key_expires_field_description + def ssh_key_expires_field_description + s_('Profiles|Key can still be used after expiration.') + end end + +ProfilesHelper.prepend_ee_mod diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index b7f31ea86a7..b91fa4762bb 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -14,6 +14,10 @@ module SidebarsHelper end end + def project_sidebar_context(project, user) + Sidebars::Context.new(**project_sidebar_context_data(project, user)) + end + private def sidebar_project_tracking_attrs @@ -27,4 +31,12 @@ module SidebarsHelper def sidebar_user_profile_tracking_attrs tracking_attrs('user_side_navigation', 'render', 'user_side_navigation') end + + def project_sidebar_context_data(project, user) + { + current_user: user, + container: project, + project: project + } + end end diff --git a/app/models/blob.rb b/app/models/blob.rb index 8a9db8b45ea..2185233a1ac 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -2,6 +2,7 @@ # Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob class Blob < SimpleDelegator + include GlobalID::Identification include Presentable include BlobLanguageFromGitAttributes include BlobActiveModel diff --git a/app/models/concerns/has_timelogs_report.rb b/app/models/concerns/has_timelogs_report.rb index dc6d4e6ac6c..90f9876de95 100644 --- a/app/models/concerns/has_timelogs_report.rb +++ b/app/models/concerns/has_timelogs_report.rb @@ -2,9 +2,10 @@ module HasTimelogsReport extend ActiveSupport::Concern + include Gitlab::Utils::StrongMemoize def timelogs(start_time, end_time) - @timelogs ||= timelogs_for(start_time, end_time) + strong_memoize(:timelogs) { timelogs_for(start_time, end_time) } end def user_can_access_group_timelogs?(current_user) diff --git a/app/models/project.rb b/app/models/project.rb index 0f2b3538cb0..e4a9d7568f6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1378,7 +1378,7 @@ class Project < ApplicationRecord def find_or_initialize_service(name) return if disabled_services.include?(name) - find_service(services, name) || build_from_instance_or_template(name) || public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend + find_service(services, name) || build_from_instance_or_template(name) || build_service(name) end # rubocop: disable CodeReuse/ServiceClass @@ -2596,6 +2596,10 @@ class Project < ApplicationRecord return Service.build_from_integration(template, project_id: id) if template end + def build_service(name) + "#{name}_service".classify.constantize.new(project_id: id) + end + def services_templates @services_templates ||= Service.for_template end diff --git a/app/models/sidebars/panel.rb b/app/models/sidebars/panel.rb index 8fa421defc6..5c8191ebda3 100644 --- a/app/models/sidebars/panel.rb +++ b/app/models/sidebars/panel.rb @@ -51,5 +51,25 @@ module Sidebars def renderable_menus @renderable_menus ||= @menus.select(&:render?) end + + def container + context.container + end + + # Auxiliar method that helps with the migration from + # regular views to the new logic + def render_raw_scope_menu_partial + # No-op + end + + # Auxiliar method that helps with the migration from + # regular views to the new logic. + # + # Any menu inside this partial will be added after + # all the menus added in the `configure_menus` + # method. + def render_raw_menus_partial + # No-op + end end end diff --git a/app/models/sidebars/projects/panel.rb b/app/models/sidebars/projects/panel.rb new file mode 100644 index 00000000000..d301d2b4c3c --- /dev/null +++ b/app/models/sidebars/projects/panel.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Sidebars + module Projects + class Panel < ::Sidebars::Panel + override :render_raw_menus_partial + def render_raw_scope_menu_partial + 'layouts/nav/sidebar/project_scope_menu' + end + + override :render_raw_menus_partial + def render_raw_menus_partial + 'layouts/nav/sidebar/project_menus' + end + + override :aria_label + def aria_label + _('Project navigation') + end + end + end +end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 3915b62aa65..d7e2e678dac 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -250,13 +250,17 @@ module Projects def make_secure_tmp_dir(tmp_path) FileUtils.mkdir_p(tmp_path) - path = Dir.mktmpdir(nil, tmp_path) + path = Dir.mktmpdir(tmp_dir_prefix, tmp_path) begin yield(path) ensure FileUtils.remove_entry_secure(path) end end + + def tmp_dir_prefix + "project-#{project.id}-build-#{build.id}-" + end end end diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 022bbd39723..5cb109cf275 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -1,469 +1,3 @@ -%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(@project), 'aria-label': _('Project navigation') } - .nav-sidebar-inner-scroll - .context-header - = link_to project_path(@project), title: @project.name do - .avatar-container.rect-avatar.s40.project-avatar - = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile', width: 40, height: 40) - .sidebar-context-title - = @project.name - %ul.sidebar-top-level-items.qa-project-sidebar - = nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do - = link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do - .nav-icon-container - = sprite_icon('home') - %span.nav-item-name - = _('Project overview') - - %ul.sidebar-sub-level-items - = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do - = link_to project_path(@project) do - %strong.fly-out-top-item-name - = _('Project overview') - %li.divider.fly-out-top-item - = nav_link(path: 'projects#show') do - = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do - %span= _('Details') - - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do - %span= _('Activity') - - - if project_nav_tab?(:releases) - = nav_link(controller: :releases) do - = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do - %span= _('Releases') - - - if project_nav_tab? :learn_gitlab - = nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do - = link_to project_learn_gitlab_path(@project) do - .nav-icon-container - = sprite_icon('home') - %span.nav-item-name - = _('Learn GitLab') - - - if project_nav_tab? :files - = nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do - = link_to project_tree_path(@project), class: 'shortcuts-tree', data: { qa_selector: "repository_link" } do - .nav-icon-container - = sprite_icon('doc-text') - %span.nav-item-name#js-onboarding-repo-link - = _('Repository') - - %ul.sidebar-sub-level-items - = nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do - = link_to project_tree_path(@project) do - %strong.fly-out-top-item-name - = _('Repository') - %li.divider.fly-out-top-item - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_tree_path(@project) do - = _('Files') - - = nav_link(controller: [:commit, :commits]) do - = link_to project_commits_path(@project, current_ref), id: 'js-onboarding-commits-link' do - = _('Commits') - - = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project), data: { qa_selector: "branches_link" }, id: 'js-onboarding-branches-link' do - = _('Branches') - - = nav_link(controller: [:tags]) do - = link_to project_tags_path(@project), data: { qa_selector: "tags_link" } do - = _('Tags') - - = nav_link(path: 'graphs#show') do - = link_to project_graph_path(@project, current_ref) do - = _('Contributors') - - = nav_link(controller: %w(network)) do - = link_to project_network_path(@project, current_ref) do - = _('Graph') - - = nav_link(controller: :compare) do - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do - = _('Compare') - - = render_if_exists 'projects/sidebar/repository_locked_files' - - - if project_nav_tab? :issues - = nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards, :iterations] : 'projects/issues') do - = link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do - .nav-icon-container - = sprite_icon('issues') - %span.nav-item-name#js-onboarding-issues-link - = _('Issues') - - if @project.issues_enabled? - %span.badge.badge-pill.count.issue_counter - = number_with_delimiter(@project.open_issues_count(current_user)) - - %ul.sidebar-sub-level-items - = nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do - = link_to project_issues_path(@project) do - %strong.fly-out-top-item-name - = _('Issues') - - if @project.issues_enabled? - %span.badge.badge-pill.count.issue_counter.fly-out-badge - = number_with_delimiter(@project.open_issues_count(current_user)) - %li.divider.fly-out-top-item - = nav_link(controller: :issues, action: :index) do - = link_to project_issues_path(@project), title: _('Issues') do - %span - = _('List') - - = nav_link(controller: :boards) do - = link_to project_boards_path(@project), title: boards_link_text, data: { qa_selector: "issue_boards_link" } do - %span - = boards_link_text - - = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do - %span - = _('Labels') - - = render 'projects/sidebar/issues_service_desk' - - = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do - %span - = _('Milestones') - - = render_if_exists 'layouts/nav/sidebar/project_iterations_link' - - - if project_nav_tab?(:external_issue_tracker) - - issue_tracker = @project.external_issue_tracker - - if issue_tracker.is_a?(JiraService) && project_jira_issues_integration? - = render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker - - else - = nav_link do - = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do - .nav-icon-container - = sprite_icon('external-link') - %span.nav-item-name - = issue_tracker.title - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(html_options: { class: "fly-out-top-item" } ) do - = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do - %strong.fly-out-top-item-name - = issue_tracker.title - - - if (project_nav_tab? :labels) && !@project.issues_enabled? - = nav_link(controller: [:labels]) do - = link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do - .nav-icon-container - = sprite_icon('label') - %span.nav-item-name#js-onboarding-labels-link - = _('Labels') - - - if project_nav_tab? :merge_requests - = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do - = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do - .nav-icon-container - = sprite_icon('git-merge') - %span.nav-item-name#js-onboarding-mr-link - = _('Merge requests') - %span.badge.badge-pill.count.merge_counter.js-merge-counter - = number_with_delimiter(@project.open_merge_requests_count) - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do - = link_to project_merge_requests_path(@project) do - %strong.fly-out-top-item-name - = _('Merge requests') - %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge - = number_with_delimiter(@project.open_merge_requests_count) - - = render_if_exists "layouts/nav/requirements_link", project: @project - - - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do - = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do - .nav-icon-container - = sprite_icon('rocket') - %span.nav-item-name#js-onboarding-pipelines-link - = _('CI/CD') - - %ul.sidebar-sub-level-items - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do - = link_to project_pipelines_path(@project) do - %strong.fly-out-top-item-name - = _('CI/CD') - %li.divider.fly-out-top-item - - if project_nav_tab? :pipelines - = nav_link(path: ['pipelines#index', 'pipelines#show']) do - = link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do - %span - = _('Pipelines') - - - if can_view_pipeline_editor?(@project) - = nav_link(controller: :pipeline_editor, action: :show) do - = link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do - %span - = s_('Pipelines|Editor') - - - if project_nav_tab? :builds - = nav_link(controller: :jobs) do - = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do - %span - = _('Jobs') - - - if Feature.enabled?(:artifacts_management_page, @project) - = nav_link(controller: :artifacts, action: :index) do - = link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do - %span - = _('Artifacts') - - - if project_nav_tab?(:pipelines) - = nav_link(controller: :pipeline_schedules) do - = link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do - %span - = _('Schedules') - - = render_if_exists "layouts/nav/test_cases_link", project: @project - - - if project_nav_tab? :security_and_compliance - = render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific - - - if project_nav_tab? :operations - = nav_link(controller: sidebar_operations_paths) do - = link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do - .nav-icon-container - = sprite_icon('cloud-gear') - %span.nav-item-name - = _('Operations') - - %ul.sidebar-sub-level-items - = nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do - = link_to sidebar_operations_link_path do - %strong.fly-out-top-item-name - = _('Operations') - %li.divider.fly-out-top-item - - - if project_nav_tab? :metrics_dashboards - = nav_link(controller: :metrics_dashboard, action: [:show]) do - = link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do - %span - = _('Metrics') - - - if project_nav_tab?(:environments) && can?(current_user, :read_pod_logs, @project) - = nav_link(controller: :logs, action: [:index]) do - = link_to project_logs_path(@project), title: _('Logs') do - %span - = _('Logs') - - - if project_nav_tab? :environments - = render "layouts/nav/sidebar/tracing_link" - - - if project_nav_tab?(:error_tracking) - = nav_link(controller: :error_tracking) do - = link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do - %span - = _('Error Tracking') - - - if project_nav_tab?(:alert_management) - = nav_link(controller: :alert_management) do - = link_to project_alert_management_index_path(@project), title: _('Alerts') do - %span - = _('Alerts') - - - if project_nav_tab?(:incidents) - = nav_link(controller: :incidents) do - = link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do - %span - = _('Incidents') - - = render_if_exists 'projects/sidebar/oncall_schedules' - - - if project_nav_tab? :serverless - = nav_link(controller: :functions) do - = link_to project_serverless_functions_path(@project), title: _('Serverless') do - %span - = _('Serverless') - - - if project_nav_tab? :terraform - = nav_link(controller: :terraform) do - = link_to project_terraform_index_path(@project), title: _('Terraform') do - %span - = _('Terraform') - - - if project_nav_tab? :clusters - - show_cluster_hint = show_gke_cluster_integration_callout?(@project) - = nav_link(controller: [:cluster_agents, :clusters]) do - = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do - %span - = _('Kubernetes') - - if show_cluster_hint - .js-feature-highlight{ disabled: true, - data: { trigger: 'manual', - container: 'body', - placement: 'right', - highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION, - highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION], - dismiss_endpoint: user_callouts_path, - auto_devops_help_path: help_page_path('topics/autodevops/index.md') } } - - if project_nav_tab? :environments - = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do - = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do - %span - = _('Environments') - - - if project_nav_tab? :feature_flags - = nav_link(controller: :feature_flags) do - = link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do - %span - = _('Feature Flags') - - - if project_nav_tab?(:product_analytics) - = nav_link(controller: :product_analytics) do - = link_to project_product_analytics_path(@project), title: _('Product Analytics') do - %span - = _('Product Analytics') - - = render_if_exists 'layouts/nav/sidebar/project_packages_link' - - - if project_nav_tab? :analytics - = render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user) - - - if project_nav_tab?(:confluence) - - confluence_url = project_wikis_confluence_path(@project) - = nav_link do - = link_to confluence_url, class: 'shortcuts-confluence' do - .nav-icon-container - = image_tag 'confluence.svg', alt: _('Confluence') - %span.nav-item-name - = _('Confluence') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(html_options: { class: 'fly-out-top-item' } ) do - = link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do - %strong.fly-out-top-item-name - = _('Confluence') - - - if project_nav_tab? :wiki - = render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki) - - - if project_nav_tab?(:external_wiki) - - external_wiki_url = @project.external_wiki.external_wiki_url - = nav_link do - = link_to external_wiki_url, class: 'shortcuts-external_wiki' do - .nav-icon-container - = sprite_icon('external-link') - %span.nav-item-name - = s_('ExternalWikiService|External wiki') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(html_options: { class: "fly-out-top-item" } ) do - = link_to external_wiki_url do - %strong.fly-out-top-item-name - = s_('ExternalWikiService|External wiki') - - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do - .nav-icon-container - = sprite_icon('snippet') - %span.nav-item-name - = _('Snippets') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do - = link_to project_snippets_path(@project) do - %strong.fly-out-top-item-name - = _('Snippets') - - = nav_link(controller: :project_members) do - = link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do - .nav-icon-container - = sprite_icon('users') - %span.nav-item-name - = _('Members') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do - = link_to project_project_members_path(@project) do - %strong.fly-out-top-item-name - = _('Members') - - - if project_nav_tab? :settings - = nav_link(path: sidebar_settings_paths) do - = link_to edit_project_path(@project) do - .nav-icon-container - = sprite_icon('settings') - %span.nav-item-name.qa-settings-item#js-onboarding-settings-link - = _('Settings') - - %ul.sidebar-sub-level-items - - can_edit = can?(current_user, :admin_project, @project) - - if can_edit - = nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do - = link_to edit_project_path(@project) do - %strong.fly-out-top-item-name - = _('Settings') - %li.divider.fly-out-top-item - = nav_link(path: %w[projects#edit]) do - = link_to edit_project_path(@project), title: _('General'), class: 'qa-general-settings-link' do - %span - = _('General') - - if can_edit - = nav_link(controller: [:integrations, :services]) do - = link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do - %span - = _('Integrations') - = nav_link(controller: [:hooks, :hook_logs]) do - = link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do - %span - = _('Webhooks') - - if can?(current_user, :read_resource_access_tokens, @project) - = nav_link(controller: [:access_tokens]) do - = link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do - %span - = _('Access Tokens') - = nav_link(controller: :repository) do - = link_to project_settings_repository_path(@project), title: _('Repository') do - %span - = _('Repository') - - if !@project.archived? && @project.feature_available?(:builds, current_user) - = nav_link(controller: :ci_cd) do - = link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do - %span - = _('CI/CD') - - if settings_operations_available? - = nav_link(controller: [:operations]) do - = link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do - = _('Operations') - - if @project.pages_available? - = nav_link(controller: :pages) do - = link_to project_pages_path(@project), title: _('Pages') do - %span - = _('Pages') - - -# Shortcut to Project > Activity - %li.hidden - = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do - %span - = _('Activity') - - -# Shortcut to Repository > Graph (formerly, Network) - - if project_nav_tab? :network - %li.hidden - = link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do - = _('Graph') - - -# Shortcut to Issues > New Issue - - if project_nav_tab?(:issues) - %li.hidden - = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do - = _('Create a new issue') - - -# Shortcut to Pipelines > Jobs - - if project_nav_tab? :builds - %li.hidden - = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do - = _('Jobs') - - -# Shortcut to commits page - - if project_nav_tab? :commits - %li.hidden - = link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do - = _('Commits') - - -# Shortcut to issue boards - - if project_nav_tab?(:issues) - %li.hidden - = link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards' - - = render 'shared/sidebar_toggle_button' +-# We're migration the project 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/_project_menus.html.haml. += render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user)) diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml new file mode 100644 index 00000000000..79a7150e030 --- /dev/null +++ b/app/views/layouts/nav/sidebar/_project_menus.html.haml @@ -0,0 +1,458 @@ += nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do + = link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do + .nav-icon-container + = sprite_icon('home') + %span.nav-item-name + = _('Project overview') + + %ul.sidebar-sub-level-items + = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do + = link_to project_path(@project) do + %strong.fly-out-top-item-name + = _('Project overview') + %li.divider.fly-out-top-item + = nav_link(path: 'projects#show') do + = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do + %span= _('Details') + + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do + %span= _('Activity') + + - if project_nav_tab?(:releases) + = nav_link(controller: :releases) do + = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do + %span= _('Releases') + +- if project_nav_tab? :learn_gitlab + = nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do + = link_to project_learn_gitlab_path(@project) do + .nav-icon-container + = sprite_icon('home') + %span.nav-item-name + = _('Learn GitLab') + +- if project_nav_tab? :files + = nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do + = link_to project_tree_path(@project), class: 'shortcuts-tree', data: { qa_selector: "repository_link" } do + .nav-icon-container + = sprite_icon('doc-text') + %span.nav-item-name#js-onboarding-repo-link + = _('Repository') + + %ul.sidebar-sub-level-items + = nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do + = link_to project_tree_path(@project) do + %strong.fly-out-top-item-name + = _('Repository') + %li.divider.fly-out-top-item + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_tree_path(@project) do + = _('Files') + + = nav_link(controller: [:commit, :commits]) do + = link_to project_commits_path(@project, current_ref), id: 'js-onboarding-commits-link' do + = _('Commits') + + = nav_link(html_options: {class: branches_tab_class}) do + = link_to project_branches_path(@project), data: { qa_selector: "branches_link" }, id: 'js-onboarding-branches-link' do + = _('Branches') + + = nav_link(controller: [:tags]) do + = link_to project_tags_path(@project), data: { qa_selector: "tags_link" } do + = _('Tags') + + = nav_link(path: 'graphs#show') do + = link_to project_graph_path(@project, current_ref) do + = _('Contributors') + + = nav_link(controller: %w(network)) do + = link_to project_network_path(@project, current_ref) do + = _('Graph') + + = nav_link(controller: :compare) do + = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do + = _('Compare') + + = render_if_exists 'projects/sidebar/repository_locked_files' + +- if project_nav_tab? :issues + = nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards, :iterations] : 'projects/issues') do + = link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do + .nav-icon-container + = sprite_icon('issues') + %span.nav-item-name#js-onboarding-issues-link + = _('Issues') + - if @project.issues_enabled? + %span.badge.badge-pill.count.issue_counter + = number_with_delimiter(@project.open_issues_count(current_user)) + + %ul.sidebar-sub-level-items + = nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do + = link_to project_issues_path(@project) do + %strong.fly-out-top-item-name + = _('Issues') + - if @project.issues_enabled? + %span.badge.badge-pill.count.issue_counter.fly-out-badge + = number_with_delimiter(@project.open_issues_count(current_user)) + %li.divider.fly-out-top-item + = nav_link(controller: :issues, action: :index) do + = link_to project_issues_path(@project), title: _('Issues') do + %span + = _('List') + + = nav_link(controller: :boards) do + = link_to project_boards_path(@project), title: boards_link_text, data: { qa_selector: "issue_boards_link" } do + %span + = boards_link_text + + = nav_link(controller: :labels) do + = link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do + %span + = _('Labels') + + = render 'projects/sidebar/issues_service_desk' + + = nav_link(controller: :milestones) do + = link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do + %span + = _('Milestones') + + = render_if_exists 'layouts/nav/sidebar/project_iterations_link' + +- if project_nav_tab?(:external_issue_tracker) + - issue_tracker = @project.external_issue_tracker + - if issue_tracker.is_a?(JiraService) && project_jira_issues_integration? + = render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker + - else + = nav_link do + = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do + .nav-icon-container + = sprite_icon('external-link') + %span.nav-item-name + = issue_tracker.title + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(html_options: { class: "fly-out-top-item" } ) do + = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do + %strong.fly-out-top-item-name + = issue_tracker.title + +- if (project_nav_tab? :labels) && !@project.issues_enabled? + = nav_link(controller: [:labels]) do + = link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do + .nav-icon-container + = sprite_icon('label') + %span.nav-item-name#js-onboarding-labels-link + = _('Labels') + +- if project_nav_tab? :merge_requests + = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do + = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do + .nav-icon-container + = sprite_icon('git-merge') + %span.nav-item-name#js-onboarding-mr-link + = _('Merge requests') + %span.badge.badge-pill.count.merge_counter.js-merge-counter + = number_with_delimiter(@project.open_merge_requests_count) + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do + = link_to project_merge_requests_path(@project) do + %strong.fly-out-top-item-name + = _('Merge requests') + %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge + = number_with_delimiter(@project.open_merge_requests_count) + += render_if_exists "layouts/nav/requirements_link", project: @project + +- if project_nav_tab? :pipelines + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do + = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do + .nav-icon-container + = sprite_icon('rocket') + %span.nav-item-name#js-onboarding-pipelines-link + = _('CI/CD') + + %ul.sidebar-sub-level-items + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do + = link_to project_pipelines_path(@project) do + %strong.fly-out-top-item-name + = _('CI/CD') + %li.divider.fly-out-top-item + - if project_nav_tab? :pipelines + = nav_link(path: ['pipelines#index', 'pipelines#show']) do + = link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do + %span + = _('Pipelines') + + - if can_view_pipeline_editor?(@project) + = nav_link(controller: :pipeline_editor, action: :show) do + = link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do + %span + = s_('Pipelines|Editor') + + - if project_nav_tab? :builds + = nav_link(controller: :jobs) do + = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do + %span + = _('Jobs') + + - if Feature.enabled?(:artifacts_management_page, @project) + = nav_link(controller: :artifacts, action: :index) do + = link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do + %span + = _('Artifacts') + + - if project_nav_tab?(:pipelines) + = nav_link(controller: :pipeline_schedules) do + = link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do + %span + = _('Schedules') + + = render_if_exists "layouts/nav/test_cases_link", project: @project + +- if project_nav_tab? :security_and_compliance + = render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific + +- if project_nav_tab? :operations + = nav_link(controller: sidebar_operations_paths) do + = link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do + .nav-icon-container + = sprite_icon('cloud-gear') + %span.nav-item-name + = _('Operations') + + %ul.sidebar-sub-level-items + = nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do + = link_to sidebar_operations_link_path do + %strong.fly-out-top-item-name + = _('Operations') + %li.divider.fly-out-top-item + + - if project_nav_tab? :metrics_dashboards + = nav_link(controller: :metrics_dashboard, action: [:show]) do + = link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do + %span + = _('Metrics') + + - if project_nav_tab?(:environments) && can?(current_user, :read_pod_logs, @project) + = nav_link(controller: :logs, action: [:index]) do + = link_to project_logs_path(@project), title: _('Logs') do + %span + = _('Logs') + + - if project_nav_tab? :environments + = render "layouts/nav/sidebar/tracing_link" + + - if project_nav_tab?(:error_tracking) + = nav_link(controller: :error_tracking) do + = link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do + %span + = _('Error Tracking') + + - if project_nav_tab?(:alert_management) + = nav_link(controller: :alert_management) do + = link_to project_alert_management_index_path(@project), title: _('Alerts') do + %span + = _('Alerts') + + - if project_nav_tab?(:incidents) + = nav_link(controller: :incidents) do + = link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do + %span + = _('Incidents') + + = render_if_exists 'projects/sidebar/oncall_schedules' + + - if project_nav_tab? :serverless + = nav_link(controller: :functions) do + = link_to project_serverless_functions_path(@project), title: _('Serverless') do + %span + = _('Serverless') + + - if project_nav_tab? :terraform + = nav_link(controller: :terraform) do + = link_to project_terraform_index_path(@project), title: _('Terraform') do + %span + = _('Terraform') + + - if project_nav_tab? :clusters + - show_cluster_hint = show_gke_cluster_integration_callout?(@project) + = nav_link(controller: [:cluster_agents, :clusters]) do + = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do + %span + = _('Kubernetes') + - if show_cluster_hint + .js-feature-highlight{ disabled: true, + data: { trigger: 'manual', + container: 'body', + placement: 'right', + highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION, + highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION], + dismiss_endpoint: user_callouts_path, + auto_devops_help_path: help_page_path('topics/autodevops/index.md') } } + - if project_nav_tab? :environments + = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do + = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do + %span + = _('Environments') + + - if project_nav_tab? :feature_flags + = nav_link(controller: :feature_flags) do + = link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do + %span + = _('Feature Flags') + + - if project_nav_tab?(:product_analytics) + = nav_link(controller: :product_analytics) do + = link_to project_product_analytics_path(@project), title: _('Product Analytics') do + %span + = _('Product Analytics') + += render_if_exists 'layouts/nav/sidebar/project_packages_link' + +- if project_nav_tab? :analytics + = render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user) + +- if project_nav_tab?(:confluence) + - confluence_url = project_wikis_confluence_path(@project) + = nav_link do + = link_to confluence_url, class: 'shortcuts-confluence' do + .nav-icon-container + = image_tag 'confluence.svg', alt: _('Confluence') + %span.nav-item-name + = _('Confluence') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(html_options: { class: 'fly-out-top-item' } ) do + = link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do + %strong.fly-out-top-item-name + = _('Confluence') + +- if project_nav_tab? :wiki + = render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki) + +- if project_nav_tab?(:external_wiki) + - external_wiki_url = @project.external_wiki.external_wiki_url + = nav_link do + = link_to external_wiki_url, class: 'shortcuts-external_wiki' do + .nav-icon-container + = sprite_icon('external-link') + %span.nav-item-name + = s_('ExternalWikiService|External wiki') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(html_options: { class: "fly-out-top-item" } ) do + = link_to external_wiki_url do + %strong.fly-out-top-item-name + = s_('ExternalWikiService|External wiki') + +- if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do + .nav-icon-container + = sprite_icon('snippet') + %span.nav-item-name + = _('Snippets') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do + = link_to project_snippets_path(@project) do + %strong.fly-out-top-item-name + = _('Snippets') + += nav_link(controller: :project_members) do + = link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do + .nav-icon-container + = sprite_icon('users') + %span.nav-item-name + = _('Members') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do + = link_to project_project_members_path(@project) do + %strong.fly-out-top-item-name + = _('Members') + +- if project_nav_tab? :settings + = nav_link(path: sidebar_settings_paths) do + = link_to edit_project_path(@project) do + .nav-icon-container + = sprite_icon('settings') + %span.nav-item-name.qa-settings-item#js-onboarding-settings-link + = _('Settings') + + %ul.sidebar-sub-level-items + - can_edit = can?(current_user, :admin_project, @project) + - if can_edit + = nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do + = link_to edit_project_path(@project) do + %strong.fly-out-top-item-name + = _('Settings') + %li.divider.fly-out-top-item + = nav_link(path: %w[projects#edit]) do + = link_to edit_project_path(@project), title: _('General'), class: 'qa-general-settings-link' do + %span + = _('General') + - if can_edit + = nav_link(controller: [:integrations, :services]) do + = link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do + %span + = _('Integrations') + = nav_link(controller: [:hooks, :hook_logs]) do + = link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do + %span + = _('Webhooks') + - if can?(current_user, :read_resource_access_tokens, @project) + = nav_link(controller: [:access_tokens]) do + = link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do + %span + = _('Access Tokens') + = nav_link(controller: :repository) do + = link_to project_settings_repository_path(@project), title: _('Repository') do + %span + = _('Repository') + - if !@project.archived? && @project.feature_available?(:builds, current_user) + = nav_link(controller: :ci_cd) do + = link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do + %span + = _('CI/CD') + - if settings_operations_available? + = nav_link(controller: [:operations]) do + = link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do + = _('Operations') + - if @project.pages_available? + = nav_link(controller: :pages) do + = link_to project_pages_path(@project), title: _('Pages') do + %span + = _('Pages') + +-# Shortcut to Project > Activity +%li.hidden + = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do + %span + = _('Activity') + +-# Shortcut to Repository > Graph (formerly, Network) +- if project_nav_tab? :network + %li.hidden + = link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do + = _('Graph') + +-# Shortcut to Issues > New Issue +- if project_nav_tab?(:issues) + %li.hidden + = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do + = _('Create a new issue') + +-# Shortcut to Pipelines > Jobs +- if project_nav_tab? :builds + %li.hidden + = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do + = _('Jobs') + +-# Shortcut to commits page +- if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do + = _('Commits') + +-# Shortcut to issue boards +- if project_nav_tab?(:issues) + %li.hidden + = link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards' diff --git a/app/views/layouts/nav/sidebar/_project_scope_menu.html.haml b/app/views/layouts/nav/sidebar/_project_scope_menu.html.haml new file mode 100644 index 00000000000..a666c032248 --- /dev/null +++ b/app/views/layouts/nav/sidebar/_project_scope_menu.html.haml @@ -0,0 +1,6 @@ +.context-header + = link_to project_path(@project), title: @project.name do + .avatar-container.rect-avatar.s40.project-avatar + = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile', width: 40, height: 40) + .sidebar-context-title + = @project.name diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index 96a05097935..35335f3ef80 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -15,11 +15,12 @@ .col.form-group = f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold' = f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, data: { qa_selector: 'key_expiry_date_field' } + %p.form-text.text-muted{ data: { qa_selector: 'key_expiry_date_field_description' } }= ssh_key_expires_field_description .js-add-ssh-key-validation-warning.hide .bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' } %strong= _('Oops, are you sure?') - %p= s_("Profiles|This doesn't look like a public SSH key, are you sure you want to add it? It will be publicly visible.") + %p= s_("Profiles|Publicly visible private SSH keys can compromise your system.") %button.btn.gl-button.btn-confirm.js-add-ssh-key-validation-confirm-submit= _("Yes, add it") diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index cc2e2a30052..6a87e052272 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -1,3 +1,5 @@ +- icon_classes = 'settings-list-icon gl-display-none gl-sm-display-block' + %li.key-list-item .gl-display-flex.gl-align-items-flex-start .key-list-item-info.gl-w-full.float-none @@ -5,15 +7,11 @@ = key.title .gl-display-flex.gl-align-items-center.gl-mt-2 - - if key.valid? - - if key.expired? - %span.gl-display-inline-block.has-tooltip{ title: s_('Profiles|Your key has expired') } - = sprite_icon('warning-solid', css_class: 'settings-list-icon gl-display-none gl-sm-display-block') - - else - = sprite_icon('key', css_class: 'settings-list-icon gl-display-none gl-sm-display-block') + - if key.valid? && !key.expired? + = sprite_icon('key', css_class: icon_classes) - else - %span.gl-display-inline-block.has-tooltip{ title: key.errors.full_messages.join(', ') } - = sprite_icon('warning-solid', css_class: 'settings-list-icon gl-display-none gl-sm-display-block') + %span.gl-display-inline-block.has-tooltip{ title: ssh_key_expiration_tooltip(key) } + = sprite_icon('warning-solid', css_class: icon_classes) %span.gl-text-truncate.gl-sm-ml-3 = key.fingerprint diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 0bea70be837..c8a5908018d 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -49,7 +49,7 @@ = render_if_exists 'projects/commits/mirror_status' -.js-branch-list{ data: { diverging_counts_endpoint: diverging_commit_counts_namespace_project_branches_path(@project.namespace, @project, format: :json) } } +.js-branch-list{ data: { diverging_counts_endpoint: diverging_commit_counts_namespace_project_branches_path(@project.namespace, @project, format: :json), default_branch: @project.default_branch } } - if can?(current_user, :admin_project, @project) - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project) .row-content-block diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml deleted file mode 100644 index d96eea77366..00000000000 --- a/app/views/shared/_delete_label_modal.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.modal{ id: "modal-delete-label-#{label.id}", tabindex: -1 } - .modal-dialog - .modal-content - .modal-header - %h3.page-title= _('Delete label: %{label_name} ?') % { label_name: label.name } - %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } - %span{ "aria-hidden": true } × - - .modal-body - %p - = html_escape(_('%{label_name} %{span_open}will be permanently deleted from %{subject_name}. This cannot be undone.%{span_close}')) % { label_name: tag.strong(label.name), subject_name: label.subject_name, span_open: ''.html_safe, span_close: ''.html_safe } - - .modal-footer - %a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }= _('Cancel') - - = link_to _('Delete label'), - label.destroy_path, - title: _('Delete'), - method: :delete, - class: 'gl-button btn btn-danger' diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 096350b8e35..45aca63d83d 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -36,10 +36,10 @@ label_text_color: label.text_color, group_name: label.project.group.name } } = _('Promote to group label') - - if can?(current_user, :admin_label, label) - %li - %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } } - %button.text-danger.remove-row{ type: 'button' }= _('Delete') + %li + %span + %button.text-danger.remove-row.js-delete-label-modal-button{ type: 'button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } } + = _('Delete') - if current_user %li.inline.label-subscription - if label.can_subscribe_to_label_in_different_levels? @@ -61,5 +61,3 @@ - else %button.gl-button.js-subscribe-button.label-subscribe-button.btn.btn-default.gl-ml-3{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title } %span= label_subscription_toggle_button_text(label, @project) - -= render 'shared/delete_label_modal', label: label diff --git a/app/views/shared/nav/_sidebar.html.haml b/app/views/shared/nav/_sidebar.html.haml new file mode 100644 index 00000000000..50586598c34 --- /dev/null +++ b/app/views/shared/nav/_sidebar.html.haml @@ -0,0 +1,10 @@ +%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.qa-project-sidebar + - if sidebar.render_raw_menus_partial + = render sidebar.render_raw_menus_partial + + = render 'shared/sidebar_toggle_button' diff --git a/app/workers/concerns/each_shard_worker.rb b/app/workers/concerns/each_shard_worker.rb index 00f589f957e..d1d558f55fe 100644 --- a/app/workers/concerns/each_shard_worker.rb +++ b/app/workers/concerns/each_shard_worker.rb @@ -24,7 +24,13 @@ module EachShardWorker end def healthy_ready_shards - ready_shards.select(&:success) + success_checks, failed_checks = ready_shards.partition(&:success) + + if failed_checks.any? + ::Gitlab::AppLogger.error(message: 'Excluding unhealthy shards', failed_checks: failed_checks.map(&:payload), class: self.class.name) + end + + success_checks end def ready_shards diff --git a/changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml b/changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml new file mode 100644 index 00000000000..e4b6e55bb11 --- /dev/null +++ b/changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Make blobs directly accessible through the graphql repository +merge_request: 58677 +author: +type: added diff --git a/changelogs/unreleased/324100-update-default-initial-branch-name.yml b/changelogs/unreleased/324100-update-default-initial-branch-name.yml new file mode 100644 index 00000000000..3850f19f977 --- /dev/null +++ b/changelogs/unreleased/324100-update-default-initial-branch-name.yml @@ -0,0 +1,5 @@ +--- +title: Update default branch in divergence graph +merge_request: 58871 +author: +type: changed diff --git a/changelogs/unreleased/324263-add-a-way-to-filter-search-for-a-namespace.yml b/changelogs/unreleased/324263-add-a-way-to-filter-search-for-a-namespace.yml new file mode 100644 index 00000000000..a9b143103fb --- /dev/null +++ b/changelogs/unreleased/324263-add-a-way-to-filter-search-for-a-namespace.yml @@ -0,0 +1,5 @@ +--- +title: Add search functionality to Jira Connect App namespaces +merge_request: 57669 +author: +type: added diff --git a/changelogs/unreleased/326209-fix-find-or-initialize-service-n-1.yml b/changelogs/unreleased/326209-fix-find-or-initialize-service-n-1.yml new file mode 100644 index 00000000000..bd933ea23d7 --- /dev/null +++ b/changelogs/unreleased/326209-fix-find-or-initialize-service-n-1.yml @@ -0,0 +1,5 @@ +--- +title: Fix N+1 queries to find or initialize services +merge_request: 58879 +author: +type: performance diff --git a/57952- Update GIicon in geo_node_header.vue.yml b/changelogs/unreleased/57952-Update GIicon in geo_node_header.vue.yml similarity index 98% rename from 57952- Update GIicon in geo_node_header.vue.yml rename to changelogs/unreleased/57952-Update GIicon in geo_node_header.vue.yml index 7e6595473a0..c2c8b30d981 100644 --- a/57952- Update GIicon in geo_node_header.vue.yml +++ b/changelogs/unreleased/57952-Update GIicon in geo_node_header.vue.yml @@ -3,5 +3,3 @@ title: Update GIicon size in geo_node_header.vue merge_request: 57952 author: singhanshuman type: changed - - diff --git a/changelogs/unreleased/ci-fix-pipeline-loading-by-sha-for-graphql.yml b/changelogs/unreleased/ci-fix-pipeline-loading-by-sha-for-graphql.yml new file mode 100644 index 00000000000..73297b5da34 --- /dev/null +++ b/changelogs/unreleased/ci-fix-pipeline-loading-by-sha-for-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Fix loading pipelines by commit SHA for GraphQL +merge_request: 59110 +author: +type: fixed diff --git a/changelogs/unreleased/ensure_project_iid_before_drop_pipeline.yml b/changelogs/unreleased/ensure_project_iid_before_drop_pipeline.yml new file mode 100644 index 00000000000..ccca0f16adc --- /dev/null +++ b/changelogs/unreleased/ensure_project_iid_before_drop_pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Ensure a project iid is set before transitioning on pipeline error +merge_request: 57783 +author: +type: performance diff --git a/changelogs/unreleased/graphql-expose-timelogs-against-issuables.yml b/changelogs/unreleased/graphql-expose-timelogs-against-issuables.yml new file mode 100644 index 00000000000..22bb210931f --- /dev/null +++ b/changelogs/unreleased/graphql-expose-timelogs-against-issuables.yml @@ -0,0 +1,5 @@ +--- +title: Expose timelogs against issues and merge requests in GraphQL +merge_request: 57321 +author: Lee Tickett @leetickett +type: added diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-gitlab-hook-data.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-gitlab-hook-data.yml new file mode 100644 index 00000000000..cefa8109984 --- /dev/null +++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-gitlab-hook-data.yml @@ -0,0 +1,5 @@ +--- +title: Fix EmptyLineAfterFinalLetItBe offenses in spec/lib/gitlab/hook_data +merge_request: 58262 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: fixed diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-groups.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-groups.yml new file mode 100644 index 00000000000..02326d55121 --- /dev/null +++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-groups.yml @@ -0,0 +1,5 @@ +--- +title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/groups +merge_request: 58423 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: fixed diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-ide.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-ide.yml new file mode 100644 index 00000000000..32bb78d6d1c --- /dev/null +++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-ide.yml @@ -0,0 +1,5 @@ +--- +title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/ide +merge_request: 58424 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: fixed diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-issues.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-issues.yml new file mode 100644 index 00000000000..c7874529642 --- /dev/null +++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-issues.yml @@ -0,0 +1,5 @@ +--- +title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/issues +merge_request: 58425 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: fixed diff --git a/changelogs/unreleased/sh-pages-tmp-dir-use-build-id.yml b/changelogs/unreleased/sh-pages-tmp-dir-use-build-id.yml new file mode 100644 index 00000000000..db9daca248e --- /dev/null +++ b/changelogs/unreleased/sh-pages-tmp-dir-use-build-id.yml @@ -0,0 +1,5 @@ +--- +title: Include project and build ID in Pages tmp directory +merge_request: 59106 +author: +type: changed diff --git a/config/feature_flags/development/ci_pipeline_ensure_iid_on_drop.yml b/config/feature_flags/development/ci_pipeline_ensure_iid_on_drop.yml new file mode 100644 index 00000000000..129f725c6cc --- /dev/null +++ b/config/feature_flags/development/ci_pipeline_ensure_iid_on_drop.yml @@ -0,0 +1,8 @@ +--- +name: ci_pipeline_ensure_iid_on_drop +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57783 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326886 +milestone: '13.11' +type: development +group: group::code review +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 36774757ed3..6003536010e 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2905,6 +2905,7 @@ Relationship between an epic and an issue. | `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. | | `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. | | `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. | +| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the issue. | | `title` | [`String!`](#string) | Title of the issue. | | `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. | | `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. | @@ -3468,6 +3469,7 @@ An edge in a connection. | `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. | | `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. | | `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. | +| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the issue. | | `title` | [`String!`](#string) | Title of the issue. | | `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. | | `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. | @@ -3980,6 +3982,7 @@ An edge in a connection. | `targetProjectId` | [`Int!`](#int) | ID of the merge request target project. | | `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Completion status of tasks. | | `timeEstimate` | [`Int!`](#int) | Time estimate of the merge request. | +| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the merge request. | | `title` | [`String!`](#string) | Title of the merge request. | | `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. | | `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the merge request. | @@ -5440,6 +5443,7 @@ Autogenerated return type of RepositionImageDiffNote. | Field | Type | Description | | ----- | ---- | ----------- | +| `blobs` | [`BlobConnection`](#blobconnection) | Blobs contained within the repository. | | `empty` | [`Boolean!`](#boolean) | Indicates repository has no visible content. | | `exists` | [`Boolean!`](#boolean) | Indicates a corresponding Git repository exists on disk. | | `rootRef` | [`String`](#string) | Default branch of the repository. | diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb index d7271df1694..f995f62f87b 100644 --- a/lib/gitlab/ci/pipeline/chain/helpers.rb +++ b/lib/gitlab/ci/pipeline/chain/helpers.rb @@ -12,7 +12,17 @@ module Gitlab end pipeline.add_error_message(message) - pipeline.drop!(drop_reason) if drop_reason && persist_pipeline? + + if drop_reason && persist_pipeline? + if Feature.enabled?(:ci_pipeline_ensure_iid_on_drop, pipeline.project, default_enabled: :yaml) + # Project iid must be called outside a transaction, so we ensure it is set here + # otherwise it may be set within the state transition transaction of the drop! call + # which it will lock the InternalId row for the whole transaction + pipeline.ensure_project_iid! + end + + pipeline.drop!(drop_reason) + end # TODO: consider not to rely on AR errors directly as they can be # polluted with other unrelated errors (e.g. state machine) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 326955837f7..4ec08243c5c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -623,9 +623,6 @@ msgstr "" msgid "%{label_for_message} unavailable" msgstr "" -msgid "%{label_name} %{span_open}will be permanently deleted from %{subject_name}. This cannot be undone.%{span_close}" -msgstr "" - msgid "%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites." msgstr "" @@ -10229,7 +10226,7 @@ msgstr "" msgid "Delete label" msgstr "" -msgid "Delete label: %{label_name} ?" +msgid "Delete label: %{labelName}" msgstr "" msgid "Delete pipeline" @@ -23895,6 +23892,9 @@ msgstr "" msgid "Profiles|Enter your name, so people you know can recognize you" msgstr "" +msgid "Profiles|Expired key is not valid." +msgstr "" + msgid "Profiles|Expires at" msgstr "" @@ -23925,6 +23925,9 @@ msgstr "" msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)" msgstr "" +msgid "Profiles|Invalid key." +msgstr "" + msgid "Profiles|Invalid password" msgstr "" @@ -23934,6 +23937,15 @@ msgstr "" msgid "Profiles|Key" msgstr "" +msgid "Profiles|Key can still be used after expiration." +msgstr "" + +msgid "Profiles|Key usable beyond expiration date." +msgstr "" + +msgid "Profiles|Key will be deleted on this date." +msgstr "" + msgid "Profiles|Last used:" msgstr "" @@ -23979,6 +23991,9 @@ msgstr "" msgid "Profiles|Public email" msgstr "" +msgid "Profiles|Publicly visible private SSH keys can compromise your system." +msgstr "" + msgid "Profiles|Remove avatar" msgstr "" @@ -24003,9 +24018,6 @@ msgstr "" msgid "Profiles|The maximum file size allowed is 200KB." msgstr "" -msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it? It will be publicly visible." -msgstr "" - msgid "Profiles|This email will be displayed on your public profile" msgstr "" @@ -24093,9 +24105,6 @@ msgstr "" msgid "Profiles|Your email address was automatically set based on your %{provider_label} account" msgstr "" -msgid "Profiles|Your key has expired" -msgstr "" - msgid "Profiles|Your location was automatically set based on your %{provider_label} account" msgstr "" diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 16c66ea5761..d9fae3db23f 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -13,7 +13,7 @@ module QA include SubMenus::Settings include SubMenus::Packages - view 'app/views/layouts/nav/sidebar/_project.html.haml' do + view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do element :activity_link element :merge_requests_link element :snippets_link diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb index 9405ea97fff..398712c04d2 100644 --- a/qa/qa/page/project/sub_menus/ci_cd.rb +++ b/qa/qa/page/project/sub_menus/ci_cd.rb @@ -13,7 +13,7 @@ module QA base.class_eval do include QA::Page::Project::SubMenus::Common - view 'app/views/layouts/nav/sidebar/_project.html.haml' do + view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do element :link_pipelines end end diff --git a/qa/qa/page/project/sub_menus/issues.rb b/qa/qa/page/project/sub_menus/issues.rb index 124faf0d346..384af3fb53e 100644 --- a/qa/qa/page/project/sub_menus/issues.rb +++ b/qa/qa/page/project/sub_menus/issues.rb @@ -13,7 +13,7 @@ module QA base.class_eval do include QA::Page::Project::SubMenus::Common - view 'app/views/layouts/nav/sidebar/_project.html.haml' do + view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do element :issue_boards_link element :issues_item element :labels_link diff --git a/qa/qa/page/project/sub_menus/operations.rb b/qa/qa/page/project/sub_menus/operations.rb index 042994062c7..af716d1af0d 100644 --- a/qa/qa/page/project/sub_menus/operations.rb +++ b/qa/qa/page/project/sub_menus/operations.rb @@ -13,7 +13,7 @@ module QA base.class_eval do include QA::Page::Project::SubMenus::Common - view 'app/views/layouts/nav/sidebar/_project.html.haml' do + view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do element :operations_link element :operations_environments_link element :operations_metrics_link diff --git a/qa/qa/page/project/sub_menus/project.rb b/qa/qa/page/project/sub_menus/project.rb index 4af640301b9..8a648279919 100644 --- a/qa/qa/page/project/sub_menus/project.rb +++ b/qa/qa/page/project/sub_menus/project.rb @@ -13,7 +13,7 @@ module QA base.class_eval do include QA::Page::Project::SubMenus::Common - view 'app/views/layouts/nav/sidebar/_project.html.haml' do + view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do element :project_link end end diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb index c78c7521b64..38fa57eacc8 100644 --- a/qa/qa/page/project/sub_menus/repository.rb +++ b/qa/qa/page/project/sub_menus/repository.rb @@ -13,7 +13,7 @@ module QA base.class_eval do include QA::Page::Project::SubMenus::Common - view 'app/views/layouts/nav/sidebar/_project.html.haml' do + view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do element :repository_link element :branches_link element :tags_link diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb index df16c329246..531c4686345 100644 --- a/qa/qa/page/project/sub_menus/settings.rb +++ b/qa/qa/page/project/sub_menus/settings.rb @@ -13,7 +13,7 @@ module QA base.class_eval do include QA::Page::Project::SubMenus::Common - view 'app/views/layouts/nav/sidebar/_project.html.haml' do + view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do element :settings_item element :general_settings_link element :integrations_settings_link diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb index 5d34acc635d..204cb808c8e 100644 --- a/spec/factories/timelogs.rb +++ b/spec/factories/timelogs.rb @@ -1,11 +1,22 @@ # frozen_string_literal: true -# Read about factories at https://github.com/thoughtbot/factory_bot - FactoryBot.define do factory :timelog do time_spent { 3600 } - issue - user { issue.project.creator } + for_issue + + factory :issue_timelog, traits: [:for_issue] + factory :merge_request_timelog, traits: [:for_merge_request] + + trait :for_issue do + issue + user { issue.author } + end + + trait :for_merge_request do + merge_request + issue { nil } + user { merge_request.author } + end end end diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb index 217f86b92cf..4ca9fe7488a 100644 --- a/spec/features/projects/labels/user_removes_labels_spec.rb +++ b/spec/features/projects/labels/user_removes_labels_spec.rb @@ -18,18 +18,18 @@ RSpec.describe "User removes labels" do visit(project_labels_path(project)) end - it "removes label" do + it "removes label", :js do page.within(".other-labels") do page.first(".label-list-item") do first('.js-label-options-dropdown').click first(".remove-row").click end - - expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.") - - first(:link, "Delete label").click end + expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.") + + first(:link, "Delete label").click + expect(page).to have_content("Label was removed").and have_no_content(label.title) end end diff --git a/spec/frontend/delete_label_modal_spec.js b/spec/frontend/delete_label_modal_spec.js new file mode 100644 index 00000000000..df70d3a8393 --- /dev/null +++ b/spec/frontend/delete_label_modal_spec.js @@ -0,0 +1,83 @@ +import { TEST_HOST } from 'helpers/test_constants'; +import initDeleteLabelModal from '~/delete_label_modal'; + +describe('DeleteLabelModal', () => { + const buttons = [ + { + labelName: 'label 1', + subjectName: 'GitLab Org', + destroyPath: `${TEST_HOST}/1`, + }, + { + labelName: 'label 2', + subjectName: 'GitLab Org', + destroyPath: `${TEST_HOST}/2`, + }, + ]; + + beforeEach(() => { + const buttonContainer = document.createElement('div'); + + buttons.forEach((x) => { + const button = document.createElement('button'); + button.setAttribute('class', 'js-delete-label-modal-button'); + button.setAttribute('data-label-name', x.labelName); + button.setAttribute('data-subject-name', x.subjectName); + button.setAttribute('data-destroy-path', x.destroyPath); + button.innerHTML = 'Action'; + buttonContainer.appendChild(button); + }); + + document.body.appendChild(buttonContainer); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + const findJsHooks = () => document.querySelectorAll('.js-delete-label-modal-button'); + const findModal = () => document.querySelector('.gl-modal'); + + it('starts with only js-containers', () => { + expect(findJsHooks()).toHaveLength(buttons.length); + expect(findModal()).not.toExist(); + }); + + describe('when first button clicked', () => { + beforeEach(() => { + initDeleteLabelModal(); + findJsHooks().item(0).click(); + }); + + it('does not replace js-containers with GlModal', () => { + expect(findJsHooks()).toHaveLength(buttons.length); + }); + + it('renders GlModal', () => { + expect(findModal()).toExist(); + }); + }); + + describe.each` + index + ${0} + ${1} + `(`when multiple buttons exist`, ({ index }) => { + beforeEach(() => { + initDeleteLabelModal(); + findJsHooks().item(index).click(); + }); + + it('correct props are passed to gl-modal', () => { + expect(findModal().querySelector('.modal-title').innerHTML).toContain( + buttons[index].labelName, + ); + expect(findModal().querySelector('.modal-body').innerHTML).toContain( + buttons[index].subjectName, + ); + expect(findModal().querySelector('.modal-footer .btn-danger').href).toContain( + buttons[index].destroyPath, + ); + }); + }); +}); diff --git a/spec/frontend/jira_connect/components/groups_list_spec.js b/spec/frontend/jira_connect/components/groups_list_spec.js index 5c645eccc0e..04f595ba23f 100644 --- a/spec/frontend/jira_connect/components/groups_list_spec.js +++ b/spec/frontend/jira_connect/components/groups_list_spec.js @@ -1,7 +1,7 @@ -import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { GlAlert, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; - import { fetchGroups } from '~/jira_connect/api'; import GroupsList from '~/jira_connect/components/groups_list.vue'; import GroupsListItem from '~/jira_connect/components/groups_list_item.vue'; @@ -12,20 +12,27 @@ jest.mock('~/jira_connect/api', () => { fetchGroups: jest.fn(), }; }); + +const mockGroupsPath = '/groups'; + describe('GroupsList', () => { let wrapper; const mockEmptyResponse = { data: [] }; const createComponent = (options = {}) => { - wrapper = shallowMount(GroupsList, { - ...options, - }); + wrapper = extendedWrapper( + shallowMount(GroupsList, { + provide: { + groupsPath: mockGroupsPath, + }, + ...options, + }), + ); }; afterEach(() => { wrapper.destroy(); - wrapper = null; }); const findGlAlert = () => wrapper.find(GlAlert); @@ -33,56 +40,72 @@ describe('GroupsList', () => { const findAllItems = () => wrapper.findAll(GroupsListItem); const findFirstItem = () => findAllItems().at(0); const findSecondItem = () => findAllItems().at(1); + const findSearchBox = () => wrapper.find(GlSearchBoxByType); + const findGroupsList = () => wrapper.findByTestId('groups-list'); - describe('isLoading is true', () => { + describe('when groups are loading', () => { it('renders loading icon', async () => { - fetchGroups.mockResolvedValue(mockEmptyResponse); + fetchGroups.mockReturnValue(new Promise(() => {})); createComponent(); - wrapper.setData({ isLoading: true }); await wrapper.vm.$nextTick(); expect(findGlLoadingIcon().exists()).toBe(true); }); }); - describe('error fetching groups', () => { + describe('when groups fetch fails', () => { it('renders error message', async () => { fetchGroups.mockRejectedValue(); createComponent(); await waitForPromises(); + expect(findGlLoadingIcon().exists()).toBe(false); expect(findGlAlert().exists()).toBe(true); expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.'); }); }); - describe('no groups returned', () => { + describe('with no groups returned', () => { it('renders empty state', async () => { fetchGroups.mockResolvedValue(mockEmptyResponse); createComponent(); await waitForPromises(); + expect(findGlLoadingIcon().exists()).toBe(false); expect(wrapper.text()).toContain('No available namespaces'); }); }); describe('with groups returned', () => { beforeEach(async () => { - fetchGroups.mockResolvedValue({ data: [mockGroup1, mockGroup2] }); + fetchGroups.mockResolvedValue({ + headers: { 'X-PAGE': 1, 'X-TOTAL': 2 }, + data: [mockGroup1, mockGroup2], + }); createComponent(); await waitForPromises(); }); it('renders groups list', () => { - expect(findAllItems().length).toBe(2); + expect(findAllItems()).toHaveLength(2); expect(findFirstItem().props('group')).toBe(mockGroup1); expect(findSecondItem().props('group')).toBe(mockGroup2); }); + it('sets GroupListItem `disabled` prop to `false`', () => { + findAllItems().wrappers.forEach((groupListItem) => { + expect(groupListItem.props('disabled')).toBe(false); + }); + }); + + it('does not set opacity of the groups list', () => { + expect(findGroupsList().classes()).not.toContain('gl-opacity-5'); + }); + it('shows error message on $emit from item', async () => { const errorMessage = 'error message'; @@ -93,5 +116,55 @@ describe('GroupsList', () => { expect(findGlAlert().exists()).toBe(true); expect(findGlAlert().text()).toContain(errorMessage); }); + + describe('when searching groups', () => { + const mockSearchTeam = 'mock search term'; + + describe('while groups are loading', () => { + beforeEach(async () => { + fetchGroups.mockClear(); + fetchGroups.mockReturnValue(new Promise(() => {})); + + findSearchBox().vm.$emit('input', mockSearchTeam); + await wrapper.vm.$nextTick(); + }); + + it('calls `fetchGroups` with search term', () => { + expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, { + page: 1, + perPage: 10, + search: mockSearchTeam, + }); + }); + + it('disables GroupListItems', async () => { + findAllItems().wrappers.forEach((groupListItem) => { + expect(groupListItem.props('disabled')).toBe(true); + }); + }); + + it('sets opacity of the groups list', () => { + expect(findGroupsList().classes()).toContain('gl-opacity-5'); + }); + + it('sets loading prop of ths search box', () => { + expect(findSearchBox().props('isLoading')).toBe(true); + }); + }); + + describe('when group search finishes loading', () => { + beforeEach(async () => { + fetchGroups.mockResolvedValue({ data: [mockGroup1] }); + findSearchBox().vm.$emit('input'); + + await waitForPromises(); + }); + + it('renders new groups list', () => { + expect(findAllItems()).toHaveLength(1); + expect(findFirstItem().props('group')).toBe(mockGroup1); + }); + }); + }); }); }); diff --git a/spec/frontend/vue_shared/components/delete_label_modal_spec.js b/spec/frontend/vue_shared/components/delete_label_modal_spec.js new file mode 100644 index 00000000000..3905690dab4 --- /dev/null +++ b/spec/frontend/vue_shared/components/delete_label_modal_spec.js @@ -0,0 +1,64 @@ +import { GlModal } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { stubComponent } from 'helpers/stub_component'; +import { TEST_HOST } from 'helpers/test_constants'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue'; + +const MOCK_MODAL_DATA = { + labelName: 'label 1', + subjectName: 'GitLab Org', + destroyPath: `${TEST_HOST}/1`, +}; + +describe('vue_shared/components/delete_label_modal', () => { + let wrapper; + + const createComponent = () => { + wrapper = extendedWrapper( + mount(DeleteLabelModal, { + propsData: { + selector: '.js-test-btn', + }, + stubs: { + GlModal: stubComponent(GlModal, { + template: + '', + }), + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findModal = () => wrapper.find(GlModal); + const findPrimaryModalButton = () => wrapper.findByTestId('delete-button'); + + describe('template', () => { + describe('when modal data is set', () => { + beforeEach(() => { + createComponent(); + wrapper.vm.labelName = MOCK_MODAL_DATA.labelName; + wrapper.vm.subjectName = MOCK_MODAL_DATA.subjectName; + wrapper.vm.destroyPath = MOCK_MODAL_DATA.destroyPath; + }); + + it('renders GlModal', () => { + expect(findModal().exists()).toBe(true); + }); + + it('displays the label name and subject name', () => { + expect(findModal().text()).toContain( + `${MOCK_MODAL_DATA.labelName} will be permanently deleted from ${MOCK_MODAL_DATA.subjectName}. This cannot be undone`, + ); + }); + + it('passes the destroyPath to the button', () => { + expect(findPrimaryModalButton().attributes('href')).toBe(MOCK_MODAL_DATA.destroyPath); + }); + }); + }); +}); diff --git a/spec/graphql/resolvers/blobs_resolver_spec.rb b/spec/graphql/resolvers/blobs_resolver_spec.rb new file mode 100644 index 00000000000..bc0344796ee --- /dev/null +++ b/spec/graphql/resolvers/blobs_resolver_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::BlobsResolver do + include GraphqlHelpers + + describe '.resolver_complexity' do + it 'adds one per path being resolved' do + control = described_class.resolver_complexity({}, child_complexity: 1) + + expect(described_class.resolver_complexity({ paths: %w[a b c] }, child_complexity: 1)) + .to eq(control + 3) + end + end + + describe '#resolve' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + + let(:repository) { project.repository } + let(:args) { { paths: paths, ref: ref } } + let(:paths) { [] } + let(:ref) { nil } + + subject(:resolve_blobs) { resolve(described_class, obj: repository, args: args, ctx: { current_user: user }) } + + context 'when unauthorized' do + it 'raises an exception' do + expect { resolve_blobs }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when authorized' do + before do + project.add_developer(user) + end + + context 'using no filter' do + it 'returns nothing' do + is_expected.to be_empty + end + end + + context 'using paths filter' do + let(:paths) { ['README.md'] } + + it 'returns the specified blobs for HEAD' do + is_expected.to contain_exactly(have_attributes(path: 'README.md')) + end + + context 'specifying a non-existent blob' do + let(:paths) { ['non-existent'] } + + it 'returns nothing' do + is_expected.to be_empty + end + end + + context 'specifying a different ref' do + let(:ref) { 'add-pdf-file' } + let(:paths) { ['files/pdf/test.pdf', 'README.md'] } + + it 'returns the specified blobs for that ref' do + is_expected.to contain_exactly( + have_attributes(path: 'files/pdf/test.pdf'), + have_attributes(path: 'README.md') + ) + end + end + end + end + end +end diff --git a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb index 69127c4b061..3d33e0b500d 100644 --- a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb +++ b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Resolvers::ProjectPipelineResolver do let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project, iid: '1234', sha: 'sha') } + let_it_be(:other_project_pipeline) { create(:ci_pipeline, project: project, iid: '1235', sha: 'sha2') } let_it_be(:other_pipeline) { create(:ci_pipeline) } let(:current_user) { create(:user) } @@ -23,6 +24,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do end it 'resolves pipeline for the passed iid' do + expect(Ci::PipelinesFinder) + .to receive(:new) + .with(project, current_user, iids: ['1234']) + .and_call_original + result = batch_sync do resolve_pipeline(project, { iid: '1234' }) end @@ -31,6 +37,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do end it 'resolves pipeline for the passed sha' do + expect(Ci::PipelinesFinder) + .to receive(:new) + .with(project, current_user, sha: ['sha']) + .and_call_original + result = batch_sync do resolve_pipeline(project, { sha: 'sha' }) end @@ -39,8 +50,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do end it 'keeps the queries under the threshold for iid' do - create(:ci_pipeline, project: project, iid: '1235') - control = ActiveRecord::QueryRecorder.new do batch_sync { resolve_pipeline(project, { iid: '1234' }) } end @@ -54,8 +63,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do end it 'keeps the queries under the threshold for sha' do - create(:ci_pipeline, project: project, sha: 'sha2') - control = ActiveRecord::QueryRecorder.new do batch_sync { resolve_pipeline(project, { sha: 'sha' }) } end diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb index bebae4c68cf..585cd657e35 100644 --- a/spec/graphql/resolvers/timelog_resolver_spec.rb +++ b/spec/graphql/resolvers/timelog_resolver_spec.rb @@ -9,51 +9,57 @@ RSpec.describe Resolvers::TimelogResolver do expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type) end - context "within a group" do + context "with a group" do let_it_be(:current_user) { create(:user) } - let(:group) { create(:group) } - let(:project) { create(:project, :public, group: group) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :public, group: group) } - before do + before_all do group.add_developer(current_user) project.add_developer(current_user) end + before do + group.clear_memoization(:timelogs) + end + describe '#resolve' do - let(:issue) { create(:issue, project: project) } - let(:issue2) { create(:issue, project: project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:issue2) { create(:issue, project: project) } + let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) } + let_it_be(:timelog2) { create(:issue_timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) } + let_it_be(:timelog3) { create(:issue_timelog, issue: issue2, spent_at: 10.days.ago) } + let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } } - let!(:timelog1) { create(:timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) } - let!(:timelog2) { create(:timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) } - let!(:timelog3) { create(:timelog, issue: issue2, spent_at: 10.days.ago) } it 'finds all timelogs within given dates' do - timelogs = resolve_timelogs(args) + timelogs = resolve_timelogs(**args) expect(timelogs).to contain_exactly(timelog1) end it 'return nothing when user has insufficient permissions' do + user = create(:user) group.add_guest(current_user) - expect(resolve_timelogs(args)).to be_empty + expect(resolve_timelogs(user: user, **args)).to be_empty end context 'when start_time and end_date are present' do let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } } it 'finds timelogs until the end of day of end_date' do - timelogs = resolve_timelogs(args) + timelogs = resolve_timelogs(**args) expect(timelogs).to contain_exactly(timelog1, timelog2) end end - context 'finds timelogs until the time specified on end_time' do + context 'when start_date and end_time are present' do let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } } it 'finds all timelogs within start_date and end_time' do - timelogs = resolve_timelogs(args) + timelogs = resolve_timelogs(**args) expect(timelogs).to contain_exactly(timelog1) end @@ -66,7 +72,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { {} } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /Start and End arguments must be present/) end end @@ -75,7 +81,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { { start_time: 6.days.ago } } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /Both Start and End arguments must be present/) end end @@ -84,7 +90,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { { end_time: 2.days.ago } } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /Both Start and End arguments must be present/) end end @@ -93,7 +99,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { { start_date: 6.days.ago } } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /Both Start and End arguments must be present/) end end @@ -102,7 +108,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { { end_date: 2.days.ago } } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /Both Start and End arguments must be present/) end end @@ -111,7 +117,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /Both Start and End arguments must be present/) end end @@ -120,7 +126,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /Both Start and End arguments must be present/) end end @@ -129,7 +135,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /Only Time or Date arguments must be present/) end end @@ -138,7 +144,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /Start argument must be before End argument/) end end @@ -147,7 +153,7 @@ RSpec.describe Resolvers::TimelogResolver do let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } } it 'returns correct error' do - expect {resolve_timelogs(args)} + expect { resolve_timelogs(**args) } .to raise_error(error_class, /The time range period cannot contain more than 60 days/) end end @@ -155,7 +161,8 @@ RSpec.describe Resolvers::TimelogResolver do end end - def resolve_timelogs(args = {}, context = { current_user: current_user }) + def resolve_timelogs(user: current_user, **args) + context = { current_user: user } resolve(described_class, obj: group, args: args, ctx: context) end end diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index 21fc530149c..6908a610aae 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status design_collection alert_management_alert severity current_user_todos moved moved_to - create_note_email] + create_note_email timelogs] fields.each do |field_name| expect(described_class).to have_graphql_field(field_name) diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb index e9199bd286e..a3bb7e502f2 100644 --- a/spec/graphql/types/repository_type_spec.rb +++ b/spec/graphql/types/repository_type_spec.rb @@ -12,4 +12,6 @@ RSpec.describe GitlabSchema.types['Repository'] do specify { expect(described_class).to have_graphql_field(:tree) } specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) } + + specify { expect(described_class).to have_graphql_field(:blobs) } end diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb index 9687d038162..2ea832f95dc 100644 --- a/spec/helpers/profiles_helper_spec.rb +++ b/spec/helpers/profiles_helper_spec.rb @@ -112,6 +112,46 @@ RSpec.describe ProfilesHelper do end end + describe "#ssh_key_expiration_tooltip" do + using RSpec::Parameterized::TableSyntax + + before do + allow(Key).to receive(:enforce_ssh_key_expiration_feature_available?).and_return(false) + end + + error_message = 'Key type is forbidden. Must be DSA, ECDSA, or ED25519' + + where(:error, :expired, :result) do + false | false | nil + true | false | error_message + false | true | 'Key usable beyond expiration date.' + true | true | error_message + end + + with_them do + let_it_be(:key) do + build(:personal_key) + end + + it do + key.expires_at = expired ? 2.days.ago : 2.days.from_now + key.errors.add(:base, error_message) if error + + expect(helper.ssh_key_expiration_tooltip(key)).to eq(result) + end + end + end + + describe "#ssh_key_expires_field_description" do + before do + allow(Key).to receive(:enforce_ssh_key_expiration_feature_available?).and_return(false) + end + + it 'returns the description' do + expect(helper.ssh_key_expires_field_description).to eq('Key can still be used after expiration.') + end + end + def stub_cas_omniauth_provider provider = OpenStruct.new( 'name' => 'cas3', diff --git a/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb new file mode 100644 index 00000000000..72a5bf8d011 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::Helpers do + let(:helper_class) do + Class.new do + include Gitlab::Ci::Pipeline::Chain::Helpers + + attr_accessor :pipeline, :command + + def initialize(pipeline, command) + self.pipeline = pipeline + self.command = command + end + end + end + + subject(:helper) { helper_class.new(pipeline, command) } + + let(:pipeline) { build(:ci_empty_pipeline) } + let(:command) { double(save_incompleted: true) } + let(:message) { 'message' } + + describe '.error' do + shared_examples 'error function' do + specify do + expect(pipeline).to receive(:drop!).with(drop_reason).and_call_original + expect(pipeline).to receive(:add_error_message).with(message).and_call_original + expect(pipeline).to receive(:ensure_project_iid!).twice.and_call_original + + subject.error(message, config_error: config_error, drop_reason: drop_reason) + + expect(pipeline.yaml_errors).to eq(yaml_error) + expect(pipeline.errors[:base]).to include(message) + end + end + + context 'when given a drop reason' do + context 'when config error is true' do + context 'sets the yaml error and overrides the drop reason' do + let(:drop_reason) { :config_error } + let(:config_error) { true } + let(:yaml_error) { message } + + it_behaves_like "error function" + end + end + + context 'when config error is false' do + context 'does not set the yaml error or override the drop reason' do + let(:drop_reason) { :size_limit_exceeded } + let(:config_error) { false } + let(:yaml_error) { nil } + + it_behaves_like "error function" + end + end + end + + context 'when the ci_pipeline_ensure_iid_on_drop feature flag is false' do + it 'does not ensure the project iid' do + stub_feature_flags(ci_pipeline_ensure_iid_on_drop: false) + expect(pipeline).to receive(:ensure_project_iid!).once + + subject.error(message, config_error: true) + end + end + end +end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 1553a989dba..b735ac7940b 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -407,13 +407,13 @@ RSpec.describe Gitlab::Database do expect(described_class.db_read_only?).to be_truthy end - it 'detects a read write database' do + it 'detects a read-write database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }]) expect(described_class.db_read_only?).to be_falsey end - it 'detects a read write database' do + it 'detects a read-write database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => false }]) expect(described_class.db_read_only?).to be_falsey diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb index 8a2395d70b2..8f898d898de 100644 --- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::HookData::IssueBuilder do let_it_be(:label) { create(:label) } let_it_be(:issue) { create(:labeled_issue, labels: [label], project: label.project) } + let(:builder) { described_class.new(issue) } describe '#build' do diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index fede7f273f1..0339faa9fcf 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::HookData::MergeRequestBuilder do let_it_be(:merge_request) { create(:merge_request) } + let(:builder) { described_class.new(merge_request) } describe '#build' do diff --git a/spec/lib/gitlab/hook_data/release_builder_spec.rb b/spec/lib/gitlab/hook_data/release_builder_spec.rb index b630780b162..449965f5df1 100644 --- a/spec/lib/gitlab/hook_data/release_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/release_builder_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::HookData::ReleaseBuilder do let_it_be(:project) { create(:project, :public, :repository) } + let(:release) { create(:release, project: project) } let(:builder) { described_class.new(release) } diff --git a/spec/models/concerns/has_timelogs_report_spec.rb b/spec/models/concerns/has_timelogs_report_spec.rb index 34d2000f96a..f694fc350ee 100644 --- a/spec/models/concerns/has_timelogs_report_spec.rb +++ b/spec/models/concerns/has_timelogs_report_spec.rb @@ -32,18 +32,16 @@ RSpec.describe HasTimelogsReport do end describe '#user_can_access_group_timelogs?' do - before do - group.add_developer(user) - end - it 'returns true if user can access group timelogs' do - expect(group.user_can_access_group_timelogs?(user)).to be_truthy + group.add_developer(user) + + expect(group).to be_user_can_access_group_timelogs(user) end it 'returns false if user has insufficient permissions' do group.add_guest(user) - expect(group.user_can_access_group_timelogs?(user)).to be_falsey + expect(group).not_to be_user_can_access_group_timelogs(user) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b964a18e148..991691a408a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -5795,16 +5795,34 @@ RSpec.describe Project, factory_default: :keep do end describe '#find_or_initialize_services' do - before do - allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity]) - allow(subject).to receive(:disabled_services).and_return(%w[prometheus]) + let_it_be(:subject) { create(:project) } + + it 'avoids N+1 database queries' do + control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services }.count + + expect(control_count).to be <= 4 end - it 'returns only enabled services' do - services = subject.find_or_initialize_services + it 'avoids N+1 database queries with more available services' do + allow(Service).to receive(:available_services_names).and_return(%w[pushover]) + control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services } - expect(services.count).to eq(2) - expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover']) + allow(Service).to receive(:available_services_names).and_call_original + expect { subject.find_or_initialize_services }.not_to exceed_query_limit(control_count) + end + + context 'with disabled services' do + before do + allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity]) + allow(subject).to receive(:disabled_services).and_return(%w[prometheus]) + end + + it 'returns only enabled services sorted' do + services = subject.find_or_initialize_services + + expect(services.size).to eq(2) + expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover']) + end end end diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb index e9019b55635..6a252b444f9 100644 --- a/spec/models/timelog_spec.rb +++ b/spec/models/timelog_spec.rb @@ -56,9 +56,9 @@ RSpec.describe Timelog do group = create(:group) subgroup = create(:group, parent: group) - create(:timelog, issue: create(:issue, project: create(:project))) - timelog1 = create(:timelog, issue: create(:issue, project: create(:project, group: group))) - timelog2 = create(:timelog, issue: create(:issue, project: create(:project, group: subgroup))) + create(:issue_timelog) + timelog1 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: group))) + timelog2 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: subgroup))) expect(described_class.for_issues_in_group(group)).to contain_exactly(timelog1, timelog2) end @@ -66,9 +66,9 @@ RSpec.describe Timelog do describe 'between_times' do it 'returns collection of timelogs within given times' do - create(:timelog, spent_at: 65.days.ago) - timelog1 = create(:timelog, spent_at: 15.days.ago) - timelog2 = create(:timelog, spent_at: 5.days.ago) + create(:issue_timelog, spent_at: 65.days.ago) + timelog1 = create(:issue_timelog, spent_at: 15.days.ago) + timelog2 = create(:issue_timelog, spent_at: 5.days.ago) timelogs = described_class.between_times(20.days.ago, 1.day.ago) expect(timelogs).to contain_exactly(timelog1, timelog2) diff --git a/spec/requests/api/graphql/group/timelogs_spec.rb b/spec/requests/api/graphql/group/timelogs_spec.rb index ca015a82148..6e21a73afa9 100644 --- a/spec/requests/api/graphql/group/timelogs_spec.rb +++ b/spec/requests/api/graphql/group/timelogs_spec.rb @@ -14,6 +14,7 @@ RSpec.describe 'Timelogs through GroupQuery' do let_it_be(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-13 14:00:00') } let_it_be(:timelog2) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 08:00:00') } let_it_be(:params) { { startTime: '2019-08-10 12:00:00', endTime: '2019-08-21 12:00:00' } } + let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] } before do @@ -34,11 +35,11 @@ RSpec.describe 'Timelogs through GroupQuery' do end it 'contains correct data', :aggregate_failures do - username = timelog_array.map {|data| data['user']['username'] } + username = timelog_array.map { |data| data['user']['username'] } spent_at = timelog_array.map { |data| data['spentAt'].to_time } time_spent = timelog_array.map { |data| data['timeSpent'] } - issue_title = timelog_array.map {|data| data['issue']['title'] } - milestone_title = timelog_array.map {|data| data['issue']['milestone']['title'] } + issue_title = timelog_array.map { |data| data['issue']['title'] } + milestone_title = timelog_array.map { |data| data['issue']['milestone']['title'] } expect(username).to eq([user.username]) expect(spent_at.first).to be_like_time(timelog1.spent_at) @@ -50,7 +51,7 @@ RSpec.describe 'Timelogs through GroupQuery' do context 'when arguments with no time are present' do let!(:timelog3) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 15:00:00') } let!(:timelog4) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-21 15:00:00') } - let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' }} + let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' } } it 'sets times as start of day and end of day' do expect(response).to have_gitlab_http_status(:ok) @@ -111,12 +112,10 @@ RSpec.describe 'Timelogs through GroupQuery' do } NODE - graphql_query_for("group", { "fullPath" => group.full_path }, - [query_graphql_field( - "timelogs", - timelog_params, - timelog_nodes - )] + graphql_query_for( + :group, + { full_path: group.full_path }, + query_graphql_field(:timelogs, timelog_params, timelog_nodes) ) end end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 9c915075c42..f93822825e0 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -5,14 +5,14 @@ require 'spec_helper' RSpec.describe 'getting an issue list for a project' do include GraphqlHelpers - let(:issues_data) { graphql_data['project']['issues']['edges'] } - let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:current_user) { create(:user) } let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) } let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) } let_it_be(:issues, reload: true) { [issue_a, issue_b] } + let(:issues_data) { graphql_data['project']['issues']['edges'] } + let(:fields) do <<~QUERY edges { @@ -76,7 +76,7 @@ RSpec.describe 'getting an issue list for a project' do end end - context 'no limit is provided' do + context 'when no limit is provided' do let(:issue_limit) { nil } it 'returns all issues' do @@ -143,13 +143,15 @@ RSpec.describe 'getting an issue list for a project' do let_it_be(:data_path) { [:project, :issues] } def pagination_query(params) - graphql_query_for(:project, { full_path: sort_project.full_path }, + graphql_query_for( + :project, + { full_path: sort_project.full_path }, query_graphql_field(:issues, params, "#{page_info} nodes { iid }") ) end def pagination_results_data(data) - data.map { |issue| issue.dig('iid').to_i } + data.map { |issue| issue['iid'].to_i } end context 'when sorting by due date' do @@ -189,27 +191,38 @@ RSpec.describe 'getting an issue list for a project' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :RELATIVE_POSITION_ASC } let(:first_param) { 2 } - let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] } + let(:expected_results) do + [ + relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, + relative_issue4.iid, relative_issue2.iid + ] + end end end end context 'when sorting by priority' do let_it_be(:sort_project) { create(:project, :public) } - let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) } - let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) } - let_it_be(:priority_label1) { create(:label, project: sort_project, priority: 1) } - let_it_be(:priority_label2) { create(:label, project: sort_project, priority: 5) } - let_it_be(:priority_issue1) { create(:issue, project: sort_project, labels: [priority_label1], milestone: late_milestone) } - let_it_be(:priority_issue2) { create(:issue, project: sort_project, labels: [priority_label2]) } - let_it_be(:priority_issue3) { create(:issue, project: sort_project, milestone: early_milestone) } - let_it_be(:priority_issue4) { create(:issue, project: sort_project) } + let_it_be(:on_project) { { project: sort_project } } + let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) } + let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) } + let_it_be(:priority_1) { create(:label, **on_project, priority: 1) } + let_it_be(:priority_2) { create(:label, **on_project, priority: 5) } + let_it_be(:priority_issue1) { create(:issue, **on_project, labels: [priority_1], milestone: late_milestone) } + let_it_be(:priority_issue2) { create(:issue, **on_project, labels: [priority_2]) } + let_it_be(:priority_issue3) { create(:issue, **on_project, milestone: early_milestone) } + let_it_be(:priority_issue4) { create(:issue, **on_project) } context 'when ascending' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :PRIORITY_ASC } let(:first_param) { 2 } - let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] } + let(:expected_results) do + [ + priority_issue3.iid, priority_issue1.iid, + priority_issue2.iid, priority_issue4.iid + ] + end end end @@ -217,7 +230,9 @@ RSpec.describe 'getting an issue list for a project' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :PRIORITY_DESC } let(:first_param) { 2 } - let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] } + let(:expected_results) do + [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] + end end end end @@ -275,7 +290,7 @@ RSpec.describe 'getting an issue list for a project' do end end - context 'fetching alert management alert' do + context 'when fetching alert management alert' do let(:fields) do <<~QUERY edges { @@ -297,7 +312,7 @@ RSpec.describe 'getting an issue list for a project' do it 'avoids N+1 queries' do control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) } - create(:alert_management_alert, :with_issue, project: project ) + create(:alert_management_alert, :with_issue, project: project) expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control) end @@ -312,7 +327,7 @@ RSpec.describe 'getting an issue list for a project' do end end - context 'fetching labels' do + context 'when fetching labels' do let(:fields) do <<~QUERY edges { @@ -362,7 +377,7 @@ RSpec.describe 'getting an issue list for a project' do end end - context 'fetching assignees' do + context 'when fetching assignees' do let(:fields) do <<~QUERY edges { @@ -420,9 +435,10 @@ RSpec.describe 'getting an issue list for a project' do query = graphql_query_for( :project, { full_path: project.full_path }, - query_graphql_field(:issues, search_params, [ + query_graphql_field( + :issues, search_params, query_graphql_field(:nodes, nil, requested_fields) - ]) + ) ) post_graphql(query, current_user: current_user) end @@ -448,5 +464,16 @@ RSpec.describe 'getting an issue list for a project' do include_examples 'N+1 query check' end + + context 'when requesting `timelogs`' do + let(:requested_fields) { 'timelogs { nodes { timeSpent } }' } + + before do + create_list(:issue_timelog, 2, issue: issue_a) + create(:issue_timelog, issue: issue_b) + end + + include_examples 'N+1 query check' + end end end diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index e07e2428f33..7fc1ef05fa7 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -299,6 +299,7 @@ RSpec.describe 'getting merge request listings nested in a project' do reviewers { nodes { username } } participants { nodes { username } } headPipeline { status } + timelogs { nodes { timeSpent } } SELECT end @@ -307,7 +308,7 @@ RSpec.describe 'getting merge request listings nested in a project' do query($first: Int) { project(fullPath: "#{project.full_path}") { mergeRequests(first: $first) { - nodes { #{mr_fields} } + nodes { iid #{mr_fields} } } } } @@ -324,6 +325,7 @@ RSpec.describe 'getting merge request listings nested in a project' do mr.assignees << current_user mr.reviewers << create(:user) mr.reviewers << current_user + mr.timelogs << create(:merge_request_timelog, merge_request: mr) end end @@ -345,7 +347,7 @@ RSpec.describe 'getting merge request listings nested in a project' do end def user_collection - { 'nodes' => all(match(a_hash_including('username' => be_present))) } + { 'nodes' => be_present.and(all(match(a_hash_including('username' => be_present)))) } end it 'returns appropriate results' do @@ -358,7 +360,8 @@ RSpec.describe 'getting merge request listings nested in a project' do 'assignees' => user_collection, 'reviewers' => user_collection, 'participants' => user_collection, - 'headPipeline' => { 'status' => be_present } + 'headPipeline' => { 'status' => be_present }, + 'timelogs' => { 'nodes' => be_one } ))) end diff --git a/spec/services/groups/auto_devops_service_spec.rb b/spec/services/groups/auto_devops_service_spec.rb index 3d89ee96823..486a99dd8df 100644 --- a/spec/services/groups/auto_devops_service_spec.rb +++ b/spec/services/groups/auto_devops_service_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Groups::AutoDevopsService, '#execute' do let_it_be(:group) { create(:group) } let_it_be(:user) { create(:user) } + let(:group_params) { { auto_devops_enabled: '0' } } let(:service) { described_class.new(group, user, group_params) } diff --git a/spec/services/groups/group_links/update_service_spec.rb b/spec/services/groups/group_links/update_service_spec.rb index 436cdf89a0f..82c4a10f15a 100644 --- a/spec/services/groups/group_links/update_service_spec.rb +++ b/spec/services/groups/group_links/update_service_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Groups::GroupLinks::UpdateService, '#execute' do let_it_be(:group) { create(:group, :private) } let_it_be(:shared_group) { create(:group, :private) } let_it_be(:project) { create(:project, group: shared_group) } + let(:group_member_user) { create(:user) } let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) } diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 19b746ade34..3a1197970f4 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -5,12 +5,14 @@ require 'spec_helper' RSpec.describe Groups::TransferService do let_it_be(:user) { create(:user) } let_it_be(:new_parent_group) { create(:group, :public) } + let!(:group_member) { create(:group_member, :owner, group: group, user: user) } let(:transfer_service) { described_class.new(group, user) } context 'handling packages' do let_it_be(:group) { create(:group, :public) } let_it_be(:new_group) { create(:group, :public) } + let(:project) { create(:project, :public, namespace: group) } before do @@ -272,6 +274,7 @@ RSpec.describe Groups::TransferService do context 'with a group integration' do let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') } + let(:new_created_integration) { Service.find_by(group: group) } context 'with an inherited integration' do diff --git a/spec/services/groups/update_shared_runners_service_spec.rb b/spec/services/groups/update_shared_runners_service_spec.rb index e2838c4ce0b..e941958eb8c 100644 --- a/spec/services/groups/update_shared_runners_service_spec.rb +++ b/spec/services/groups/update_shared_runners_service_spec.rb @@ -59,6 +59,7 @@ RSpec.describe Groups::UpdateSharedRunnersService do context 'disable shared Runners' do let_it_be(:group) { create(:group) } + let(:params) { { shared_runners_setting: 'disabled_and_unoverridable' } } it 'receives correct method and succeeds' do diff --git a/spec/services/ide/base_config_service_spec.rb b/spec/services/ide/base_config_service_spec.rb index debdc6e5809..ee57f2c18ec 100644 --- a/spec/services/ide/base_config_service_spec.rb +++ b/spec/services/ide/base_config_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ide::BaseConfigService do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:sha) { 'sha' } describe '#execute' do diff --git a/spec/services/ide/schemas_config_service_spec.rb b/spec/services/ide/schemas_config_service_spec.rb index 19e5ca9e87d..69ad9b5cbea 100644 --- a/spec/services/ide/schemas_config_service_spec.rb +++ b/spec/services/ide/schemas_config_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ide::SchemasConfigService do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:filename) { 'sample.yml' } let(:schema_content) { double(body: '{"title":"Sample schema"}') } diff --git a/spec/services/ide/terminal_config_service_spec.rb b/spec/services/ide/terminal_config_service_spec.rb index 2bfc8a7ff3c..483b6413be3 100644 --- a/spec/services/ide/terminal_config_service_spec.rb +++ b/spec/services/ide/terminal_config_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ide::TerminalConfigService do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:sha) { 'sha' } describe '#execute' do diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 16433d49ca1..80fe2474ecd 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Issues::BuildService do let_it_be(:project) { create(:project, :repository) } let_it_be(:developer) { create(:user) } let_it_be(:guest) { create(:user) } + let(:user) { developer } before_all do diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb index 9ceb4ffeec5..44180a322ca 100644 --- a/spec/services/issues/clone_service_spec.rb +++ b/spec/services/issues/clone_service_spec.rb @@ -242,6 +242,7 @@ RSpec.describe Issues::CloneService do context 'issue with a design', :clean_gitlab_redis_shared_state do let_it_be(:new_project) { create(:project) } + let!(:design) { create(:design, :with_lfs_file, issue: old_issue) } let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) } let(:subject) { clone_service.execute(old_issue, new_project) } diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index d362f4efb7c..2ae1edcc804 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -11,6 +11,7 @@ RSpec.describe Issues::CreateService do describe '#execute' do let_it_be(:assignee) { create(:user) } let_it_be(:milestone) { create(:milestone, project: project) } + let(:issue) { described_class.new(project, user, opts).execute } context 'when params are valid' do diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb index d199f825276..d04480bec18 100644 --- a/spec/services/issues/export_csv_service_spec.rb +++ b/spec/services/issues/export_csv_service_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Issues::ExportCsvService do let_it_be(:project) { create(:project, :public, group: group) } let_it_be(:issue) { create(:issue, project: project, author: user) } let_it_be(:bad_issue) { create(:issue, project: project, author: user) } + subject { described_class.new(Issue.all, project) } it 'renders csv to string' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index eb124f07900..2f29a2e2022 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -206,6 +206,7 @@ RSpec.describe Issues::MoveService do context 'issue with a design', :clean_gitlab_redis_shared_state do let_it_be(:new_project) { create(:project) } + let!(:design) { create(:design, :with_lfs_file, issue: old_issue) } let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) } let(:subject) { move_service.execute(old_issue, new_project) } diff --git a/spec/services/issues/related_branches_service_spec.rb b/spec/services/issues/related_branches_service_spec.rb index a8a1f95e800..c9c029bca4f 100644 --- a/spec/services/issues/related_branches_service_spec.rb +++ b/spec/services/issues/related_branches_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Issues::RelatedBranchesService do let_it_be(:developer) { create(:user) } let_it_be(:issue) { create(:issue) } + let(:user) { developer } subject { described_class.new(issue.project, user) } diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 24a2fa74b77..e0d6b9afcff 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -724,9 +724,7 @@ RSpec.describe Projects::CreateService, '#execute' do it 'cleans invalid record and logs warning', :aggregate_failures do invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json) - allow_next_instance_of(Project) do |instance| - allow(instance).to receive(:build_prometheus_service).and_return(invalid_service_record) - end + allow(PrometheusService).to receive(:new).and_return(invalid_service_record) expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })) project = create_project(user, opts) diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index afdb4c3115a..71cb2ebdc33 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -55,6 +55,12 @@ RSpec.describe Projects::UpdatePagesService do end end + it 'creates a temporary directory with the project and build ID' do + expect(Dir).to receive(:mktmpdir).with("project-#{project.id}-build-#{build.id}-", anything).and_call_original + + subject.execute + end + it "doesn't deploy to legacy storage if it's disabled" do allow(Settings.pages.local_store).to receive(:enabled).and_return(false) diff --git a/spec/views/profiles/keys/_form.html.haml_spec.rb b/spec/views/profiles/keys/_form.html.haml_spec.rb new file mode 100644 index 00000000000..62bb271bd9c --- /dev/null +++ b/spec/views/profiles/keys/_form.html.haml_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'profiles/keys/_form.html.haml' do + let_it_be(:key) { Key.new } + + let(:page) { Capybara::Node::Simple.new(rendered) } + + before do + assign(:key, key) + end + + context 'when the form partial is used' do + before do + allow(view).to receive(:ssh_key_expires_field_description).and_return('Key can still be used after expiration.') + + render + end + + it 'renders the form with the correct action' do + expect(page.find('form')['action']).to eq('/-/profile/keys') + end + + it 'has the key field', :aggregate_failures do + expect(rendered).to have_field('Key', type: 'textarea', placeholder: 'Typically starts with "ssh-ed25519 …" or "ssh-rsa …"') + expect(rendered).to have_text("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Do not paste your private SSH key, as that can compromise your identity.") + end + + it 'has the title field', :aggregate_failures do + expect(rendered).to have_field('Title', type: 'text', placeholder: 'e.g. My MacBook key') + expect(rendered).to have_text('Give your individual key a title.') + end + + it 'has the expires at field', :aggregate_failures do + expect(rendered).to have_field('Expires at', type: 'date') + expect(page.find_field('Expires at')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d")) + expect(rendered).to have_text('Key can still be used after expiration.') + end + + it 'has the validation warning', :aggregate_failures do + expect(rendered).to have_text("Oops, are you sure? Publicly visible private SSH keys can compromise your system.") + expect(rendered).to have_button('Yes, add it') + end + + it 'has the submit button' do + expect(rendered).to have_button('Add key') + end + end +end diff --git a/spec/views/profiles/keys/_key.html.haml_spec.rb b/spec/views/profiles/keys/_key.html.haml_spec.rb new file mode 100644 index 00000000000..a29b8ecc3d5 --- /dev/null +++ b/spec/views/profiles/keys/_key.html.haml_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'profiles/keys/_key.html.haml' do + let_it_be(:user) { create(:user) } + + before do + allow(view).to receive(:key).and_return(key) + allow(view).to receive(:is_admin).and_return(false) + end + + context 'when the key partial is used' do + let_it_be(:key) do + create(:personal_key, + user: user, + last_used_at: 7.days.ago, + expires_at: 2.days.from_now) + end + + it 'displays the correct values', :aggregate_failures do + render + + expect(rendered).to have_text(key.title) + expect(rendered).to have_css('[data-testid="key-icon"]') + expect(rendered).to have_text(key.fingerprint) + expect(rendered).to have_text(l(key.last_used_at, format: "%b %d, %Y")) + expect(rendered).to have_text(l(key.created_at, format: "%b %d, %Y")) + expect(rendered).to have_text(key.expires_at.to_date) + expect(response).to render_template(partial: 'shared/ssh_keys/_key_delete') + end + + context 'when the key has not been used' do + let_it_be(:key) do + create(:personal_key, + user: user, + last_used_at: nil) + end + + it 'renders "Never" for last used' do + render + + expect(rendered).to have_text('Last used: Never') + end + end + + context 'when the key does not have an expiration date' do + let_it_be(:key) do + create(:personal_key, + user: user, + expires_at: nil) + end + + it 'renders "Never" for expires' do + render + + expect(rendered).to have_text('Expires: Never') + end + end + + context 'when the key is not deletable' do + # Turns out key.can_delete? is only false for LDAP keys + # but LDAP keys don't exist outside EE + before do + allow(key).to receive(:can_delete?).and_return(false) + end + + it 'does not render the partial' do + render + + expect(response).not_to render_template(partial: 'shared/ssh_keys/_key_delete') + end + end + + context 'icon tooltip' do + using RSpec::Parameterized::TableSyntax + + where(:valid, :expiry, :result) do + false | 2.days.from_now | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519' + false | 2.days.ago | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519' + true | 2.days.ago | 'Key usable beyond expiration date.' + true | 2.days.from_now | '' + end + + with_them do + let_it_be(:key) do + create(:personal_key, user: user) + end + + it 'renders the correct icon', :aggregate_failures do + unless valid + stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE) + end + + key.expires_at = expiry + + render + + if result.empty? + expect(rendered).to have_css('[data-testid="key-icon"]') + else + expect(rendered).to have_css('[data-testid="warning-solid-icon"]') + expect(rendered).to have_selector("span.has-tooltip[title='#{result}']") + end + end + end + end + end +end diff --git a/spec/workers/projects/post_creation_worker_spec.rb b/spec/workers/projects/post_creation_worker_spec.rb index 06e824f1530..b15b7b76b56 100644 --- a/spec/workers/projects/post_creation_worker_spec.rb +++ b/spec/workers/projects/post_creation_worker_spec.rb @@ -64,10 +64,7 @@ RSpec.describe Projects::PostCreationWorker do it 'cleans invalid record and logs warning', :aggregate_failures do invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json) - - allow_next_found_instance_of(Project) do |instance| - allow(instance).to receive(:build_prometheus_service).and_return(invalid_service_record) - end + allow(PrometheusService).to receive(:new).and_return(invalid_service_record) expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice subject diff --git a/spec/workers/repository_check/dispatch_worker_spec.rb b/spec/workers/repository_check/dispatch_worker_spec.rb index 5e1bc76ec8e..829abc7d895 100644 --- a/spec/workers/repository_check/dispatch_worker_spec.rb +++ b/spec/workers/repository_check/dispatch_worker_spec.rb @@ -42,5 +42,12 @@ RSpec.describe RepositoryCheck::DispatchWorker do subject.perform end + + it 'logs unhealthy shards' do + log_data = { message: "Excluding unhealthy shards", failed_checks: [{ labels: { shard: unhealthy_shard_name }, message: '14:Connect Failed', status: 'failed' }], class: described_class.name } + expect(Gitlab::AppLogger).to receive(:error).with(a_hash_including(log_data)) + + subject.perform + end end end
- {{ - s__('Integrations|You must have owner or maintainer permissions to link namespaces.') - }} -
+ {{ s__('Integrations|You must have owner or maintainer permissions to link namespaces.') }} +