diff options
49 files changed, 615 insertions, 268 deletions
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 95e1e8af9b3..1d4a6e64f9d 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -111,12 +111,7 @@ export default { * @returns {Boolean|Undefined} */ canShowDate() { - return ( - this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined - ); + return this.model && this.model.last_deployment && this.model.last_deployment.deployed_at; }, /** @@ -124,14 +119,9 @@ export default { * * @returns {String} */ - createdDate() { - if ( - this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.created_at - ) { - return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + deployedDate() { + if (this.canShowDate) { + return timeagoInstance.format(this.model.last_deployment.deployed_at); } return ''; }, @@ -547,7 +537,7 @@ export default { <div v-if="!model.isFolder" class="table-section section-10" role="gridcell"> <div role="rowheader" class="table-mobile-header">{{ s__('Environments|Updated') }}</div> <span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content"> - {{ createdDate }} + {{ deployedDate }} </span> </div> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 8b356ee6e97..549324831e9 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -69,7 +69,11 @@ export default { :disabled="currentBranch && !currentBranch.can_push" :title="$options.currentBranchPermissionsTooltip" > - <span class="ide-radio-label" v-html="commitToCurrentBranchText"> </span> + <span + class="ide-radio-label" + data-qa-selector="commit_to_current_branch_radio" + v-html="commitToCurrentBranchText" + ></span> </radio-group> <radio-group :value="$options.commitToNewBranch" diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 821e6691fe4..69ef116043a 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -245,27 +245,3 @@ label { .input-group-text { max-height: $input-height; } - -.gl-form-checkbox { - align-items: baseline; - margin-right: 1rem; - margin-bottom: 0.25rem; - - .form-check-input { - margin-right: 0; - } - - .form-check-label { - padding-left: $gl-padding-8; - } - - &.form-check-inline .form-check-input { - align-self: flex-start; - height: 1.5 * $gl-font-size; - } - - .form-check-input:disabled, - .form-check-input:disabled ~ .form-check-label { - cursor: not-allowed; - } -} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index f4cc0a5851b..d492c5227cf 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -46,6 +46,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @noteable = @merge_request @commits_count = @merge_request.commits_count @issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar') + @current_user_data = UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json set_pipeline_variables diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb index f730b015c0a..e8c7f9622a9 100644 --- a/app/finders/members_finder.rb +++ b/app/finders/members_finder.rb @@ -60,15 +60,32 @@ class MembersFinder # We're interested in a list of members without duplicates by user_id. # We prefer project members over group members, project members should go first. <<~SQL - SELECT DISTINCT ON (user_id, invite_email) member_union.* - FROM (#{union.to_sql}) AS member_union - ORDER BY user_id, - invite_email, - CASE - WHEN type = 'ProjectMember' THEN 1 - WHEN type = 'GroupMember' THEN 2 - ELSE 3 - END + SELECT DISTINCT ON (user_id, invite_email) #{member_columns} + FROM (#{union.to_sql}) AS #{member_union_table} + LEFT JOIN users on users.id = member_union.user_id + LEFT JOIN project_authorizations on project_authorizations.user_id = users.id + AND + project_authorizations.project_id = #{project.id} + ORDER BY user_id, + invite_email, + CASE + WHEN type = 'ProjectMember' THEN 1 + WHEN type = 'GroupMember' THEN 2 + ELSE 3 + END SQL end + + def member_union_table + 'member_union' + end + + def member_columns + Member.column_names.map do |column_name| + # fallback to members.access_level when project_authorizations.access_level is missing + next "COALESCE(#{ProjectAuthorization.table_name}.access_level, #{member_union_table}.access_level) access_level" if column_name == 'access_level' + + "#{member_union_table}.#{column_name}" + end.join(',') + end end diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb new file mode 100644 index 00000000000..e22be6880bb --- /dev/null +++ b/app/serializers/merge_request_noteable_entity.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class MergeRequestNoteableEntity < Grape::Entity + include RequestAwareEntity + + # Currently this attr is exposed to be used in app/assets/javascripts/notes/stores/getters.js + # in order to determine whether a noteable is an issue or an MR + expose :merge_params + + expose :state + expose :source_branch + expose :target_branch + expose :diff_head_sha + + expose :create_note_path do |merge_request| + project_notes_path(merge_request.project, target_type: 'merge_request', target_id: merge_request.id) + end + + expose :preview_note_path do |merge_request| + preview_markdown_path(merge_request.project, target_type: 'MergeRequest', target_id: merge_request.iid) + end + + expose :supports_suggestion?, as: :can_receive_suggestion + + expose :create_issue_to_resolve_discussions_path do |merge_request| + presenter(merge_request).create_issue_to_resolve_discussions_path + end + + expose :new_blob_path do |merge_request| + if presenter(merge_request).can_push_to_source_branch? + project_new_blob_path(merge_request.source_project, merge_request.source_branch) + end + end + + expose :current_user do + expose :can_create_note do |merge_request| + can?(current_user, :create_note, merge_request) + end + + expose :can_update do |merge_request| + can?(current_user, :update_merge_request, merge_request) + end + end + + private + + delegate :current_user, to: :request + + def presenter(merge_request) + @presenters ||= {} + @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter + end +end diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb index 65132b4b215..cd33ffa702a 100644 --- a/app/serializers/merge_request_poll_widget_entity.rb +++ b/app/serializers/merge_request_poll_widget_entity.rb @@ -65,8 +65,6 @@ class MergeRequestPollWidgetEntity < IssuableEntity end end - expose :supports_suggestion?, as: :can_receive_suggestion - expose :create_issue_to_resolve_discussions_path do |merge_request| presenter(merge_request).create_issue_to_resolve_discussions_path end @@ -84,17 +82,9 @@ class MergeRequestPollWidgetEntity < IssuableEntity presenter(merge_request).can_cherry_pick_on_current_merge_request? end - expose :can_create_note do |merge_request| - can?(current_user, :create_note, merge_request) - end - expose :can_create_issue do |merge_request| can?(current_user, :create_issue, merge_request.project) end - - expose :can_update do |merge_request| - can?(current_user, :update_merge_request, merge_request) - end end expose :can_push_to_source_branch do |merge_request| diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb index bd2e682a122..aa67cd1f39e 100644 --- a/app/serializers/merge_request_serializer.rb +++ b/app/serializers/merge_request_serializer.rb @@ -13,6 +13,8 @@ class MergeRequestSerializer < BaseSerializer MergeRequestSidebarExtrasEntity when 'basic' MergeRequestBasicEntity + when 'noteable' + MergeRequestNoteableEntity else # fallback to widget for old poll requests without `serializer` set MergeRequestWidgetEntity diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index c8088608cb0..2f2c42a7387 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -3,10 +3,6 @@ class MergeRequestWidgetEntity < Grape::Entity include RequestAwareEntity - # Currently this attr is exposed to be used in app/assets/javascripts/notes/stores/getters.js - # in order to determine whether a noteable is an issue or an MR - expose :merge_params - expose :source_project_full_path do |merge_request| merge_request.source_project&.full_path end @@ -35,18 +31,10 @@ class MergeRequestWidgetEntity < Grape::Entity cached_widget_project_json_merge_request_path(merge_request.target_project, merge_request, format: :json) end - expose :create_note_path do |merge_request| - project_notes_path(merge_request.project, target_type: 'merge_request', target_id: merge_request.id) - end - expose :commit_change_content_path do |merge_request| commit_change_content_project_merge_request_path(merge_request.project, merge_request) end - expose :preview_note_path do |merge_request| - preview_markdown_path(merge_request.project, target_type: 'MergeRequest', target_id: merge_request.iid) - end - expose :conflicts_docs_path do |merge_request| help_page_path('user/project/merge_requests/resolve_conflicts.md') end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index ee7223d6349..1b48b20e28b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -67,7 +67,7 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee')) end - # Called when the assignees of an Issue is changed or removed + # Called when the assignees of an issuable is changed or removed # # issuable - Issuable object (responds to assignees) # project - Project owning noteable @@ -88,10 +88,12 @@ module SystemNoteService def change_issuable_assignees(issuable, project, author, old_assignees) unassigned_users = old_assignees - issuable.assignees added_users = issuable.assignees.to_a - old_assignees - text_parts = [] - text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any? - text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any? + + Gitlab::I18n.with_default_locale do + text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any? + text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any? + end body = text_parts.join(' and ') diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index 752be02443c..ef2ab4c698e 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -22,7 +22,8 @@ .table-section.section-15{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' }= _("Created") - %span.table-mobile-content= time_ago_with_tooltip(deployment.created_at) + - if deployment.deployed_at + %span.table-mobile-content= time_ago_with_tooltip(deployment.deployed_at) .table-section.section-20.table-button-footer{ role: 'gridcell' } .btn-group.table-action-buttons diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index af3bd8dcd69..ea166d622eb 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -6,6 +6,7 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes') +- number_of_pipelines = @pipelines.size .merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version } } = render "projects/merge_requests/mr_title" @@ -41,11 +42,11 @@ = tab_link_for @merge_request, :commits do = _("Commits") %span.badge.badge-pill= @commits_count - - if @pipelines.any? + - if number_of_pipelines.nonzero? %li.pipelines-tab = tab_link_for @merge_request, :pipelines do = _("Pipelines") - %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size + %span.badge.badge-pill.js-pipelines-mr-count= number_of_pipelines %li.diffs-tab.qa-diffs-tab = tab_link_for @merge_request, :diffs do = _("Changes") @@ -63,21 +64,21 @@ %script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe .issuable-discussion.js-vue-notes-event #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json, - noteable_data: serialize_issuable(@merge_request), + noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'), noteable_type: 'MergeRequest', target_type: 'merge_request', help_page_path: suggest_changes_help_path, - current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json} } + current_user_data: @current_user_data} } #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - - if @pipelines.any? + - if number_of_pipelines.nonzero? = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request) #js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?, endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters), help_page_path: suggest_changes_help_path, - current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json, + current_user_data: @current_user_data, project_path: project_path(@merge_request.project), changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'), is_fluid_layout: fluid_layout.to_s, diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 5cc6b5a173b..e1797e6db2a 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -21,7 +21,7 @@ .form-actions - if @milestone.new_record? - = f.submit _('Create milestone'), class: 'btn-create btn qa-milestone-create-button' + = f.submit _('Create milestone'), class: 'btn-success btn qa-milestone-create-button' = link_to _('Cancel'), project_milestones_path(@project), class: 'btn btn-cancel' - else = f.submit _('Save changes'), class: 'btn-success btn' diff --git a/changelogs/unreleased/56130-operations-environments-shows-incorrect-deployment-date-for-manual-.yml b/changelogs/unreleased/56130-operations-environments-shows-incorrect-deployment-date-for-manual-.yml new file mode 100644 index 00000000000..92f25ac07e2 --- /dev/null +++ b/changelogs/unreleased/56130-operations-environments-shows-incorrect-deployment-date-for-manual-.yml @@ -0,0 +1,6 @@ +--- +title: Update the timestamp in Operations > Environments to show correct deployment + date for manual deploy jobs +merge_request: 32072 +author: +type: fixed diff --git a/changelogs/unreleased/62284-follow-up-from-resolve-api-to-get-all-project-group-members-returns-duplicates.yml b/changelogs/unreleased/62284-follow-up-from-resolve-api-to-get-all-project-group-members-returns-duplicates.yml new file mode 100644 index 00000000000..0c73f73c297 --- /dev/null +++ b/changelogs/unreleased/62284-follow-up-from-resolve-api-to-get-all-project-group-members-returns-duplicates.yml @@ -0,0 +1,5 @@ +--- +title: Uses projects_authorizations.access_level in MembersFinder +merge_request: 28887 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/63262-notes-are-persisted-with-the-user-s-locale.yml b/changelogs/unreleased/63262-notes-are-persisted-with-the-user-s-locale.yml new file mode 100644 index 00000000000..e55beb9db09 --- /dev/null +++ b/changelogs/unreleased/63262-notes-are-persisted-with-the-user-s-locale.yml @@ -0,0 +1,5 @@ +--- +title: Do not translate system notes into author's language +merge_request: 32264 +author: +type: fixed diff --git a/changelogs/unreleased/ce-xanf-move-auto-merge-failed-to-jest.yml b/changelogs/unreleased/ce-xanf-move-auto-merge-failed-to-jest.yml new file mode 100644 index 00000000000..5a56a668c54 --- /dev/null +++ b/changelogs/unreleased/ce-xanf-move-auto-merge-failed-to-jest.yml @@ -0,0 +1,5 @@ +--- +title: Refactored Karma spec to Jest for mr_widget_auto_merge_failed +merge_request: 32282 +author: Illya Klymov +type: other diff --git a/changelogs/unreleased/cluster_deployments.yml b/changelogs/unreleased/cluster_deployments.yml new file mode 100644 index 00000000000..d854d16ea72 --- /dev/null +++ b/changelogs/unreleased/cluster_deployments.yml @@ -0,0 +1,5 @@ +--- +title: Add index to improve group cluster deployments query performance +merge_request: 31988 +author: +type: other diff --git a/changelogs/unreleased/id-optimize-sql-requests-mr-show.yml b/changelogs/unreleased/id-optimize-sql-requests-mr-show.yml new file mode 100644 index 00000000000..8b171a96316 --- /dev/null +++ b/changelogs/unreleased/id-optimize-sql-requests-mr-show.yml @@ -0,0 +1,5 @@ +--- +title: Reduce the number of SQL requests on MR-show +merge_request: 32192 +author: +type: performance diff --git a/changelogs/unreleased/new-project-milestone-primary-button.yml b/changelogs/unreleased/new-project-milestone-primary-button.yml new file mode 100644 index 00000000000..ac0305a2e21 --- /dev/null +++ b/changelogs/unreleased/new-project-milestone-primary-button.yml @@ -0,0 +1,5 @@ +--- +title: New project milestone primary button +merge_request: 32355 +author: Lee Tickett +type: fixed diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb index f9055285e5c..a3810be70b2 100644 --- a/config/initializers/peek.rb +++ b/config/initializers/peek.rb @@ -1,6 +1,7 @@ require 'peek/adapters/redis' Peek::Adapters::Redis.prepend ::Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled +Peek.singleton_class.prepend ::Gitlab::PerformanceBar::WithTopLevelWarnings Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis::Cache.params) } diff --git a/db/migrate/20190819131155_add_cluster_status_index_to_deployments.rb b/db/migrate/20190819131155_add_cluster_status_index_to_deployments.rb new file mode 100644 index 00000000000..bfa91e33558 --- /dev/null +++ b/db/migrate/20190819131155_add_cluster_status_index_to_deployments.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddClusterStatusIndexToDeployments < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :deployments, [:cluster_id, :status] + end + + def down + remove_concurrent_index :deployments, [:cluster_id, :status] + end +end diff --git a/db/migrate/20190826090628_remove_redundant_deployments_index.rb b/db/migrate/20190826090628_remove_redundant_deployments_index.rb new file mode 100644 index 00000000000..6b009c17d64 --- /dev/null +++ b/db/migrate/20190826090628_remove_redundant_deployments_index.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class RemoveRedundantDeploymentsIndex < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + remove_concurrent_index :deployments, :cluster_id + end + + def down + add_concurrent_index :deployments, :cluster_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 454ea939a6f..54774b0a65b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1146,7 +1146,7 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do t.integer "status", limit: 2, null: false t.datetime_with_timezone "finished_at" t.integer "cluster_id" - t.index ["cluster_id"], name: "index_deployments_on_cluster_id" + t.index ["cluster_id", "status"], name: "index_deployments_on_cluster_id_and_status" t.index ["created_at"], name: "index_deployments_on_created_at" t.index ["deployable_type", "deployable_id"], name: "index_deployments_on_deployable_type_and_deployable_id" t.index ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id" diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 96e8c30a679..173471e3af8 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -13,7 +13,7 @@ importance. GitLab is built on top of [Ruby on Rails](https://rubyonrails.org/), and we're using [RSpec] for all the backend tests, with [Capybara] for end-to-end integration testing. -On the frontend side, we're using [Karma] and [Jasmine] for JavaScript unit and +On the frontend side, we're using [Jest](https://jestjs.io/) and [Karma](http://karma-runner.github.io/)/[Jasmine](https://jasmine.github.io/) for JavaScript unit and integration testing. Following are two great articles that everyone should read to understand what @@ -64,6 +64,4 @@ Everything you should know about how to run end-to-end tests using [RSpec]: https://github.com/rspec/rspec-rails#feature-specs [Capybara]: https://github.com/teamcapybara/capybara -[Karma]: http://karma-runner.github.io/ -[Jasmine]: https://jasmine.github.io/ [gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md index a998ab74a96..0f7a24042bb 100644 --- a/doc/development/ux_guide/animation.md +++ b/doc/development/ux_guide/animation.md @@ -1,5 +1,5 @@ --- -redirect_to: 'https://design.gitlab.com/product-foundations/motion' +redirect_to: 'https://design.gitlab.com/product-foundations/motion/' --- -The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/product-foundations/motion). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/product-foundations/motion/). diff --git a/doc/development/ux_guide/illustrations.md b/doc/development/ux_guide/illustrations.md index 3592d25c95d..815f870f8c5 100644 --- a/doc/development/ux_guide/illustrations.md +++ b/doc/development/ux_guide/illustrations.md @@ -1,5 +1,5 @@ --- -redirect_to: 'https://design.gitlab.com/product-foundations/illustration' +redirect_to: 'https://design.gitlab.com/product-foundations/illustration/' --- -The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/product-foundations/illustration). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/product-foundations/illustration/). diff --git a/doc/user/project/import/tfvc.md b/doc/user/project/import/tfvc.md index 375522b77d0..9b148224e10 100644 --- a/doc/user/project/import/tfvc.md +++ b/doc/user/project/import/tfvc.md @@ -6,7 +6,7 @@ type: concepts Team Foundation Server (TFS), renamed [Azure DevOps Server](https://azure.microsoft.com/en-us/services/devops/server/) in 2019, is a set of tools developed by Microsoft which also includes -[Team Foundation Version Control](https://docs.microsoft.com/en-us/azure/devops/repos/tfvc/overview) +[Team Foundation Version Control](https://docs.microsoft.com/en-us/azure/devops/repos/tfvc/overview?view=azure-devops) (TFVC), a centralized version control system similar to Git. In this document, we focus on the TFVC to Git migration. diff --git a/doc/user/project/operations/tracing.md b/doc/user/project/operations/tracing.md index b92d2e49839..3fb3be3c21f 100644 --- a/doc/user/project/operations/tracing.md +++ b/doc/user/project/operations/tracing.md @@ -17,8 +17,8 @@ systems. ### Deploying Jaeger To learn more about deploying Jaeger, read the official -[Getting Started documentation](https://www.jaegertracing.io/docs/1.13/getting-started/). -There is an easy to use [all-in-one Docker image](https://www.jaegertracing.io/docs/1.13/getting-started/#AllinoneDockerimage), +[Getting Started documentation](https://www.jaegertracing.io/docs/latest/getting-started/). +There is an easy to use [all-in-one Docker image](https://www.jaegertracing.io/docs/latest/getting-started/#AllinoneDockerimage), as well as deployment options for [Kubernetes](https://github.com/jaegertracing/jaeger-kubernetes) and [OpenShift](https://github.com/jaegertracing/jaeger-openshift). @@ -27,7 +27,7 @@ and [OpenShift](https://github.com/jaegertracing/jaeger-openshift). GitLab provides an easy way to open the Jaeger UI from within your project: 1. [Set up Jaeger](#deploying-jaeger) and configure your application using one of the - [client libraries](https://www.jaegertracing.io/docs/1.13/client-libraries/). + [client libraries](https://www.jaegertracing.io/docs/latest/client-libraries/). 1. Navigate to your project's **Settings > Operations** and provide the Jaeger URL. 1. Click **Save changes** for the changes to take effect. 1. You can now visit **Operations > Tracing** in your project's sidebar and diff --git a/lib/gitlab/performance_bar/with_top_level_warnings.rb b/lib/gitlab/performance_bar/with_top_level_warnings.rb new file mode 100644 index 00000000000..fb5c5c5959d --- /dev/null +++ b/lib/gitlab/performance_bar/with_top_level_warnings.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module PerformanceBar + module WithTopLevelWarnings + def results + results = super + + results.merge(has_warnings: has_warnings?(results)) + end + + def has_warnings?(results) + results[:data].any? do |_, value| + value[:warnings].present? + end + end + end + end +end diff --git a/lib/peek/views/active_record.rb b/lib/peek/views/active_record.rb index 2d78818630d..a35783c1971 100644 --- a/lib/peek/views/active_record.rb +++ b/lib/peek/views/active_record.rb @@ -3,6 +3,24 @@ module Peek module Views class ActiveRecord < DetailedView + DEFAULT_THRESHOLDS = { + calls: 100, + duration: 3, + individual_call: 1 + }.freeze + + THRESHOLDS = { + production: { + calls: 100, + duration: 15, + individual_call: 5 + } + }.freeze + + def self.thresholds + @thresholds ||= THRESHOLDS.fetch(Rails.env.to_sym, DEFAULT_THRESHOLDS) + end + private def setup_subscribers diff --git a/lib/peek/views/detailed_view.rb b/lib/peek/views/detailed_view.rb index f4ca1cb5075..4f3eddaf11b 100644 --- a/lib/peek/views/detailed_view.rb +++ b/lib/peek/views/detailed_view.rb @@ -3,11 +3,16 @@ module Peek module Views class DetailedView < View + def self.thresholds + {} + end + def results { - duration: formatted_duration, + duration: format_duration(duration), calls: calls, - details: details + details: details, + warnings: warnings } end @@ -18,30 +23,48 @@ module Peek private def duration - detail_store.map { |entry| entry[:duration] }.sum # rubocop:disable CodeReuse/ActiveRecord + detail_store.map { |entry| entry[:duration] }.sum * 1000 # rubocop:disable CodeReuse/ActiveRecord end def calls detail_store.count end + def details + call_details + .sort { |a, b| b[:duration] <=> a[:duration] } + .map(&method(:format_call_details)) + end + + def warnings + [ + warning_for(calls, self.class.thresholds[:calls], label: "#{key} calls"), + warning_for(duration, self.class.thresholds[:duration], label: "#{key} duration") + ].flatten.compact + end + def call_details detail_store end def format_call_details(call) - call.merge(duration: (call[:duration] * 1000).round(3)) - end + duration = (call[:duration] * 1000).round(3) - def details - call_details - .sort { |a, b| b[:duration] <=> a[:duration] } - .map(&method(:format_call_details)) + call.merge(duration: duration, + warnings: warning_for(duration, self.class.thresholds[:individual_call])) end - def formatted_duration - ms = duration * 1000 + def warning_for(actual, threshold, label: nil) + if threshold && actual > threshold + prefix = "#{label}: " if label + + ["#{prefix}#{actual} over #{threshold}"] + else + [] + end + end + def format_duration(ms) if ms >= 1000 "%.2fms" % ms else diff --git a/lib/peek/views/gitaly.rb b/lib/peek/views/gitaly.rb index 6ad6ddfd89d..f669feae254 100644 --- a/lib/peek/views/gitaly.rb +++ b/lib/peek/views/gitaly.rb @@ -3,6 +3,24 @@ module Peek module Views class Gitaly < DetailedView + DEFAULT_THRESHOLDS = { + calls: 30, + duration: 1, + individual_call: 0.5 + }.freeze + + THRESHOLDS = { + production: { + calls: 30, + duration: 1, + individual_call: 0.5 + } + }.freeze + + def self.thresholds + @thresholds ||= THRESHOLDS.fetch(Rails.env.to_sym, DEFAULT_THRESHOLDS) + end + private def duration diff --git a/lib/peek/views/rugged.rb b/lib/peek/views/rugged.rb index 18b3f422852..3ed54a010f8 100644 --- a/lib/peek/views/rugged.rb +++ b/lib/peek/views/rugged.rb @@ -12,7 +12,7 @@ module Peek private def duration - ::Gitlab::RuggedInstrumentation.query_time + ::Gitlab::RuggedInstrumentation.query_time_ms end def calls diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index 37bca97fec7..7541baed467 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -34,6 +34,10 @@ module QA element :dropdown_filter_input end + view 'app/assets/javascripts/ide/components/commit_sidebar/actions.vue' do + element :commit_to_current_branch_radio + end + view 'app/assets/javascripts/ide/components/commit_sidebar/form.vue' do element :begin_commit_button element :commit_button @@ -104,7 +108,7 @@ module QA # animation is still in process even when the buttons have the # expected visibility. commit_success_msg_shown = retry_until do - uncheck_element :start_new_mr_checkbox + click_element :commit_to_current_branch_radio click_element :commit_button wait(reload: false) do diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb index 4203f58fe81..6920fb4e572 100644 --- a/spec/finders/members_finder_spec.rb +++ b/spec/finders/members_finder_spec.rb @@ -17,11 +17,10 @@ describe MembersFinder, '#execute' do result = described_class.new(project, user2).execute - expect(result.to_a).to match_array([member1, member2, member3]) + expect(result).to contain_exactly(member1, member2, member3) end - it 'includes nested group members if asked' do - project = create(:project, namespace: group) + it 'includes nested group members if asked', :nested_groups do nested_group.request_access(user1) member1 = group.add_maintainer(user2) member2 = nested_group.add_maintainer(user3) @@ -29,7 +28,28 @@ describe MembersFinder, '#execute' do result = described_class.new(project, user2).execute(include_descendants: true) - expect(result.to_a).to match_array([member1, member2, member3]) + expect(result).to contain_exactly(member1, member2, member3) + end + + it 'returns the members.access_level when the user is invited', :nested_groups do + member_invite = create(:project_member, :invited, project: project, invite_email: create(:user).email) + member1 = group.add_maintainer(user2) + + result = described_class.new(project, user2).execute(include_descendants: true) + + expect(result).to contain_exactly(member1, member_invite) + expect(result.last.access_level).to eq(member_invite.access_level) + end + + it 'returns the highest access_level for the user', :nested_groups do + member1 = project.add_guest(user1) + group.add_developer(user1) + nested_group.add_reporter(user1) + + result = described_class.new(project, user1).execute(include_descendants: true) + + expect(result).to contain_exactly(member1) + expect(result.first.access_level).to eq(Gitlab::Access::DEVELOPER) end context 'when include_invited_groups_members == true' do @@ -37,8 +57,8 @@ describe MembersFinder, '#execute' do set(:linked_group) { create(:group, :public, :access_requestable) } set(:nested_linked_group) { create(:group, parent: linked_group) } - set(:linked_group_member) { linked_group.add_developer(user1) } - set(:nested_linked_group_member) { nested_linked_group.add_developer(user2) } + set(:linked_group_member) { linked_group.add_guest(user1) } + set(:nested_linked_group_member) { nested_linked_group.add_guest(user2) } it 'includes all the invited_groups members including members inherited from ancestor groups' do create(:project_group_link, project: project, group: nested_linked_group) @@ -60,5 +80,17 @@ describe MembersFinder, '#execute' do expect(subject).to contain_exactly(linked_group_member) end + + context 'when the user is a member of invited group and ancestor groups' do + it 'returns the highest access_level for the user limited by project_group_link.group_access', :nested_groups do + create(:project_group_link, project: project, group: nested_linked_group, group_access: Gitlab::Access::REPORTER) + nested_linked_group.add_developer(user1) + + result = subject + + expect(result).to contain_exactly(linked_group_member, nested_linked_group_member) + expect(result.first.access_level).to eq(Gitlab::Access::REPORTER) + end + end end end diff --git a/spec/fixtures/api/schemas/entities/merge_request_noteable.json b/spec/fixtures/api/schemas/entities/merge_request_noteable.json new file mode 100644 index 00000000000..88b0fecc24c --- /dev/null +++ b/spec/fixtures/api/schemas/entities/merge_request_noteable.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "properties" : { + "merge_params": { "type": ["object", "null"] }, + "state": { "type": "string" }, + "source_branch": { "type": "string" }, + "target_branch": { "type": "string" }, + "diff_head_sha": { "type": "string" }, + "create_note_path": { "type": ["string", "null"] }, + "preview_note_path": { "type": ["string", "null"] }, + "create_issue_to_resolve_discussions_path": { "type": ["string", "null"] }, + "new_blob_path": { "type": ["string", "null"] }, + "can_receive_suggestion": { "type": "boolean" }, + "current_user": { + "type": "object", + "required": [ + "can_create_note", + "can_update" + ], + "properties": { + "can_create_note": { "type": "boolean" }, + "can_update": { "type": "boolean" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json index 2052892dfa3..1eda0e12920 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json @@ -24,22 +24,20 @@ "ci_status": { "type": ["string", "null"] }, "cancel_auto_merge_path": { "type": ["string", "null"] }, "test_reports_path": { "type": ["string", "null"] }, - "can_receive_suggestion": { "type": "boolean" }, "create_issue_to_resolve_discussions_path": { "type": ["string", "null"] }, "current_user": { "type": "object", "required": [ "can_remove_source_branch", "can_revert_on_current_merge_request", - "can_cherry_pick_on_current_merge_request" + "can_cherry_pick_on_current_merge_request", + "can_create_issue" ], "properties": { "can_remove_source_branch": { "type": "boolean" }, "can_revert_on_current_merge_request": { "type": ["boolean", "null"] }, "can_cherry_pick_on_current_merge_request": { "type": ["boolean", "null"] }, - "can_create_note": { "type": "boolean" }, - "can_create_issue": { "type": "boolean" }, - "can_update": { "type": "boolean" } + "can_create_issue": { "type": "boolean" } }, "additionalProperties": false }, diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index 779a47222b7..e2df7952d8f 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -5,7 +5,6 @@ { "$ref": "merge_request_poll_widget.json" }, { "properties" : { - "merge_params": { "type": ["object", "null"] }, "source_project_full_path": { "type": ["string", "null"]}, "target_project_full_path": { "type": ["string", "null"]}, "email_patches_path": { "type": "string" }, @@ -13,9 +12,7 @@ "merge_request_basic_path": { "type": "string" }, "merge_request_widget_path": { "type": "string" }, "merge_request_cached_widget_path": { "type": "string" }, - "create_note_path": { "type": ["string", "null"] }, "commit_change_content_path": { "type": "string" }, - "preview_note_path": { "type": ["string", "null"] }, "conflicts_docs_path": { "type": ["string", "null"] }, "merge_request_pipelines_docs_path": { "type": ["string", "null"] }, "ci_environments_status_path": { "type": "string" }, diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js new file mode 100644 index 00000000000..1f4d1e17ea0 --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -0,0 +1,55 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; + +describe('MRWidgetAutoMergeFailed', () => { + let wrapper; + const mergeError = 'This is the merge error'; + const findButton = () => wrapper.find('button'); + + const createComponent = (props = {}) => { + wrapper = shallowMount(AutoMergeFailedComponent, { + sync: false, + propsData: { ...props }, + }); + }; + + beforeEach(() => { + createComponent({ + mr: { mergeError }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders failed message', () => { + expect(wrapper.text()).toContain('This merge request failed to be merged automatically'); + }); + + it('renders merge error provided', () => { + expect(wrapper.text()).toContain(mergeError); + }); + + it('render refresh button', () => { + expect(findButton().text()).toEqual('Refresh'); + }); + + it('emits event and shows loading icon when button is clicked', () => { + jest.spyOn(eventHub, '$emit'); + findButton().trigger('click'); + + expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested'); + + return wrapper.vm.$nextTick(() => { + expect(findButton().attributes('disabled')).toEqual('disabled'); + expect( + findButton() + .find(GlLoadingIcon) + .exists(), + ).toBe(true); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js new file mode 100644 index 00000000000..328eec0a80a --- /dev/null +++ b/spec/frontend/vue_shared/components/file_icon_spec.js @@ -0,0 +1,75 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +describe('File Icon component', () => { + let wrapper; + const findIcon = () => wrapper.find('svg'); + const getIconName = () => + findIcon() + .find('use') + .element.getAttribute('xlink:href') + .replace(`${gon.sprite_file_icons}#`, ''); + + const createComponent = (props = {}) => { + wrapper = shallowMount(FileIcon, { + sync: false, + propsData: { ...props }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render a span element and an icon', () => { + createComponent({ + fileName: 'test.js', + }); + + expect(wrapper.element.tagName).toEqual('SPAN'); + expect(findIcon().exists()).toBeDefined(); + }); + + it.each` + fileName | iconName + ${'test.js'} | ${'javascript'} + ${'test.png'} | ${'image'} + ${'webpack.js'} | ${'webpack'} + `('should render a $iconName icon based on file ending', ({ fileName, iconName }) => { + createComponent({ fileName }); + expect(getIconName()).toBe(iconName); + }); + + it('should render a standard folder icon', () => { + createComponent({ + fileName: 'js', + folder: true, + }); + + expect(findIcon().exists()).toBe(false); + expect(wrapper.find(Icon).props('cssClasses')).toContain('folder-icon'); + }); + + it('should render a loading icon', () => { + createComponent({ + fileName: 'test.js', + loading: true, + }); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('should add a special class and a size class', () => { + const size = 120; + createComponent({ + fileName: 'test.js', + cssClasses: 'extraclasses', + size, + }); + + expect(findIcon().classes()).toContain(`s${size}`); + expect(findIcon().classes()).toContain('extraclasses'); + }); +}); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 388d7063d13..f9ee4648128 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -106,6 +106,7 @@ describe('Environment item', () => { play_path: '/play', }, ], + deployed_at: '2016-11-29T18:11:58.430Z', }, has_stop_action: true, environment_path: 'root/ci-folders/environments/31', @@ -139,9 +140,7 @@ describe('Environment item', () => { it('should render last deployment date', () => { const timeagoInstance = new timeago(); // eslint-disable-line - const formatedDate = timeagoInstance.format( - environment.last_deployment.deployable.created_at, - ); + const formatedDate = timeagoInstance.format(environment.last_deployment.deployed_at); expect( component.$el.querySelector('.environment-created-date-timeago').textContent, diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js deleted file mode 100644 index 55a11a72551..00000000000 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ /dev/null @@ -1,47 +0,0 @@ -import Vue from 'vue'; -import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; -import eventHub from '~/vue_merge_request_widget/event_hub'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('MRWidgetAutoMergeFailed', () => { - let vm; - const mergeError = 'This is the merge error'; - - beforeEach(() => { - const Component = Vue.extend(autoMergeFailedComponent); - vm = mountComponent(Component, { - mr: { mergeError }, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders failed message', () => { - expect(vm.$el.textContent).toContain('This merge request failed to be merged automatically'); - }); - - it('renders merge error provided', () => { - expect(vm.$el.innerText).toContain(mergeError); - }); - - it('render refresh button', () => { - expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Refresh'); - }); - - it('emits event and shows loading icon when button is clicked', done => { - spyOn(eventHub, '$emit'); - vm.$el.querySelector('button').click(); - - expect(eventHub.$emit.calls.argsFor(0)[0]).toEqual('MRWidgetUpdateRequested'); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); - expect(vm.$el.querySelector('button .loading-container span').classList).toContain( - 'gl-spinner', - ); - done(); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js deleted file mode 100644 index 1f61e19fa84..00000000000 --- a/spec/javascripts/vue_shared/components/file_icon_spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import Vue from 'vue'; -import fileIcon from '~/vue_shared/components/file_icon.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('File Icon component', () => { - let vm; - let FileIcon; - - beforeEach(() => { - FileIcon = Vue.extend(fileIcon); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should render a span element with an svg', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.js', - }); - - expect(vm.$el.tagName).toEqual('SPAN'); - expect(vm.$el.querySelector('span > svg')).toBeDefined(); - }); - - it('should render a javascript icon based on file ending', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.js', - }); - - expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe( - `${gon.sprite_file_icons}#javascript`, - ); - }); - - it('should render a image icon based on file ending', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.png', - }); - - expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe( - `${gon.sprite_file_icons}#image`, - ); - }); - - it('should render a webpack icon based on file namer', () => { - vm = mountComponent(FileIcon, { - fileName: 'webpack.js', - }); - - expect(vm.$el.firstChild.firstChild.getAttribute('xlink:href')).toBe( - `${gon.sprite_file_icons}#webpack`, - ); - }); - - it('should render a standard folder icon', () => { - vm = mountComponent(FileIcon, { - fileName: 'js', - folder: true, - }); - - expect(vm.$el.querySelector('span > svg > use').getAttribute('xlink:href')).toBe( - `${gon.sprite_file_icons}#folder`, - ); - }); - - it('should render a loading icon', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.js', - loading: true, - }); - - const { classList } = vm.$el.querySelector('.loading-container span'); - - expect(classList.contains('gl-spinner')).toEqual(true); - }); - - it('should add a special class and a size class', () => { - vm = mountComponent(FileIcon, { - fileName: 'test.js', - cssClasses: 'extraclasses', - size: 120, - }); - - const { classList } = vm.$el.firstChild; - const containsSizeClass = classList.contains('s120'); - const containsCustomClass = classList.contains('extraclasses'); - - expect(containsSizeClass).toBe(true); - expect(containsCustomClass).toBe(true); - }); -}); diff --git a/spec/lib/gitlab/performance_bar/with_top_level_warnings_spec.rb b/spec/lib/gitlab/performance_bar/with_top_level_warnings_spec.rb new file mode 100644 index 00000000000..3b92261f0fe --- /dev/null +++ b/spec/lib/gitlab/performance_bar/with_top_level_warnings_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +describe Gitlab::PerformanceBar::WithTopLevelWarnings do + using RSpec::Parameterized::TableSyntax + + subject { Module.new } + + before do + subject.singleton_class.prepend(described_class) + end + + describe '#has_warnings?' do + where(:has_warnings, :results) do + false | { data: {} } + false | { data: { gitaly: { warnings: [] } } } + true | { data: { gitaly: { warnings: [1] } } } + true | { data: { gitaly: { warnings: [] }, redis: { warnings: [1] } } } + end + + with_them do + it do + expect(subject.has_warnings?(results)).to eq(has_warnings) + end + end + end +end diff --git a/spec/lib/peek/views/detailed_view_spec.rb b/spec/lib/peek/views/detailed_view_spec.rb new file mode 100644 index 00000000000..d8660a55ea9 --- /dev/null +++ b/spec/lib/peek/views/detailed_view_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Peek::Views::DetailedView, :request_store do + context 'when a class defines thresholds' do + let(:threshold_view) do + Class.new(described_class) do + def self.thresholds + { + calls: 1, + duration: 10, + individual_call: 5 + } + end + + def key + 'threshold-view' + end + end.new + end + + context 'when the results exceed the calls threshold' do + before do + allow(threshold_view) + .to receive(:detail_store).and_return([{ duration: 0.001 }, { duration: 0.001 }]) + end + + it 'adds a warning to the results key' do + expect(threshold_view.results).to include(warnings: [a_string_matching('threshold-view calls')]) + end + end + + context 'when the results exceed the duration threshold' do + before do + allow(threshold_view) + .to receive(:detail_store).and_return([{ duration: 0.011 }]) + end + + it 'adds a warning to the results key' do + expect(threshold_view.results).to include(warnings: [a_string_matching('threshold-view duration')]) + end + end + + context 'when a single call exceeds the duration threshold' do + before do + allow(threshold_view) + .to receive(:detail_store).and_return([{ duration: 0.001 }, { duration: 0.006 }]) + end + + it 'adds a warning to that call detail entry' do + expect(threshold_view.results) + .to include(details: a_collection_containing_exactly( + { duration: 1.0, warnings: [] }, + { duration: 6.0, warnings: ['6.0 over 5'] } + )) + end + end + end + + context 'when a view does not define thresholds' do + let(:no_threshold_view) { Class.new(described_class).new } + + before do + allow(no_threshold_view) + .to receive(:detail_store).and_return([{ duration: 100 }, { duration: 100 }]) + end + + it 'does not add warnings to the top level' do + expect(no_threshold_view.results).to include(warnings: []) + end + + it 'does not add warnings to call details entries' do + expect(no_threshold_view.results) + .to include(details: a_collection_containing_exactly( + { duration: 100000, warnings: [] }, + { duration: 100000, warnings: [] } + )) + end + end +end diff --git a/spec/lib/peek/views/redis_detailed_spec.rb b/spec/lib/peek/views/redis_detailed_spec.rb index 61096e6c69e..fa9532226f2 100644 --- a/spec/lib/peek/views/redis_detailed_spec.rb +++ b/spec/lib/peek/views/redis_detailed_spec.rb @@ -21,10 +21,10 @@ describe Peek::Views::RedisDetailed, :request_store do expect(subject.results[:details].count).to eq(1) expect(subject.results[:details].first) - .to eq({ - cmd: expected, - duration: 1000 - }) + .to include({ + cmd: expected, + duration: 1000 + }) end end diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb index 276e0f6ff3d..d1483c3c41e 100644 --- a/spec/serializers/merge_request_serializer_spec.rb +++ b/spec/serializers/merge_request_serializer_spec.rb @@ -41,6 +41,14 @@ describe MergeRequestSerializer do end end + context 'noteable merge request serialization' do + let(:serializer) { 'noteable' } + + it 'matches noteable merge request json schema' do + expect(json_entity).to match_schema('entities/merge_request_noteable', strict: true) + end + end + context 'no serializer' do let(:serializer) { nil } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index f46f9633c1c..910fe3b50b7 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -212,6 +212,13 @@ describe SystemNoteService do expect(build_note([assignee, assignee1, assignee2], [assignee, assignee1])).to eq \ "unassigned @#{assignee2.username}" end + + it 'builds a correct phrase when the locale is different' do + Gitlab::I18n.with_locale('pt-BR') do + expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \ + "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}" + end + end end describe '.change_milestone' do |