diff options
71 files changed, 1038 insertions, 308 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9c94cb46e74..e85eb1bfd2d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -729,8 +729,6 @@ Rails/SaveBang: - 'ee/spec/models/merge_train_spec.rb' - 'spec/models/packages/package_spec.rb' - 'ee/spec/models/project_ci_cd_setting_spec.rb' - - 'ee/spec/models/project_services/github_service_spec.rb' - - 'ee/spec/models/project_services/jenkins_service_spec.rb' - 'ee/spec/models/project_spec.rb' - 'ee/spec/models/protected_environment_spec.rb' - 'ee/spec/models/repository_spec.rb' @@ -1113,12 +1111,6 @@ Rails/SaveBang: - 'spec/models/pages_domain_spec.rb' - 'spec/models/project_auto_devops_spec.rb' - 'spec/models/project_feature_spec.rb' - - 'spec/models/project_services/bamboo_service_spec.rb' - - 'spec/models/project_services/buildkite_service_spec.rb' - - 'spec/models/project_services/jira_service_spec.rb' - - 'spec/models/project_services/packagist_service_spec.rb' - - 'spec/models/project_services/pipelines_email_service_spec.rb' - - 'spec/models/project_services/teamcity_service_spec.rb' - 'spec/models/project_spec.rb' - 'spec/models/project_team_spec.rb' - 'spec/models/protectable_dropdown_spec.rb' diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js deleted file mode 100644 index 1c95bc5081c..00000000000 --- a/app/assets/javascripts/ajax_loading_spinner.js +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'jquery'; - -export default class AjaxLoadingSpinner { - static init() { - const $elements = $('.js-ajax-loading-spinner'); - - $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); - $elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete); - } - - static ajaxBeforeSend(e) { - e.target.setAttribute('disabled', ''); - const iconElement = e.target.querySelector('i'); - // get first fa- icon - const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g)[0]; - iconElement.dataset.icon = originalIcon; - AjaxLoadingSpinner.toggleLoadingIcon(iconElement); - $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); - } - - static ajaxComplete(e) { - e.target.removeAttribute('disabled'); - const iconElement = e.target.querySelector('i'); - AjaxLoadingSpinner.toggleLoadingIcon(iconElement); - $(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete); - } - - static toggleLoadingIcon(iconElement) { - const { classList } = iconElement; - classList.toggle(iconElement.dataset.icon); - classList.toggle('gl-spinner'); - classList.toggle('gl-spinner-orange'); - classList.toggle('gl-spinner-sm'); - } -} diff --git a/app/assets/javascripts/branches/ajax_loading_spinner.js b/app/assets/javascripts/branches/ajax_loading_spinner.js new file mode 100644 index 00000000000..79f4f919f3d --- /dev/null +++ b/app/assets/javascripts/branches/ajax_loading_spinner.js @@ -0,0 +1,31 @@ +import $ from 'jquery'; + +export default class AjaxLoadingSpinner { + static init() { + const $elements = $('.js-ajax-loading-spinner'); + $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); + } + + static ajaxBeforeSend(e) { + const button = e.target; + const newButton = document.createElement('button'); + newButton.classList.add('btn', 'btn-default', 'disabled', 'gl-button'); + newButton.setAttribute('disabled', 'disabled'); + + const spinner = document.createElement('span'); + spinner.classList.add('align-text-bottom', 'gl-spinner', 'gl-spinner-sm', 'gl-spinner-orange'); + newButton.appendChild(spinner); + + button.classList.add('hidden'); + button.parentNode.insertBefore(newButton, button.nextSibling); + + $(button).one('ajax:error', () => { + newButton.remove(); + button.classList.remove('hidden'); + }); + + $(button).one('ajax:success', () => { + $(button).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); + }); + } +} diff --git a/app/assets/javascripts/packages/details/components/additional_metadata.vue b/app/assets/javascripts/packages/details/components/additional_metadata.vue index a3de6dd46c7..76e0976ac05 100644 --- a/app/assets/javascripts/packages/details/components/additional_metadata.vue +++ b/app/assets/javascripts/packages/details/components/additional_metadata.vue @@ -1,7 +1,7 @@ <script> import { GlLink, GlSprintf } from '@gitlab/ui'; import { s__ } from '~/locale'; -import DetailsRow from '~/registry/shared/components/details_row.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import { generateConanRecipe } from '../utils'; import { PackageType } from '../../shared/constants'; diff --git a/app/assets/javascripts/packages/details/components/package_history.vue b/app/assets/javascripts/packages/details/components/package_history.vue index 96ce106884d..413ab1d15cb 100644 --- a/app/assets/javascripts/packages/details/components/package_history.vue +++ b/app/assets/javascripts/packages/details/components/package_history.vue @@ -2,7 +2,7 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { s__ } from '~/locale'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import HistoryElement from './history_element.vue'; +import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; export default { name: 'PackageHistory', @@ -16,7 +16,7 @@ export default { components: { GlLink, GlSprintf, - HistoryElement, + HistoryItem, TimeAgoTooltip, }, props: { @@ -46,7 +46,7 @@ export default { <div class="issuable-discussion"> <h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3> <ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline"> - <history-element icon="clock" data-testid="created-on"> + <history-item icon="clock" data-testid="created-on"> <gl-sprintf :message="$options.i18n.createdOn"> <template #name> <strong>{{ packageEntity.name }}</strong> @@ -58,8 +58,8 @@ export default { <time-ago-tooltip :time="packageEntity.created_at" /> </template> </gl-sprintf> - </history-element> - <history-element icon="pencil" data-testid="updated-at"> + </history-item> + <history-item icon="pencil" data-testid="updated-at"> <gl-sprintf :message="$options.i18n.updatedAtText"> <template #name> <strong>{{ packageEntity.name }}</strong> @@ -71,9 +71,9 @@ export default { <time-ago-tooltip :time="packageEntity.updated_at" /> </template> </gl-sprintf> - </history-element> + </history-item> <template v-if="packagePipeline"> - <history-element icon="commit" data-testid="commit"> + <history-item icon="commit" data-testid="commit"> <gl-sprintf :message="$options.i18n.commitText"> <template #link> <gl-link :href="packagePipeline.project.commit_url">{{ @@ -84,8 +84,8 @@ export default { <strong>{{ packagePipeline.ref }}</strong> </template> </gl-sprintf> - </history-element> - <history-element icon="pipeline" data-testid="pipeline"> + </history-item> + <history-item icon="pipeline" data-testid="pipeline"> <gl-sprintf :message="$options.i18n.pipelineText"> <template #link> <gl-link :href="packagePipeline.project.pipeline_url" @@ -97,9 +97,9 @@ export default { </template> <template #author>{{ packagePipeline.user.name }}</template> </gl-sprintf> - </history-element> + </history-item> </template> - <history-element icon="package" data-testid="published"> + <history-item icon="package" data-testid="published"> <gl-sprintf :message="$options.i18n.publishText"> <template #project> <strong>{{ projectName }}</strong> @@ -108,7 +108,7 @@ export default { <time-ago-tooltip :time="packageEntity.created_at" /> </template> </gl-sprintf> - </history-element> + </history-item> </ul> </div> </template> diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index 37e8c75f299..623d0a10606 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -1,4 +1,4 @@ -import AjaxLoadingSpinner from '~/ajax_loading_spinner'; +import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner'; import DeleteModal from '~/branches/branches_delete_modal'; import initDiverganceGraph from '~/branches/divergence_graph'; diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue index a4c497d0f59..661213733ac 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue @@ -7,7 +7,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { formatDate } from '~/lib/utils/datetime_utility'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import DeleteButton from '../delete_button.vue'; -import DetailsRow from '~/registry/shared/components/details_row.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import { REMOVE_TAG_BUTTON_TITLE, DIGEST_LABEL, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue index 54e1e9e9bc5..506d566cf70 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue @@ -1,5 +1,6 @@ <script> /* eslint-disable vue/no-v-html */ +import { GlLoadingIcon } from '@gitlab/ui'; import { sprintf, s__ } from '~/locale'; import statusCodes from '~/lib/utils/http_status'; import { bytesToMiB } from '~/lib/utils/number_utils'; @@ -11,6 +12,7 @@ export default { name: 'MemoryUsage', components: { MemoryGraph, + GlLoadingIcon, }, props: { metricsUrl: { @@ -156,8 +158,9 @@ export default { <template> <div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage"> <p v-if="shouldShowLoading" class="usage-info js-usage-info usage-info-loading"> - <i class="fa fa-spinner fa-spin usage-info-load-spinner" aria-hidden="true"> </i - >{{ s__('mrWidget|Loading deployment statistics') }} + <gl-loading-icon class="usage-info-load-spinner" />{{ + s__('mrWidget|Loading deployment statistics') + }} </p> <p v-if="shouldShowMemoryGraph" diff --git a/app/assets/javascripts/registry/shared/components/details_row.vue b/app/assets/javascripts/vue_shared/components/registry/details_row.vue index 2e245fadead..2e245fadead 100644 --- a/app/assets/javascripts/registry/shared/components/details_row.vue +++ b/app/assets/javascripts/vue_shared/components/registry/details_row.vue diff --git a/app/assets/javascripts/packages/details/components/history_element.vue b/app/assets/javascripts/vue_shared/components/registry/history_item.vue index 8a51c1528cf..a60b630b207 100644 --- a/app/assets/javascripts/packages/details/components/history_element.vue +++ b/app/assets/javascripts/vue_shared/components/registry/history_item.vue @@ -3,12 +3,11 @@ import { GlIcon } from '@gitlab/ui'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; export default { - name: 'HistoryElement', + name: 'HistoryItem', components: { GlIcon, TimelineEntryItem, }, - props: { icon: { type: String, @@ -29,7 +28,9 @@ export default { <slot></slot> </span> </div> - <div class="note-body"></div> + <div class="note-body"> + <slot name="body"></slot> + </div> </div> </timeline-entry-item> </template> diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6226685d93d..2e3e725712b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -919,12 +919,12 @@ } .issuable-todo-btn { - .fa-spinner { + .gl-spinner { display: none; } &.is-loading { - .fa-spinner { + .gl-spinner { display: inline-block; } diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index d21d96fd527..2f06cd84ee5 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -55,8 +55,7 @@ module SendFileUpload def image_scaling_request?(file_upload) avatar_safe_for_scaling?(file_upload) && scaling_allowed_by_feature_flags?(file_upload) && - valid_image_scaling_width? && - current_user + valid_image_scaling_width? end def avatar_safe_for_scaling?(file_upload) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 55170cbfa6b..e8ea39d7ffc 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -4,7 +4,7 @@ module IssuesHelper def issue_css_classes(issue) classes = ["issue"] classes << "closed" if issue.closed? - classes << "today" if issue.today? + classes << "today" if issue.new? classes << "user-can-drag" if @sort == 'relative_position' classes.join(' ') end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 46cd9649c2d..a6986029f0d 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -385,8 +385,12 @@ module Issuable Date.today == created_at.to_date end + def created_hours_ago + (Time.now.utc.to_i - created_at.utc.to_i) / 3600 + end + def new? - today? && created_at == updated_at + created_hours_ago < 24 end def open? diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index cf1ef6a9710..f88fb806b97 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -17,8 +17,6 @@ module Issues Issues::CloseService end - private - NO_REBALANCING_NEEDED = ((RelativePositioning::MIN_POSITION * 0.9999)..(RelativePositioning::MAX_POSITION * 0.9999)).freeze def rebalance_if_needed(issue) @@ -32,6 +30,8 @@ module Issues IssueRebalancingWorker.perform_async(nil, issue.project_id) end + private + def create_assignee_note(issue, old_assignees) SystemNoteService.change_issuable_assignees( issue, issue.project, current_user, old_assignees) diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index d9209191c1c..297a6b2ecdc 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -16,12 +16,12 @@ module Issues def before_create(issue) spam_check(issue, current_user, action: :create) - issue.move_to_end # current_user (defined in BaseService) is not available within run_after_commit block user = current_user issue.run_after_commit do NewIssueWorker.perform_async(issue.id, user.id) + IssuePlacementWorker.perform_async(issue.id) end end @@ -30,7 +30,6 @@ module Issues user_agent_detail_service.create resolve_discussions_with_issue(issuable) delete_milestone_total_issue_counter_cache(issuable.milestone) - rebalance_if_needed(issuable) super end diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index ed7dbdeae93..020a4361203 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -50,25 +50,25 @@ - if can?(current_user, :push_code, @project) - if branch.name == @project.repository.root_ref - %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", + %button{ class: "btn btn-remove remove-row has-tooltip disabled", disabled: true, title: s_('Branches|The default branch cannot be deleted') } - = icon("trash-o") + = sprite_icon("remove") - elsif protected_branch?(@project, branch) - if can?(current_user, :push_to_delete_protected_branch, @project) - %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", + %button{ class: "btn btn-remove remove-row has-tooltip", title: s_('Branches|Delete protected branch'), data: { toggle: "modal", target: "#modal-delete-branch", delete_path: project_branch_path(@project, branch.name), branch_name: branch.name, is_merged: ("true" if merged) } } - = icon("trash-o") + = sprite_icon("remove") - else - %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", + %button{ class: "btn btn-remove remove-row has-tooltip disabled", disabled: true, title: s_('Branches|Only a project maintainer or owner can delete a protected branch') } - = icon("trash-o") + = sprite_icon("remove") - else = link_to project_branch_path(@project, branch.name), class: "btn btn-remove remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip", @@ -77,4 +77,4 @@ data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } }, remote: true, 'aria-label' => s_('Branches|Delete branch') do - = icon("trash-o") + = sprite_icon("remove") diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c4e8a2d51ee..b336ab3eb00 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -63,7 +63,7 @@ // Fallback while content is loading .title.hide-collapsed = _('Time tracking') - = icon('spinner spin', 'aria-hidden': 'true') + = loading_icon - if issuable_sidebar.has_key?(:due_date) .block.due_date .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) } diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml index de4df016cfb..7b5926fc186 100644 --- a/app/views/shared/issuable/_sidebar_todo.html.haml +++ b/app/views/shared/issuable/_sidebar_todo.html.haml @@ -12,4 +12,4 @@ data: todo_button_data } %span.issuable-todo-inner.js-issuable-todo-inner< = is_collapsed ? button_icon : button_title - = icon('spin spinner', 'aria-hidden': 'true') + = loading_icon diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index dd743bd6ac4..86f20275c3b 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1452,6 +1452,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: issue_placement + :feature_category: :issue_tracking + :has_external_dependencies: + :urgency: :high + :resource_boundary: :cpu + :weight: 2 + :idempotent: true + :tags: [] - :name: issue_rebalancing :feature_category: :issue_tracking :has_external_dependencies: diff --git a/app/workers/issue_placement_worker.rb b/app/workers/issue_placement_worker.rb new file mode 100644 index 00000000000..65bf3394153 --- /dev/null +++ b/app/workers/issue_placement_worker.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class IssuePlacementWorker + include ApplicationWorker + + idempotent! + feature_category :issue_tracking + urgency :high + worker_resource_boundary :cpu + weight 2 + + # Move at most the most recent 100 issues + QUERY_LIMIT = 100 + + # rubocop: disable CodeReuse/ActiveRecord + def perform(issue_id) + issue = Issue.id_in(issue_id).first + return unless issue + + # Move the most recent 100 unpositioned items to the end. + # This is to deal with out-of-order execution of the worker, + # while preserving creation order. + to_place = Issue + .relative_positioning_query_base(issue) + .where(relative_position: nil) + .order({ created_at: :desc }, { id: :desc }) + .limit(QUERY_LIMIT) + + Issue.move_nulls_to_end(to_place.to_a.reverse) + Issues::BaseService.new(nil).rebalance_if_needed(to_place.max_by(&:relative_position)) + rescue RelativePositioning::NoSpaceLeft => e + Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id) + IssueRebalancingWorker.perform_async(nil, issue.project_id) + end + # rubocop: enable CodeReuse/ActiveRecord +end diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb index e0e28767f8d..be9a168c3f6 100644 --- a/app/workers/new_issue_worker.rb +++ b/app/workers/new_issue_worker.rb @@ -12,8 +12,8 @@ class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker def perform(issue_id, user_id) return unless objects_found?(issue_id, user_id) - EventCreateService.new.open_issue(issuable, user) - NotificationService.new.new_issue(issuable, user) + ::EventCreateService.new.open_issue(issuable, user) + ::NotificationService.new.new_issue(issuable, user) issuable.create_cross_references!(user) end diff --git a/changelogs/unreleased/202267-migrate-spinner-for-app-assets-javascripts-vue_merge_request_widge.yml b/changelogs/unreleased/202267-migrate-spinner-for-app-assets-javascripts-vue_merge_request_widge.yml new file mode 100644 index 00000000000..56976945c94 --- /dev/null +++ b/changelogs/unreleased/202267-migrate-spinner-for-app-assets-javascripts-vue_merge_request_widge.yml @@ -0,0 +1,5 @@ +--- +title: Migrate '.fa-spinner' to '.spinner' for 'app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue' +merge_request: 41142 +author: Gilang Gumilar +type: changed diff --git a/changelogs/unreleased/202590-migrate-spinner-for-app-views-shared-issuable.yml b/changelogs/unreleased/202590-migrate-spinner-for-app-views-shared-issuable.yml new file mode 100644 index 00000000000..d5ba511c735 --- /dev/null +++ b/changelogs/unreleased/202590-migrate-spinner-for-app-views-shared-issuable.yml @@ -0,0 +1,5 @@ +--- +title: Migrate '.fa-spinner' to '.spinner' for 'app/views/shared/issuable' +merge_request: 41132 +author: Gilang Gumilar +type: changed diff --git a/changelogs/unreleased/205578-add-package-count-to-usage-data-28days.yml b/changelogs/unreleased/205578-add-package-count-to-usage-data-28days.yml new file mode 100644 index 00000000000..9e162873f09 --- /dev/null +++ b/changelogs/unreleased/205578-add-package-count-to-usage-data-28days.yml @@ -0,0 +1,5 @@ +--- +title: Adds monthly package data to usage ping +merge_request: 40452 +author: +type: added diff --git a/changelogs/unreleased/ajk-relative-positioning-async-move-to-end.yml b/changelogs/unreleased/ajk-relative-positioning-async-move-to-end.yml new file mode 100644 index 00000000000..f10b774d659 --- /dev/null +++ b/changelogs/unreleased/ajk-relative-positioning-async-move-to-end.yml @@ -0,0 +1,5 @@ +--- +title: Ensure issue creation is not blocked by positioning +merge_request: 41313 +author: +type: fixed diff --git a/changelogs/unreleased/dz-improve-new-issue-highlight.yml b/changelogs/unreleased/dz-improve-new-issue-highlight.yml new file mode 100644 index 00000000000..5b2cc72971a --- /dev/null +++ b/changelogs/unreleased/dz-improve-new-issue-highlight.yml @@ -0,0 +1,5 @@ +--- +title: Change logic behind new issues highlight +merge_request: 41150 +author: +type: changed diff --git a/changelogs/unreleased/leipert-ajax-spinner-icons.yml b/changelogs/unreleased/leipert-ajax-spinner-icons.yml new file mode 100644 index 00000000000..a934daa0c87 --- /dev/null +++ b/changelogs/unreleased/leipert-ajax-spinner-icons.yml @@ -0,0 +1,5 @@ +--- +title: Change icon for branch delete button +merge_request: 39968 +author: +type: changed diff --git a/changelogs/unreleased/rails-save-bang-21.yml b/changelogs/unreleased/rails-save-bang-21.yml new file mode 100644 index 00000000000..4bf6d0c4520 --- /dev/null +++ b/changelogs/unreleased/rails-save-bang-21.yml @@ -0,0 +1,5 @@ +--- +title: Fix Rails/SaveBang offenses for *spec/models/project_services* +merge_request: 41320 +author: Rajendra Kadam +type: fixed diff --git a/config/feature_flags/development/security-on-demand-scans-site-validation.yml b/config/feature_flags/development/security_on_demand_scans_site_validation.yml index 381c173b1bf..27ec926d9ac 100644 --- a/config/feature_flags/development/security-on-demand-scans-site-validation.yml +++ b/config/feature_flags/development/security_on_demand_scans_site_validation.yml @@ -1,5 +1,5 @@ --- -name: security-on-demand-scans-site-validation +name: security_on_demand_scans_site_validation introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40685 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241815 group: group::dynamic analysis diff --git a/config/initializers/carrierwave_patch.rb b/config/initializers/carrierwave_patch.rb index 53fba307926..ad3ff36138f 100644 --- a/config/initializers/carrierwave_patch.rb +++ b/config/initializers/carrierwave_patch.rb @@ -7,7 +7,9 @@ require "carrierwave/storage/fog" # # This patch also incorporates # https://github.com/carrierwaveuploader/carrierwave/pull/2375 to -# provide Azure support. This is already in CarrierWave v2.1.x, but +# provide Azure support +# and https://github.com/carrierwaveuploader/carrierwave/pull/2397 to +# support custom expire_at. This is already in CarrierWave v2.1.x, but # upgrading this gem is a significant task: # https://gitlab.com/gitlab-org/gitlab/-/issues/216067 module CarrierWave @@ -28,7 +30,7 @@ module CarrierWave # avoid a get by using local references local_directory = connection.directories.new(key: @uploader.fog_directory) local_file = local_directory.files.new(key: path) - expire_at = ::Fog::Time.now + @uploader.fog_authenticated_url_expiration + expire_at = options[:expire_at] || ::Fog::Time.now + @uploader.fog_authenticated_url_expiration case @uploader.fog_credentials[:provider] when 'AWS', 'Google' # Older versions of fog-google do not support options as a parameter diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 0452a9b6621..bb43e61775b 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -138,6 +138,8 @@ - 2 - - irker - 1 +- - issue_placement + - 2 - - issue_rebalancing - 1 - - jira_connect diff --git a/db/migrate/20200901203055_add_id_created_at_index_to_packages.rb b/db/migrate/20200901203055_add_id_created_at_index_to_packages.rb new file mode 100644 index 00000000000..d92309e3fef --- /dev/null +++ b/db/migrate/20200901203055_add_id_created_at_index_to_packages.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIdCreatedAtIndexToPackages < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + INDEX_NAME = 'index_packages_packages_on_id_and_created_at' + + def up + add_concurrent_index :packages_packages, [:id, :created_at], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name(:packages_packages, INDEX_NAME) + end +end diff --git a/db/schema_migrations/20200901203055 b/db/schema_migrations/20200901203055 new file mode 100644 index 00000000000..166f9069f40 --- /dev/null +++ b/db/schema_migrations/20200901203055 @@ -0,0 +1 @@ +eb13fb285ac9af83bbc66397a5352a824575ad4af93178b98fbfc1be2e11ce8b
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 893cb8d9e31..754fbac8dd1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20463,6 +20463,8 @@ CREATE INDEX index_packages_package_files_on_package_id_and_file_name ON public. CREATE INDEX index_packages_packages_on_creator_id ON public.packages_packages USING btree (creator_id); +CREATE INDEX index_packages_packages_on_id_and_created_at ON public.packages_packages USING btree (id, created_at); + CREATE INDEX index_packages_packages_on_name_trigram ON public.packages_packages USING gin (name public.gin_trgm_ops); CREATE INDEX index_packages_packages_on_project_id_and_created_at ON public.packages_packages USING btree (project_id, created_at); diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 90d43aa3531..e1321deea21 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -6016,7 +6016,7 @@ type GeoNode { name: String """ - Package file registries of the GeoNode. Available only when feature flag `geo_self_service_framework` is enabled + Package file registries of the GeoNode. Available only when feature flag `geo_package_file_replication` is enabled """ packageFileRegistries( """ @@ -6096,6 +6096,37 @@ type GeoNode { syncObjectStorage: Boolean """ + Find terraform state registries on this Geo node. Available only when feature + flag `geo_terraform_state_replication` is enabled + """ + terraformStateRegistries( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Filters registries by their ID + """ + ids: [ID!] + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): TerraformStateRegistryConnection + + """ The user-facing URL for this Geo node """ url: String @@ -15713,6 +15744,86 @@ type TaskCompletionStatus { } """ +Represents the sync and verification state of a terraform state +""" +type TerraformStateRegistry { + """ + Timestamp when the TerraformStateRegistry was created + """ + createdAt: Time + + """ + ID of the TerraformStateRegistry + """ + id: ID! + + """ + Error message during sync of the TerraformStateRegistry + """ + lastSyncFailure: String + + """ + Timestamp of the most recent successful sync of the TerraformStateRegistry + """ + lastSyncedAt: Time + + """ + Timestamp after which the TerraformStateRegistry should be resynced + """ + retryAt: Time + + """ + Number of consecutive failed sync attempts of the TerraformStateRegistry + """ + retryCount: Int + + """ + Sync state of the TerraformStateRegistry + """ + state: RegistryState + + """ + ID of the TerraformState + """ + terraformStateId: ID! +} + +""" +The connection type for TerraformStateRegistry. +""" +type TerraformStateRegistryConnection { + """ + A list of edges. + """ + edges: [TerraformStateRegistryEdge] + + """ + A list of nodes. + """ + nodes: [TerraformStateRegistry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type TerraformStateRegistryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: TerraformStateRegistry +} + +""" Represents a requirement test report. """ type TestReport { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index a7f15dc6a35..36bc3f6e83d 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -16819,7 +16819,7 @@ }, { "name": "packageFileRegistries", - "description": "Package file registries of the GeoNode. Available only when feature flag `geo_self_service_framework` is enabled", + "description": "Package file registries of the GeoNode. Available only when feature flag `geo_package_file_replication` is enabled", "args": [ { "name": "ids", @@ -17020,6 +17020,77 @@ "deprecationReason": null }, { + "name": "terraformStateRegistries", + "description": "Find terraform state registries on this Geo node. Available only when feature flag `geo_terraform_state_replication` is enabled", + "args": [ + { + "name": "ids", + "description": "Filters registries by their ID", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TerraformStateRegistryConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "url", "description": "The user-facing URL for this Geo node", "args": [ @@ -46324,6 +46395,251 @@ }, { "kind": "OBJECT", + "name": "TerraformStateRegistry", + "description": "Represents the sync and verification state of a terraform state", + "fields": [ + { + "name": "createdAt", + "description": "Timestamp when the TerraformStateRegistry was created", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "ID of the TerraformStateRegistry", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastSyncFailure", + "description": "Error message during sync of the TerraformStateRegistry", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastSyncedAt", + "description": "Timestamp of the most recent successful sync of the TerraformStateRegistry", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "retryAt", + "description": "Timestamp after which the TerraformStateRegistry should be resynced", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "retryCount", + "description": "Number of consecutive failed sync attempts of the TerraformStateRegistry", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": "Sync state of the TerraformStateRegistry", + "args": [ + + ], + "type": { + "kind": "ENUM", + "name": "RegistryState", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terraformStateId", + "description": "ID of the TerraformState", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TerraformStateRegistryConnection", + "description": "The connection type for TerraformStateRegistry.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TerraformStateRegistryEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TerraformStateRegistry", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TerraformStateRegistryEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "TerraformStateRegistry", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "TestReport", "description": "Represents a requirement test report.", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 136286f355b..829f18b1b18 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2317,6 +2317,21 @@ Completion status of tasks | `completedCount` | Int! | Number of completed tasks | | `count` | Int! | Number of total tasks | +## TerraformStateRegistry + +Represents the sync and verification state of a terraform state + +| Name | Type | Description | +| --- | ---- | ---------- | +| `createdAt` | Time | Timestamp when the TerraformStateRegistry was created | +| `id` | ID! | ID of the TerraformStateRegistry | +| `lastSyncFailure` | String | Error message during sync of the TerraformStateRegistry | +| `lastSyncedAt` | Time | Timestamp of the most recent successful sync of the TerraformStateRegistry | +| `retryAt` | Time | Timestamp after which the TerraformStateRegistry should be resynced | +| `retryCount` | Int | Number of consecutive failed sync attempts of the TerraformStateRegistry | +| `state` | RegistryState | Sync state of the TerraformStateRegistry | +| `terraformStateId` | ID! | ID of the TerraformState | + ## TestReport Represents a requirement test report. diff --git a/doc/development/geo/framework.md b/doc/development/geo/framework.md index 75f938cbc59..c2aa0ff233d 100644 --- a/doc/development/geo/framework.md +++ b/doc/development/geo/framework.md @@ -566,7 +566,7 @@ the Admin Area UI, and Prometheus! null: true, resolver: ::Resolvers::Geo::WidgetRegistriesResolver, description: 'Find widget registries on this Geo node', - feature_flag: :geo_self_service_framework + feature_flag: :geo_widget_replication ``` 1. Add the new `widget_registries` field name to the `expected_fields` array in diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index 722b49a3af9..1e70770856e 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -138,7 +138,90 @@ const label = __('Subscribe'); ``` In order to test JavaScript translations you have to change the GitLab -localization to other language than English and you have to generate JSON files +localization to another language than English and you have to generate JSON files +using `bin/rake gettext:po_to_json` or `bin/rake gettext:compile`. + +### Vue files + +In Vue files we make both the `__()` (double underscore parenthesis) function and the `s__()` (namespaced double underscore parenthesis) function available that you can import from the `~/locale` file. For instance: + +```javascript +import { __, s__ } from '~/locale'; +const label = __('Subscribe'); +const nameSpacedlabel = __('Plan|Subscribe'); +``` + +For the static text strings we suggest two patterns for using these translations in Vue files: + +- External constants file: + + ```javascript + javascripts + │ + └───alert_settings + │ │ constants.js + │ └───components + │ │ alert_settings_form.vue + + + // constants.js + + import { s__ } from '~/locale'; + + /* Integration constants */ + + export const I18N_ALERT_SETTINGS_FORM = { + saveBtnLabel: __('Save changes'), + }; + + + // alert_settings_form.vue + + import { + I18N_ALERT_SETTINGS_FORM, + } from '../constants'; + + <script> + export default { + i18n: { + I18N_ALERT_SETTINGS_FORM, + } + } + </script> + + <template> + <gl-button + ref="submitBtn" + variant="success" + type="submit" + > + {{ $options.i18n.I18N_ALERT_SETTINGS_FORM }} + </gl-button> + </template> + ``` + + When possible, you should opt for this pattern, as this allows you to import these strings directly into your component specs for re-use during testing. + +- Internal component `$options` object `: + + ```javascript + <script> + export default { + i18n: { + buttonLabel: s__('Plan|Button Label') + } + }, + </script> + + <template> + <gl-button :aria-label="$options.i18n.buttonLabel"> + {{ $options.i18n.buttonLabel }} + </gl-button> + </template> + ``` + +In order to visually test the Vue translations you have to change the GitLab +localization to another language than English and you have to generate JSON files using `bin/rake gettext:po_to_json` or `bin/rake gettext:compile`. ### Dynamic translations diff --git a/doc/install/installation.md b/doc/install/installation.md index f0a5970ef87..3e5668be18a 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -147,8 +147,7 @@ ldd $(command -v git) | grep pcre2 The output should contain `libpcre2-8.so.0`. -Is the system packaged Git too old, or not compiled with pcre2? -Remove it: +If the system packaged Git is too old or not compiled with `pcre2`, remove it: ```shell sudo apt-get remove git-core diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 0f29b2be54e..34c75b6b04c 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -70,7 +70,7 @@ This view allows you to: - Filter image repositories by their name. - [Delete](#delete-images-from-within-gitlab) one or more image repository. - Navigate to the image repository details page. -- Show a **Quick start** dropdown with the most common commands to log in, build and push +- Show a **Quick start** dropdown with the most common commands to log in, build and push. - Show a banner if the optional [cleanup policy](#cleanup-policy) is enabled for this project. ### Control Container Registry for your group diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 22c344a27f7..5ea84a35d17 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -181,6 +181,7 @@ module Gitlab successful_deployments: deployment_count(Deployment.success.where(last_28_days_time_period)), failed_deployments: deployment_count(Deployment.failed.where(last_28_days_time_period)), # rubocop: enable UsageData/LargeTable: + packages: count(::Packages::Package.where(last_28_days_time_period)), personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)), project_snippets: count(ProjectSnippet.where(last_28_days_time_period)) }.tap do |data| diff --git a/locale/gitlab.pot b/locale/gitlab.pot index acd052ab99d..dc3e6580b26 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7779,6 +7779,9 @@ msgstr "" msgid "DastProfiles|Do you want to discard your changes?" msgstr "" +msgid "DastProfiles|Download validation text file" +msgstr "" + msgid "DastProfiles|Edit feature will come soon. Please create a new profile if changes needed" msgstr "" @@ -7839,21 +7842,57 @@ msgstr "" msgid "DastProfiles|Site Profiles" msgstr "" +msgid "DastProfiles|Site is not validated yet, please follow the steps." +msgstr "" + +msgid "DastProfiles|Site must be validated to run an active scan." +msgstr "" + msgid "DastProfiles|Spider timeout" msgstr "" +msgid "DastProfiles|Step 1 - Choose site validation method" +msgstr "" + +msgid "DastProfiles|Step 2 - Add following text to the target site" +msgstr "" + +msgid "DastProfiles|Step 3 - Confirm text file location and validate" +msgstr "" + msgid "DastProfiles|Target URL" msgstr "" msgid "DastProfiles|Target timeout" msgstr "" +msgid "DastProfiles|Text file validation" +msgstr "" + msgid "DastProfiles|The maximum number of seconds allowed for the site under test to respond to a request." msgstr "" msgid "DastProfiles|The maximum number of seconds allowed for the spider to traverse the site." msgstr "" +msgid "DastProfiles|Validate" +msgstr "" + +msgid "DastProfiles|Validate target site" +msgstr "" + +msgid "DastProfiles|Validating..." +msgstr "" + +msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the choosen method." +msgstr "" + +msgid "DastProfiles|Validation must be turned off to change the target URL" +msgstr "" + +msgid "DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site." +msgstr "" + msgid "Data is still calculating..." msgstr "" diff --git a/package.json b/package.json index 2b25df68d86..ce902a96581 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@gitlab/ui": "20.18.1", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-1", - "@sentry/browser": "^5.10.2", + "@sentry/browser": "^5.22.3", "@sourcegraph/code-host-integration": "0.0.50", "@toast-ui/editor": "^2.3.1", "@toast-ui/vue-editor": "^2.3.1", diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index 9adec42842c..747ccd7ba1b 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -79,6 +79,23 @@ RSpec.describe SendFileUpload do it_behaves_like 'handles image resize requests allowed by FFs' end + context 'when boths FFs are enabled globally' do + before do + stub_feature_flags(dynamic_image_resizing_requester: true) + stub_feature_flags(dynamic_image_resizing_owner: true) + end + + it_behaves_like 'handles image resize requests allowed by FFs' + + context 'when current_user is nil' do + before do + allow(controller).to receive(:current_user).and_return(nil) + end + + it_behaves_like 'handles image resize requests allowed by FFs' + end + end + context 'when only FF based on content requester is enabled for current user' do before do stub_feature_flags(dynamic_image_resizing_requester: image_requester) diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb index f6514b9b973..8b0df81422b 100644 --- a/spec/factories/usage_data.rb +++ b/spec/factories/usage_data.rb @@ -101,6 +101,7 @@ FactoryBot.define do create(:package, project: projects[0]) create(:package, project: projects[0]) create(:package, project: projects[1]) + create(:package, created_at: 2.months.ago, project: projects[1]) ProjectFeature.first.update_attribute('repository_access_level', 0) diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 77de9ae4f9f..84c39ad65e9 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -12,7 +12,7 @@ "title": { "type": "string" }, "confidential": { "type": "boolean" }, "due_date": { "type": ["date", "null"] }, - "relative_position": { "type": "integer" }, + "relative_position": { "type": ["integer", "null"] }, "time_estimate": { "type": "integer" }, "issue_sidebar_endpoint": { "type": "string" }, "toggle_subscription_endpoint": { "type": "string" }, diff --git a/spec/frontend/ajax_loading_spinner_spec.js b/spec/frontend/ajax_loading_spinner_spec.js deleted file mode 100644 index c5d66eb2143..00000000000 --- a/spec/frontend/ajax_loading_spinner_spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import $ from 'jquery'; -import AjaxLoadingSpinner from '~/ajax_loading_spinner'; - -describe('Ajax Loading Spinner', () => { - const fixtureTemplate = 'static/ajax_loading_spinner.html'; - preloadFixtures(fixtureTemplate); - - beforeEach(() => { - loadFixtures(fixtureTemplate); - AjaxLoadingSpinner.init(); - }); - - it('change current icon with spinner icon and disable link while waiting ajax response', done => { - jest.spyOn($, 'ajax').mockImplementation(req => { - const xhr = new XMLHttpRequest(); - const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner'); - const icon = ajaxLoadingSpinner.querySelector('i'); - - req.beforeSend(xhr, { dataType: 'text/html' }); - - expect(icon).not.toHaveClass('fa-trash-o'); - expect(icon).toHaveClass('gl-spinner'); - expect(icon).toHaveClass('gl-spinner-orange'); - expect(icon).toHaveClass('gl-spinner-sm'); - expect(icon.dataset.icon).toEqual('fa-trash-o'); - expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(''); - - req.complete({}); - - done(); - const deferred = $.Deferred(); - return deferred.promise(); - }); - document.querySelector('.js-ajax-loading-spinner').click(); - }); - - it('use original icon again and enabled the link after complete the ajax request', done => { - jest.spyOn($, 'ajax').mockImplementation(req => { - const xhr = new XMLHttpRequest(); - const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner'); - - req.beforeSend(xhr, { dataType: 'text/html' }); - req.complete({}); - - const icon = ajaxLoadingSpinner.querySelector('i'); - - expect(icon).toHaveClass('fa-trash-o'); - expect(icon).not.toHaveClass('gl-spinner'); - expect(icon).not.toHaveClass('gl-spinner-orange'); - expect(icon).not.toHaveClass('gl-spinner-sm'); - expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null); - - done(); - const deferred = $.Deferred(); - return deferred.promise(); - }); - document.querySelector('.js-ajax-loading-spinner').click(); - }); -}); diff --git a/spec/frontend/branches/ajax_loading_spinner_spec.js b/spec/frontend/branches/ajax_loading_spinner_spec.js new file mode 100644 index 00000000000..a6404faa445 --- /dev/null +++ b/spec/frontend/branches/ajax_loading_spinner_spec.js @@ -0,0 +1,32 @@ +import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner'; + +describe('Ajax Loading Spinner', () => { + let ajaxLoadingSpinnerElement; + let fauxEvent; + beforeEach(() => { + document.body.innerHTML = ` + <div> + <a class="js-ajax-loading-spinner" + data-remote + href="http://goesnowhere.nothing/whereami"> + <i class="fa fa-trash-o"></i> + </a></div>`; + AjaxLoadingSpinner.init(); + ajaxLoadingSpinnerElement = document.querySelector('.js-ajax-loading-spinner'); + fauxEvent = { target: ajaxLoadingSpinnerElement }; + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('`ajaxBeforeSend` event handler sets current icon to spinner and disables link', () => { + expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).toBeNull(); + expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(false); + + AjaxLoadingSpinner.ajaxBeforeSend(fauxEvent); + + expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).not.toBeNull(); + expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(true); + }); +}); diff --git a/spec/frontend/fixtures/static/ajax_loading_spinner.html b/spec/frontend/fixtures/static/ajax_loading_spinner.html deleted file mode 100644 index 0e1ebb32b1c..00000000000 --- a/spec/frontend/fixtures/static/ajax_loading_spinner.html +++ /dev/null @@ -1,3 +0,0 @@ -<a class="js-ajax-loading-spinner" data-remote href="http://goesnowhere.nothing/whereami"> -<i class="fa fa-trash-o"></i> -</a> diff --git a/spec/frontend/packages/details/components/additional_metadata_spec.js b/spec/frontend/packages/details/components/additional_metadata_spec.js index b2337b86740..111e4205abb 100644 --- a/spec/frontend/packages/details/components/additional_metadata_spec.js +++ b/spec/frontend/packages/details/components/additional_metadata_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { GlLink, GlSprintf } from '@gitlab/ui'; -import DetailsRow from '~/registry/shared/components/details_row.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import component from '~/packages/details/components/additional_metadata.vue'; import { mavenPackage, conanPackage, nugetPackage, npmPackage } from '../../mock_data'; diff --git a/spec/frontend/packages/details/components/package_history_spec.js b/spec/frontend/packages/details/components/package_history_spec.js index 28bcbbb2b1b..f745a457b0a 100644 --- a/spec/frontend/packages/details/components/package_history_spec.js +++ b/spec/frontend/packages/details/components/package_history_spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { GlLink, GlSprintf } from '@gitlab/ui'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import HistoryElement from '~/packages/details/components/history_element.vue'; +import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; import component from '~/packages/details/components/package_history.vue'; import { mavenPackage, mockPipelineInfo } from '../../mock_data'; @@ -17,8 +17,8 @@ describe('Package History', () => { wrapper = shallowMount(component, { propsData: { ...defaultProps, ...props }, stubs: { - HistoryElement: { - props: HistoryElement.props, + HistoryItem: { + props: HistoryItem.props, template: '<div data-testid="history-element"><slot></slot></div>', }, GlSprintf, diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js index a21facefc97..ef22979ca7d 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js @@ -6,7 +6,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import component from '~/registry/explorer/components/details_page/tags_list_row.vue'; import DeleteButton from '~/registry/explorer/components/delete_button.vue'; -import DetailsRow from '~/registry/shared/components/details_row.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import { REMOVE_TAG_BUTTON_TITLE, REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, diff --git a/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap index a1751d69c70..2abae33bc19 100644 --- a/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap +++ b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`History Element renders the correct markup 1`] = ` +exports[`History Item renders the correct markup 1`] = ` <li class="timeline-entry system-note note-wrapper gl-mb-6!" > @@ -31,7 +31,11 @@ exports[`History Element renders the correct markup 1`] = ` <div class="note-body" - /> + > + <div + data-testid="body-slot" + /> + </div> </div> </div> </li> diff --git a/spec/frontend/registry/shared/components/details_row_spec.js b/spec/frontend/vue_shared/components/registry/details_row_spec.js index 5ae4e0ab37f..16a55b84787 100644 --- a/spec/frontend/registry/shared/components/details_row_spec.js +++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { GlIcon } from '@gitlab/ui'; -import component from '~/registry/shared/components/details_row.vue'; +import component from '~/vue_shared/components/registry/details_row.vue'; describe('DetailsRow', () => { let wrapper; diff --git a/spec/frontend/packages/details/components/history_element_spec.js b/spec/frontend/vue_shared/components/registry/history_item_spec.js index e8746fc93f5..d51ddda2e3e 100644 --- a/spec/frontend/packages/details/components/history_element_spec.js +++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js @@ -1,9 +1,9 @@ import { shallowMount } from '@vue/test-utils'; import { GlIcon } from '@gitlab/ui'; -import component from '~/packages/details/components/history_element.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import component from '~/vue_shared/components/registry/history_item.vue'; -describe('History Element', () => { +describe('History Item', () => { let wrapper; const defaultProps = { icon: 'pencil', @@ -17,6 +17,7 @@ describe('History Element', () => { }, slots: { default: '<div data-testid="default-slot"></div>', + body: '<div data-testid="body-slot"></div>', }, }); }; @@ -29,6 +30,7 @@ describe('History Element', () => { const findTimelineEntry = () => wrapper.find(TimelineEntryItem); const findGlIcon = () => wrapper.find(GlIcon); const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]'); + const findBodySlot = () => wrapper.find('[data-testid="body-slot"]'); it('renders the correct markup', () => { mountComponent(); @@ -41,11 +43,19 @@ describe('History Element', () => { expect(findDefaultSlot().exists()).toBe(true); }); + + it('has a body slot', () => { + mountComponent(); + + expect(findBodySlot().exists()).toBe(true); + }); + it('has a timeline entry', () => { mountComponent(); expect(findTimelineEntry().exists()).toBe(true); }); + it('has an icon', () => { mountComponent(); diff --git a/spec/initializers/carrierwave_patch_spec.rb b/spec/initializers/carrierwave_patch_spec.rb index d577eca2ac7..c4a7bfa59c6 100644 --- a/spec/initializers/carrierwave_patch_spec.rb +++ b/spec/initializers/carrierwave_patch_spec.rb @@ -28,5 +28,17 @@ RSpec.describe 'CarrierWave::Storage::Fog::File' do expect(subject.authenticated_url).to eq("https://sa.blob.core.windows.net/test_container/test_blob?token") end end + + context 'with custom expire_at' do + it 'properly sets expires param' do + expire_at = 24.hours.from_now + + expect_next_instance_of(Fog::Storage::AzureRM::File) do |file| + expect(file).to receive(:url).with(expire_at).and_call_original + end + + expect(subject.authenticated_url(expire_at: expire_at)).to eq("https://sa.blob.core.windows.net/test_container/test_blob?token") + end + end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index a119c85401d..a036cb95f63 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -479,7 +479,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(count_data[:project_snippets]).to eq(4) expect(count_data[:projects_with_packages]).to eq(2) - expect(count_data[:packages]).to eq(3) + expect(count_data[:packages]).to eq(4) end it 'gathers object store usage correctly' do @@ -572,6 +572,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(counts_monthly[:snippets]).to eq(3) expect(counts_monthly[:personal_snippets]).to eq(1) expect(counts_monthly[:project_snippets]).to eq(2) + expect(counts_monthly[:packages]).to eq(3) end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 46fe942fec1..8d2eb3b5e2a 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -295,20 +295,14 @@ RSpec.describe Issuable do end describe "#new?" do - it "returns true when created today and record hasn't been updated" do - allow(issue).to receive(:today?).and_return(true) - expect(issue.new?).to be_truthy - end - - it "returns false when not created today" do - allow(issue).to receive(:today?).and_return(false) + it "returns false when created 30 hours ago" do + allow(issue).to receive(:created_at).and_return(Time.current - 30.hours) expect(issue.new?).to be_falsey end - it "returns false when record has been updated" do - allow(issue).to receive(:today?).and_return(true) - issue.update_attribute(:updated_at, 1.hour.ago) - expect(issue.new?).to be_falsey + it "returns true when created 20 hours ago" do + allow(issue).to receive(:created_at).and_return(Time.current - 20.hours) + expect(issue.new?).to be_truthy end end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index 4d2474cc56a..45afbcca96d 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do let_it_be(:project) { create(:project) } subject(:service) do - described_class.create( + described_class.create!( project: project, properties: { bamboo_url: bamboo_url, @@ -85,7 +85,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do bamboo_service = service bamboo_service.bamboo_url = 'http://gitlab1.com' - bamboo_service.save + bamboo_service.save! expect(bamboo_service.password).to be_nil end @@ -94,7 +94,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do bamboo_service = service bamboo_service.username = 'some_name' - bamboo_service.save + bamboo_service.save! expect(bamboo_service.password).to eq('password') end @@ -104,7 +104,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do bamboo_service.bamboo_url = 'http://gitlab_edited.com' bamboo_service.password = 'password' - bamboo_service.save + bamboo_service.save! expect(bamboo_service.password).to eq('password') expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') @@ -117,7 +117,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do bamboo_service.bamboo_url = 'http://gitlab_edited.com' bamboo_service.password = 'password' - bamboo_service.save + bamboo_service.save! expect(bamboo_service.password).to eq('password') expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 3d0c2cc1006..f6bf1551bf0 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe BuildkiteService, :use_clean_rails_memory_store_caching do let(:project) { create(:project) } subject(:service) do - described_class.create( + described_class.create!( project: project, properties: { service_hook: true, diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index fdfee9894fa..6da16905363 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -28,7 +28,7 @@ RSpec.describe JiraService do } end - let(:service) { described_class.create(options) } + let(:service) { described_class.create!(options) } it 'sets the URL properly' do # jira-ruby gem parses the URI and handles trailing slashes fine: @@ -102,7 +102,7 @@ RSpec.describe JiraService do } end - subject { described_class.create(params) } + subject { described_class.create!(params) } it 'does not store data into properties' do expect(subject.properties).to be_nil @@ -189,7 +189,7 @@ RSpec.describe JiraService do let_it_be(:new_url) { 'http://jira-new.example.com' } before do - service.update(username: new_username, url: new_url) + service.update!(username: new_username, url: new_url) end it 'leaves properties field emtpy' do @@ -209,12 +209,12 @@ RSpec.describe JiraService do context 'when updating the url, api_url, username, or password' do it 'updates deployment type' do - service.update(url: 'http://first.url') - service.jira_tracker_data.update(deployment_type: 'server') + service.update!(url: 'http://first.url') + service.jira_tracker_data.update!(deployment_type: 'server') expect(service.jira_tracker_data.deployment_server?).to be_truthy - service.update(api_url: 'http://another.url') + service.update!(api_url: 'http://another.url') service.jira_tracker_data.reload expect(service.jira_tracker_data.deployment_cloud?).to be_truthy @@ -222,25 +222,25 @@ RSpec.describe JiraService do end it 'calls serverInfo for url' do - service.update(url: 'http://first.url') + service.update!(url: 'http://first.url') expect(WebMock).to have_requested(:get, /serverInfo/) end it 'calls serverInfo for api_url' do - service.update(api_url: 'http://another.url') + service.update!(api_url: 'http://another.url') expect(WebMock).to have_requested(:get, /serverInfo/) end it 'calls serverInfo for username' do - service.update(username: 'test-user') + service.update!(username: 'test-user') expect(WebMock).to have_requested(:get, /serverInfo/) end it 'calls serverInfo for password' do - service.update(password: 'test-password') + service.update!(password: 'test-password') expect(WebMock).to have_requested(:get, /serverInfo/) end @@ -248,7 +248,7 @@ RSpec.describe JiraService do context 'when not updating the url, api_url, username, or password' do it 'does not update deployment type' do - service.update(jira_issue_transition_id: 'jira_issue_transition_id') + expect {service.update!(jira_issue_transition_id: 'jira_issue_transition_id')}.to raise_error(ActiveRecord::RecordInvalid) expect(WebMock).not_to have_requested(:get, /serverInfo/) end @@ -268,7 +268,7 @@ RSpec.describe JiraService do it 'resets password if url changed' do service service.url = 'http://jira_edited.example.com' - service.save + service.save! expect(service.reload.url).to eq('http://jira_edited.example.com') expect(service.password).to be_nil @@ -276,7 +276,7 @@ RSpec.describe JiraService do it 'does not reset password if url "changed" to the same url as before' do service.url = 'http://jira.example.com' - service.save + service.save! expect(service.reload.url).to eq('http://jira.example.com') expect(service.password).not_to be_nil @@ -284,7 +284,7 @@ RSpec.describe JiraService do it 'resets password if url not changed but api url added' do service.api_url = 'http://jira_edited.example.com/rest/api/2' - service.save + service.save! expect(service.reload.api_url).to eq('http://jira_edited.example.com/rest/api/2') expect(service.password).to be_nil @@ -293,7 +293,7 @@ RSpec.describe JiraService do it 'does not reset password if new url is set together with password, even if it\'s the same password' do service.url = 'http://jira_edited.example.com' service.password = password - service.save + service.save! expect(service.password).to eq(password) expect(service.url).to eq('http://jira_edited.example.com') @@ -302,14 +302,14 @@ RSpec.describe JiraService do it 'resets password if url changed, even if setter called multiple times' do service.url = 'http://jira1.example.com/rest/api/2' service.url = 'http://jira1.example.com/rest/api/2' - service.save + service.save! expect(service.password).to be_nil end it 'does not reset password if username changed' do service.username = 'some_name' - service.save + service.save! expect(service.reload.password).to eq(password) end @@ -317,7 +317,7 @@ RSpec.describe JiraService do it 'does not reset password if password changed' do service.url = 'http://jira_edited.example.com' service.password = 'new_password' - service.save + service.save! expect(service.reload.password).to eq('new_password') end @@ -325,7 +325,7 @@ RSpec.describe JiraService do it 'does not reset password if the password is touched and same as before' do service.url = 'http://jira_edited.example.com' service.password = password - service.save + service.save! expect(service.reload.password).to eq(password) end @@ -342,20 +342,20 @@ RSpec.describe JiraService do it 'resets password if api url changed' do service.api_url = 'http://jira_edited.example.com/rest/api/2' - service.save + service.save! expect(service.password).to be_nil end it 'does not reset password if url changed' do service.url = 'http://jira_edited.example.com' - service.save + service.save! expect(service.password).to eq(password) end it 'resets password if api url set to empty' do - service.update(api_url: '') + service.update!(api_url: '') expect(service.reload.password).to be_nil end @@ -372,7 +372,7 @@ RSpec.describe JiraService do it 'saves password if new url is set together with password' do service.url = 'http://jira_edited.example.com/rest/api/2' service.password = 'password' - service.save + service.save! expect(service.reload.password).to eq('password') expect(service.reload.url).to eq('http://jira_edited.example.com/rest/api/2') end @@ -441,7 +441,7 @@ RSpec.describe JiraService do allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123') allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) - @jira_service.save + @jira_service.save! project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123' @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions' @@ -709,9 +709,11 @@ RSpec.describe JiraService do describe '#test' do let(:server_info_results) { { 'url' => 'http://url', 'deploymentType' => 'Cloud' } } + let_it_be(:project) { create(:project, :repository) } let(:jira_service) do described_class.new( url: url, + project: project, username: username, password: password ) @@ -728,7 +730,7 @@ RSpec.describe JiraService do end it 'gets Jira project with API URL if set' do - jira_service.update(api_url: 'http://jira.api.com') + jira_service.update!(api_url: 'http://jira.api.com') expect(server_info).to eq(success: true, result: server_info_results) expect(WebMock).to have_requested(:get, /jira.api.com/) diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/project_services/packagist_service_spec.rb index 35f282b638b..33b5c9809c7 100644 --- a/spec/models/project_services/packagist_service_spec.rb +++ b/spec/models/project_services/packagist_service_spec.rb @@ -33,7 +33,7 @@ RSpec.describe PackagistService do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } - let(:packagist_service) { described_class.create(packagist_params) } + let(:packagist_service) { described_class.create!(packagist_params) } before do stub_request(:post, packagist_hook_url) diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb index 9a8386c619e..21cc5d44558 100644 --- a/spec/models/project_services/pipelines_email_service_spec.rb +++ b/spec/models/project_services/pipelines_email_service_spec.rb @@ -81,7 +81,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'when pipeline is succeeded' do before do data[:object_attributes][:status] = 'success' - pipeline.update(status: 'success') + pipeline.update!(status: 'success') end it_behaves_like 'sending email' @@ -91,7 +91,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'on default branch' do before do data[:object_attributes][:ref] = project.default_branch - pipeline.update(ref: project.default_branch) + pipeline.update!(ref: project.default_branch) end context 'notifications are enabled only for default branch' do @@ -115,7 +115,7 @@ RSpec.describe PipelinesEmailService, :mailer do before do create(:protected_branch, project: project, name: 'a-protected-branch') data[:object_attributes][:ref] = 'a-protected-branch' - pipeline.update(ref: 'a-protected-branch') + pipeline.update!(ref: 'a-protected-branch') end context 'notifications are enabled only for default branch' do @@ -138,7 +138,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'on a neither protected nor default branch' do before do data[:object_attributes][:ref] = 'a-random-branch' - pipeline.update(ref: 'a-random-branch') + pipeline.update!(ref: 'a-random-branch') end context 'notifications are enabled only for default branch' do @@ -177,7 +177,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'with succeeded pipeline' do before do data[:object_attributes][:status] = 'success' - pipeline.update(status: 'success') + pipeline.update!(status: 'success') end it_behaves_like 'not sending email' @@ -195,7 +195,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'with succeeded pipeline' do before do data[:object_attributes][:status] = 'success' - pipeline.update(status: 'success') + pipeline.update!(status: 'success') end it_behaves_like 'not sending email' @@ -206,7 +206,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'on default branch' do before do data[:object_attributes][:ref] = project.default_branch - pipeline.update(ref: project.default_branch) + pipeline.update!(ref: project.default_branch) end context 'notifications are enabled only for default branch' do @@ -230,7 +230,7 @@ RSpec.describe PipelinesEmailService, :mailer do before do create(:protected_branch, project: project, name: 'a-protected-branch') data[:object_attributes][:ref] = 'a-protected-branch' - pipeline.update(ref: 'a-protected-branch') + pipeline.update!(ref: 'a-protected-branch') end context 'notifications are enabled only for default branch' do @@ -253,7 +253,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'on a neither protected nor default branch' do before do data[:object_attributes][:ref] = 'a-random-branch' - pipeline.update(ref: 'a-random-branch') + pipeline.update!(ref: 'a-random-branch') end context 'notifications are enabled only for default branch' do @@ -281,7 +281,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'with failed pipeline' do before do data[:object_attributes][:status] = 'failed' - pipeline.update(status: 'failed') + pipeline.update!(status: 'failed') end it_behaves_like 'not sending email' @@ -295,7 +295,7 @@ RSpec.describe PipelinesEmailService, :mailer do context 'with failed pipeline' do before do data[:object_attributes][:status] = 'failed' - pipeline.update(status: 'failed') + pipeline.update!(status: 'failed') end it_behaves_like 'sending email' diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index a3fda33664a..f71dad86a08 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do let(:project) { create(:project) } subject(:service) do - described_class.create( + described_class.create!( project: project, properties: { teamcity_url: teamcity_url, @@ -85,7 +85,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do teamcity_service = service teamcity_service.teamcity_url = 'http://gitlab1.com' - teamcity_service.save + teamcity_service.save! expect(teamcity_service.password).to be_nil end @@ -94,7 +94,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do teamcity_service = service teamcity_service.username = 'some_name' - teamcity_service.save + teamcity_service.save! expect(teamcity_service.password).to eq('password') end @@ -104,7 +104,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do teamcity_service.teamcity_url = 'http://gitlab_edited.com' teamcity_service.password = 'password' - teamcity_service.save + teamcity_service.save! expect(teamcity_service.password).to eq('password') expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') @@ -117,7 +117,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do teamcity_service.teamcity_url = 'http://gitlab_edited.com' teamcity_service.password = 'password' - teamcity_service.save + teamcity_service.save! expect(teamcity_service.password).to eq('password') expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 794da96ed11..2c36b7ea4b6 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -75,35 +75,10 @@ RSpec.describe Issues::CreateService do expect(Todo.where(attributes).count).to eq 1 end - it 'rebalances if needed' do - create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION) - expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id) + it 'moves the issue to the end, in an asynchronous worker' do + expect(IssuePlacementWorker).to receive(:perform_async).with(Integer) - expect(issue.relative_position).to eq(project.issues.maximum(:relative_position)) - end - - it 'does not rebalance if the flag is disabled' do - stub_feature_flags(rebalance_issues: false) - - create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION) - expect(IssueRebalancingWorker).not_to receive(:perform_async) - - expect(issue.relative_position).to eq(project.issues.maximum(:relative_position)) - end - - it 'does rebalance if the flag is enabled for the project' do - stub_feature_flags(rebalance_issues: project) - - create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION) - expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id) - - expect(issue.relative_position).to eq(project.issues.maximum(:relative_position)) - end - - it 'does not rebalance unless needed' do - expect(IssueRebalancingWorker).not_to receive(:perform_async) - - expect(issue.relative_position).to eq(project.issues.maximum(:relative_position)) + described_class.new(project, user, opts).execute end context 'when label belongs to project group' do diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb index b6ad488a48c..78b937a1caf 100644 --- a/spec/services/issues/reorder_service_spec.rb +++ b/spec/services/issues/reorder_service_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Issues::ReorderService do - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } + let_it_be(:user) { create_default(:user) } let_it_be(:group) { create(:group) } + let_it_be(:project, reload: true) { create(:project, namespace: group) } shared_examples 'issues reorder service' do context 'when reordering issues' do @@ -14,7 +14,7 @@ RSpec.describe Issues::ReorderService do end it 'returns false with both invalid params' do - params = { move_after_id: nil, move_before_id: 1 } + params = { move_after_id: nil, move_before_id: non_existing_record_id } expect(service(params).execute(issue1)).to be_falsey end @@ -27,27 +27,39 @@ RSpec.describe Issues::ReorderService do expect(issue1.relative_position) .to be_between(issue2.relative_position, issue3.relative_position) end + + it 'sorts issues if only given one neighbour, on the left' do + params = { move_before_id: issue3.id } + + service(params).execute(issue1) + + expect(issue1.relative_position).to be > issue3.relative_position + end + + it 'sorts issues if only given one neighbour, on the right' do + params = { move_after_id: issue1.id } + + service(params).execute(issue3) + + expect(issue3.relative_position).to be < issue1.relative_position + end end end describe '#execute' do - let(:issue1) { create(:issue, project: project, relative_position: 10) } - let(:issue2) { create(:issue, project: project, relative_position: 20) } - let(:issue3) { create(:issue, project: project, relative_position: 30) } + let_it_be(:issue1, reload: true) { create(:issue, project: project, relative_position: 10) } + let_it_be(:issue2) { create(:issue, project: project, relative_position: 20) } + let_it_be(:issue3, reload: true) { create(:issue, project: project, relative_position: 30) } context 'when ordering issues in a project' do - let(:parent) { project } - before do - parent.add_developer(user) + project.add_developer(user) end it_behaves_like 'issues reorder service' end context 'when ordering issues in a group' do - let(:project) { create(:project, namespace: group) } - before do group.add_developer(user) end diff --git a/spec/workers/issue_placement_worker_spec.rb b/spec/workers/issue_placement_worker_spec.rb new file mode 100644 index 00000000000..05d1ab8496e --- /dev/null +++ b/spec/workers/issue_placement_worker_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IssuePlacementWorker do + describe '#perform' do + let_it_be(:time) { Time.now.utc } + let_it_be(:project) { create(:project) } + let_it_be(:author) { create(:user) } + let_it_be(:common_attrs) { { author: author, project: project } } + let_it_be(:unplaced) { common_attrs.merge(relative_position: nil) } + let_it_be(:issue) { create(:issue, **unplaced, created_at: time) } + let_it_be(:issue_a) { create(:issue, **unplaced, created_at: time - 1.minute) } + let_it_be(:issue_b) { create(:issue, **unplaced, created_at: time - 2.minutes) } + let_it_be(:issue_c) { create(:issue, **unplaced, created_at: time + 1.minute) } + let_it_be(:issue_d) { create(:issue, **unplaced, created_at: time + 2.minutes) } + let_it_be(:issue_e) { create(:issue, **common_attrs, relative_position: 10, created_at: time + 1.minute) } + let_it_be(:issue_f) { create(:issue, **unplaced, created_at: time + 1.minute) } + + let_it_be(:irrelevant) { create(:issue, relative_position: nil, created_at: time) } + + it 'places all issues created at most 5 minutes before this one at the end, most recent last' do + expect do + described_class.new.perform(issue.id) + end.not_to change { irrelevant.reset.relative_position } + + expect(project.issues.order_relative_position_asc) + .to eq([issue_e, issue_b, issue_a, issue, issue_c, issue_f, issue_d]) + expect(project.issues.where(relative_position: nil)).not_to exist + end + + it 'schedules rebalancing if needed' do + issue_a.update!(relative_position: RelativePositioning::MAX_POSITION) + + expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id) + + described_class.new.perform(issue.id) + end + + it 'limits the sweep to QUERY_LIMIT records' do + # Ensure there are more than N issues in this set + n = described_class::QUERY_LIMIT + create_list(:issue, n - 5, **unplaced) + + expect(Issue).to receive(:move_nulls_to_end).with(have_attributes(count: n)).and_call_original + + described_class.new.perform(issue.id) + + expect(project.issues.where(relative_position: nil)).to exist + end + + it 'anticipates the failure to find the issue' do + id = non_existing_record_id + + expect { described_class.new.perform(id) }.not_to raise_error + end + + it 'anticipates the failure to place the issues, and schedules rebalancing' do + allow(Issue).to receive(:move_nulls_to_end) { raise RelativePositioning::NoSpaceLeft } + + expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id) + expect(Gitlab::ErrorTracking) + .to receive(:log_exception) + .with(RelativePositioning::NoSpaceLeft, issue_id: issue.id) + + described_class.new.perform(issue.id) + end + end +end diff --git a/spec/workers/new_issue_worker_spec.rb b/spec/workers/new_issue_worker_spec.rb index 6386af8d253..d56732770a3 100644 --- a/spec/workers/new_issue_worker_spec.rb +++ b/spec/workers/new_issue_worker_spec.rb @@ -39,10 +39,10 @@ RSpec.describe NewIssueWorker do end context 'when everything is ok' do - let(:project) { create(:project, :public) } - let(:mentioned) { create(:user) } - let(:user) { create(:user) } - let(:issue) { create(:issue, project: project, description: "issue for #{mentioned.to_reference}") } + let_it_be(:user) { create_default(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:mentioned) { create(:user) } + let_it_be(:issue) { create(:issue, project: project, description: "issue for #{mentioned.to_reference}") } it 'creates a new event record' do expect { worker.perform(issue.id, user.id) }.to change { Event.count }.from(0).to(1) diff --git a/yarn.lock b/yarn.lock index 67f564fc6b6..74b63faa243 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1076,56 +1076,56 @@ resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.3-1.tgz#9b9eb8858a6507162911007d355d9a206e1c5caa" integrity sha512-szFhWD+V5TAxVNVIG16klgq+ypqA5k5AecLarTTrXgOG8cawVbQdOAwLbCmzkwiQ60rGSxAFoC1u2LrzxSK2Aw== -"@sentry/browser@^5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.10.2.tgz#0bbb05505c58ea998c833cffec3f922fe4b4fa58" - integrity sha512-r3eyBu2ln7odvWtXARCZPzpuGrKsD6U9F3gKTu4xdFkA0swSLUvS7AC2FUksj/1BE23y+eB/zzPT+RYJ58tidA== - dependencies: - "@sentry/core" "5.10.2" - "@sentry/types" "5.10.0" - "@sentry/utils" "5.10.2" +"@sentry/browser@^5.22.3": + version "5.22.3" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.22.3.tgz#7a64bd1cf01bf393741a3e4bf35f82aa927f5b4e" + integrity sha512-2TzE/CoBa5ZkvxJizDdi1Iz1ldmXSJpFQ1mL07PIXBjCt0Wxf+WOuFSj5IP4L40XHfJE5gU8wEvSH0VDR8nXtA== + dependencies: + "@sentry/core" "5.22.3" + "@sentry/types" "5.22.3" + "@sentry/utils" "5.22.3" tslib "^1.9.3" -"@sentry/core@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.10.2.tgz#1cb64489e6f8363c3249415b49d3f1289814825f" - integrity sha512-sKVeFH3v8K8xw2vM5MKMnnyAAwih+JSE3pbNL0CcCCA+/SwX+3jeAo2BhgXev2SAR/TjWW+wmeC9TdIW7KyYbg== +"@sentry/core@5.22.3": + version "5.22.3" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.3.tgz#030f435f2b518f282ba8bd954dac90cd70888bd7" + integrity sha512-eGL5uUarw3o4i9QUb9JoFHnhriPpWCaqeaIBB06HUpdcvhrjoowcKZj1+WPec5lFg5XusE35vez7z/FPzmJUDw== dependencies: - "@sentry/hub" "5.10.2" - "@sentry/minimal" "5.10.2" - "@sentry/types" "5.10.0" - "@sentry/utils" "5.10.2" + "@sentry/hub" "5.22.3" + "@sentry/minimal" "5.22.3" + "@sentry/types" "5.22.3" + "@sentry/utils" "5.22.3" tslib "^1.9.3" -"@sentry/hub@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.10.2.tgz#25d9f36b8f7c5cb65cf486737fa61dc9bf69b7e3" - integrity sha512-hSlZIiu3hcR/I5yEhlpN9C0nip+U7hiRzRzUQaBiHO4YG4TC58NqnOPR89D/ekiuHIXzFpjW9OQmqtAMRoSUYA== +"@sentry/hub@5.22.3": + version "5.22.3" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.3.tgz#08309a70d2ea8d5e313d05840c1711f34f2fffe5" + integrity sha512-INo47m6N5HFEs/7GMP9cqxOIt7rmRxdERunA3H2L37owjcr77MwHVeeJ9yawRS6FMtbWXplgWTyTIWIYOuqVbw== dependencies: - "@sentry/types" "5.10.0" - "@sentry/utils" "5.10.2" + "@sentry/types" "5.22.3" + "@sentry/utils" "5.22.3" tslib "^1.9.3" -"@sentry/minimal@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.10.2.tgz#267c2f3aa6877a0fe7a86971942e83f3ee616580" - integrity sha512-GalixiM9sckYfompH5HHTp9XT2BcjawBkcl1DMEKUBEi37+kUq0bivOBmnN1G/I4/wWOUdnAI/kagDWaWpbZPg== +"@sentry/minimal@5.22.3": + version "5.22.3" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.3.tgz#706e4029ae5494123d3875c658ba8911aa5cc440" + integrity sha512-HoINpYnVYCpNjn2XIPIlqH5o4BAITpTljXjtAftOx6Hzj+Opjg8tR8PWliyKDvkXPpc4kXK9D6TpEDw8MO0wZA== dependencies: - "@sentry/hub" "5.10.2" - "@sentry/types" "5.10.0" + "@sentry/hub" "5.22.3" + "@sentry/types" "5.22.3" tslib "^1.9.3" -"@sentry/types@5.10.0": - version "5.10.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.10.0.tgz#4f0ba31b6e4d5371112c38279f11f66c73b43746" - integrity sha512-TW20GzkCWsP6uAxR2JIpIkiitCKyIOfkyDsKBeLqYj4SaZjfvBPnzgNCcYR0L0UsP1/Es6oHooZfIGSkp6GGxQ== +"@sentry/types@5.22.3": + version "5.22.3" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.3.tgz#d1d547b30ee8bd7771fa893af74c4f3d71f0fd18" + integrity sha512-cv+VWK0YFgCVDvD1/HrrBWOWYG3MLuCUJRBTkV/Opdy7nkdNjhCAJQrEyMM9zX0sac8FKWKOHT0sykNh8KgmYw== -"@sentry/utils@5.10.2": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.10.2.tgz#261f575079d30aaf604e59f5f4de0aa21db22252" - integrity sha512-UcbbaFpYrGSV448lQ16Cr+W/MPuKUflQQUdrMCt5vgaf5+M7kpozlcji4GGGZUCXIA7oRP93ABoXj55s1OM9zw== +"@sentry/utils@5.22.3": + version "5.22.3" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.3.tgz#e3bda3e789239eb16d436f768daa12829f33d18f" + integrity sha512-AHNryXMBvIkIE+GQxTlmhBXD0Ksh+5w1SwM5qi6AttH+1qjWLvV6WB4+4pvVvEoS8t5F+WaVUZPQLmCCWp6zKw== dependencies: - "@sentry/types" "5.10.0" + "@sentry/types" "5.22.3" tslib "^1.9.3" "@sindresorhus/is@^0.14.0": |