diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index c0f53afa039..4b25908aa6a 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -4,7 +4,7 @@ - .docs:rules:review-docs image: ruby:2.6-alpine stage: review - dependencies: [] + needs: [] variables: # We're cloning the repo instead of downloading the script for now # because some repos are private and CI_JOB_TOKEN cannot access files. diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8c9e2e30a5b..21599a74a56 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1274,14 +1274,12 @@ Graphql/IDType: Exclude: - 'ee/app/graphql/ee/mutations/issues/update.rb' - 'ee/app/graphql/ee/types/boards/board_issue_input_base_type.rb' - - 'ee/app/graphql/mutations/instance_security_dashboard/remove_project.rb' - 'ee/app/graphql/mutations/issues/set_epic.rb' - 'ee/app/graphql/mutations/iterations/update.rb' - 'ee/app/graphql/resolvers/iterations_resolver.rb' - 'app/graphql/mutations/boards/create.rb' - 'app/graphql/mutations/boards/issues/issue_move_list.rb' - 'app/graphql/mutations/boards/lists/update.rb' - - 'app/graphql/mutations/discussions/toggle_resolve.rb' - 'app/graphql/mutations/issues/update.rb' - 'app/graphql/mutations/metrics/dashboard/annotations/delete.rb' - 'app/graphql/mutations/snippets/destroy.rb' diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue index 64c1a2565be..7aa5c98aa0b 100644 --- a/app/assets/javascripts/analytics/instance_statistics/components/app.vue +++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue @@ -1,19 +1,30 @@ diff --git a/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue new file mode 100644 index 00000000000..a4a1d40b70b --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue @@ -0,0 +1,143 @@ + + diff --git a/app/assets/javascripts/analytics/instance_statistics/constants.js b/app/assets/javascripts/analytics/instance_statistics/constants.js index 5ea5d17c974..846c0ef408b 100644 --- a/app/assets/javascripts/analytics/instance_statistics/constants.js +++ b/app/assets/javascripts/analytics/instance_statistics/constants.js @@ -1,5 +1,5 @@ import { getDateInPast } from '~/lib/utils/datetime_utility'; -const TOTAL_DAYS_TO_SHOW = 365; +export const TOTAL_DAYS_TO_SHOW = 365; export const TODAY = new Date(); export const START_DATE = getDateInPast(TODAY, TOTAL_DAYS_TO_SHOW); diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/fragments/count.fragment.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/fragments/count.fragment.graphql new file mode 100644 index 00000000000..40cef95c2e7 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/graphql/fragments/count.fragment.graphql @@ -0,0 +1,4 @@ +fragment Count on InstanceStatisticsMeasurement { + count + recordedAt +} diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql index fd8282683d9..f14c2658674 100644 --- a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql +++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql @@ -1,32 +1,34 @@ +#import "../fragments/count.fragment.graphql" + query getInstanceCounts { projects: instanceStatisticsMeasurements(identifier: PROJECTS, first: 1) { nodes { - count + ...Count } } groups: instanceStatisticsMeasurements(identifier: GROUPS, first: 1) { nodes { - count + ...Count } } users: instanceStatisticsMeasurements(identifier: USERS, first: 1) { nodes { - count + ...Count } } issues: instanceStatisticsMeasurements(identifier: ISSUES, first: 1) { nodes { - count + ...Count } } mergeRequests: instanceStatisticsMeasurements(identifier: MERGE_REQUESTS, first: 1) { nodes { - count + ...Count } } pipelines: instanceStatisticsMeasurements(identifier: PIPELINES, first: 1) { nodes { - count + ...Count } } } diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/users.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/users.query.graphql new file mode 100644 index 00000000000..6235e36eb89 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/users.query.graphql @@ -0,0 +1,13 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" +#import "../fragments/count.fragment.graphql" + +query getUsersCount($first: Int, $after: String) { + users: instanceStatisticsMeasurements(identifier: USERS, first: $first, after: $after) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } +} diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss index 0f786681146..a3338ff13b5 100644 --- a/app/assets/stylesheets/fontawesome_custom.scss +++ b/app/assets/stylesheets/fontawesome_custom.scss @@ -125,10 +125,6 @@ content: '\f077'; } -.fa-github::before { - content: '\f09b'; -} - .fa-paperclip::before { content: '\f0c6'; } diff --git a/app/graphql/mutations/discussions/toggle_resolve.rb b/app/graphql/mutations/discussions/toggle_resolve.rb index 41fd22c6b55..4492da74706 100644 --- a/app/graphql/mutations/discussions/toggle_resolve.rb +++ b/app/graphql/mutations/discussions/toggle_resolve.rb @@ -8,7 +8,7 @@ module Mutations description 'Toggles the resolved state of a discussion' argument :id, - GraphQL::ID_TYPE, + Types::GlobalIDType[Discussion], required: true, description: 'The global id of the discussion' @@ -54,7 +54,10 @@ module Mutations end def find_object(id:) - GitlabSchema.object_from_id(id, expected_type: ::Discussion) + # TODO: remove explicit coercion once compatibility layer has been removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = Types::GlobalIDType[Discussion].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) end def resolve!(discussion) diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 140420a32bd..ab9d11abe98 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -80,6 +80,7 @@ module Boards set_scope set_non_archived set_attempt_search_optimizations + set_issue_types params end @@ -116,6 +117,10 @@ module Boards end end + def set_issue_types + params[:issue_types] = Issue::TYPES_FOR_LIST + end + # rubocop: disable CodeReuse/ActiveRecord def board_label_ids @board_label_ids ||= board.lists.movable.pluck(:label_id) diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb index 2de6ed9fa1c..3145739fe91 100644 --- a/app/services/issues/build_service.rb +++ b/app/services/issues/build_service.rb @@ -64,20 +64,26 @@ module Issues private - def whitelisted_issue_params - base_params = [:title, :description, :confidential] - admin_params = [:milestone_id, :issue_type] + def allowed_issue_base_params + [:title, :description, :confidential, :issue_type] + end + def allowed_issue_admin_params + [:milestone_id] + end + + def allowed_issue_params if can?(current_user, :admin_issue, project) - params.slice(*(base_params + admin_params)) + params.slice(*(allowed_issue_base_params + allowed_issue_admin_params)) else - params.slice(*base_params) + params.slice(*allowed_issue_base_params) end end def build_issue_params - { author: current_user }.merge(issue_params_with_info_from_discussions) - .merge(whitelisted_issue_params) + { author: current_user } + .merge(issue_params_with_info_from_discussions) + .merge(allowed_issue_params) end end end diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index e86d4236be8..7e49cad7902 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -10,7 +10,9 @@ = import_github_authorize_message - if github_import_configured? && !has_ci_cd_only_params? - = link_to icon('github', text: title), status_import_github_path, class: 'btn btn-success' + = link_to status_import_github_path, class: 'btn btn-success gl-button' do + = sprite_icon('github', css_class: 'gl-mr-2') + = title %hr diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index ee295e70cce..ba6a5657d12 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -2,7 +2,9 @@ - page_title title - breadcrumb_title title - header_title _("Projects"), root_path -%h3.page-title.mb-0 - = icon 'github', class: 'fa-2x', text: _('Import repositories from GitHub') +%h3.page-title.mb-0.gl-display-flex + .gl-display-flex.gl-align-items-center.gl-justify-content-center + = sprite_icon('github', css_class: 'gl-mr-2') + = _('Import repositories from GitHub') = render 'import/githubish_status', provider: 'github' diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml index 58f1d7b4990..8b94133fd8a 100644 --- a/app/views/projects/_import_project_pane.html.haml +++ b/app/views/projects/_import_project_pane.html.haml @@ -15,7 +15,8 @@ - if github_import_enabled? %div = link_to new_import_github_path, class: 'btn js-import-github', **tracking_attrs(track_label, 'click_button', 'github') do - = icon('github', text: 'GitHub') + = sprite_icon('github') + GitHub - if bitbucket_import_enabled? %div diff --git a/app/views/projects/pages/_pages_settings.html.haml b/app/views/projects/pages/_pages_settings.html.haml index 8aa02074205..51483176d6f 100644 --- a/app/views/projects/pages/_pages_settings.html.haml +++ b/app/views/projects/pages/_pages_settings.html.haml @@ -1,6 +1,7 @@ = form_for @project, url: project_pages_path(@project), html: { class: 'inline', title: pages_https_only_title } do |f| + = render_if_exists 'shared/pages/max_pages_size_input', form: f + - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https - = render_if_exists 'shared/pages/max_pages_size_input', form: f .form-group .form-check @@ -9,5 +10,5 @@ %strong = s_('GitLabPages|Force HTTPS (requires valid certificates)') - .gl-mt-3 - = f.submit s_('GitLabPages|Save'), class: 'btn btn-success' + .gl-mt-3 + = f.submit s_('GitLabPages|Save'), class: 'btn btn-success gl-button' diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml index 0281c093636..3347966f39a 100644 --- a/app/views/shared/issuable/form/_type_selector.html.haml +++ b/app/views/shared/issuable/form/_type_selector.html.haml @@ -1,4 +1,4 @@ -- return unless issuable.supports_issue_type? && can?(current_user, :admin_issue, @project) +- return unless issuable.supports_issue_type? && can?(current_user, :create_issue, @project) .form-group.row.gl-mb-0 = form.label :type, 'Type', class: 'col-form-label col-sm-2' diff --git a/changelogs/unreleased/216438-add-lsif-to-go-auto-devops-gitlab-ci-yml.yml b/changelogs/unreleased/216438-add-lsif-to-go-auto-devops-gitlab-ci-yml.yml new file mode 100644 index 00000000000..08df708e2da --- /dev/null +++ b/changelogs/unreleased/216438-add-lsif-to-go-auto-devops-gitlab-ci-yml.yml @@ -0,0 +1,5 @@ +--- +title: Add LSIF to Go Auto DevOps gitlab-ci.yml +merge_request: 40072 +author: +type: added diff --git a/changelogs/unreleased/268042-graphql-use-new-global_id-for-discussion-mutations.yml b/changelogs/unreleased/268042-graphql-use-new-global_id-for-discussion-mutations.yml new file mode 100644 index 00000000000..45cace04adc --- /dev/null +++ b/changelogs/unreleased/268042-graphql-use-new-global_id-for-discussion-mutations.yml @@ -0,0 +1,5 @@ +--- +title: Update GraphQL discussionToggleResolve mutation input id to be more type-specific +merge_request: 45346 +author: +type: changed diff --git a/changelogs/unreleased/john_long-update_project_pages_settings.yml b/changelogs/unreleased/john_long-update_project_pages_settings.yml new file mode 100644 index 00000000000..45fde761968 --- /dev/null +++ b/changelogs/unreleased/john_long-update_project_pages_settings.yml @@ -0,0 +1,5 @@ +--- +title: Allow size limit to be available by default in the project pages settings form +merge_request: 45054 +author: +type: fixed diff --git a/changelogs/unreleased/mw-replace-fa-github-icons.yml b/changelogs/unreleased/mw-replace-fa-github-icons.yml new file mode 100644 index 00000000000..4959d502263 --- /dev/null +++ b/changelogs/unreleased/mw-replace-fa-github-icons.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-github with GitLab SVG MERGE_REQUEST_ID +merge_request: 45533 +author: +type: changed diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 3888c8f1bee..adb1f719f3c 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -91,8 +91,8 @@ The following metrics are available: | `gitlab_transaction_rails_queue_duration_total` | Counter | 9.4 | Measures latency between GitLab Workhorse forwarding a request to Rails | `controller`, `action` | | `gitlab_transaction_view_duration_total` | Counter | 9.4 | Duration for views | `controller`, `action`, `view` | | `gitlab_view_rendering_duration_seconds` | Histogram | 10.2 | Duration for views (histogram) | `controller`, `action`, `view` | -| `http_requests_total` | Counter | 9.4 | Rack request count | `method` | -| `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | `method`, `status` | +| `http_requests_total` | Counter | 9.4 | Rack request count | `method`, `status` | +| `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | `method` | | `gitlab_transaction_db_count_total` | Counter | 13.1 | Counter for total number of SQL calls | `controller`, `action` | | `gitlab_transaction_db_write_count_total` | Counter | 13.1 | Counter for total number of write SQL calls | `controller`, `action` | | `gitlab_transaction_db_cached_count_total` | Counter | 13.1 | Counter for total number of cached SQL calls | `controller`, `action` | diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index f5a7b9839b1..a44f8f70311 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -5870,7 +5870,7 @@ input DiscussionToggleResolveInput { """ The global id of the discussion """ - id: ID! + id: DiscussionID! """ Will resolve the discussion when true, and unresolve the discussion when false @@ -16396,7 +16396,7 @@ input RemoveProjectFromSecurityDashboardInput { """ ID of the project to remove from the Instance Security Dashboard """ - id: ID! + id: ProjectID! } """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 8edd30c0172..6914ba29c57 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -16105,7 +16105,7 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "DiscussionID", "ofType": null } }, @@ -47191,7 +47191,7 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "ProjectID", "ofType": null } }, diff --git a/doc/ci/README.md b/doc/ci/README.md index 9bea4a707d7..dca6d8baa79 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -24,7 +24,7 @@ webcast to learn about continuous methods and how GitLab’s built-in CI can hel ## Overview Continuous Integration works by pushing small code chunks to your -application's code base hosted in a Git repository, and to every +application's codebase hosted in a Git repository, and to every push, run a pipeline of scripts to build, test, and validate the code changes before merging them into the main branch. diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md index bd6be48ecef..0026cf4d18a 100644 --- a/doc/topics/autodevops/customize.md +++ b/doc/topics/autodevops/customize.md @@ -392,6 +392,7 @@ The following table lists variables used to disable jobs. | `SAST_DISABLED` | From GitLab 11.0, used to disable the `sast` job. If the variable is present, the job won't be created. | | `TEST_DISABLED` | From GitLab 11.0, used to disable the `test` job. If the variable is present, the job won't be created. | | `SECRET_DETECTION_DISABLED` | From GitLab 13.1, used to disable the `secret_detection` job. If the variable is present, the job won't be created. | +| `CODE_INTELLIGENCE_DISABLED` | From GitLab 13.6, used to disable the `code_intelligence` job. If the variable is present, the job won't be created. | ### Application secret variables diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 2a4ca3977a5..1952fadc076 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -95,6 +95,7 @@ project in a simple and automatic way: 1. [Auto Deploy](stages.md#auto-deploy) 1. [Auto Browser Performance Testing](stages.md#auto-browser-performance-testing) **(PREMIUM)** 1. [Auto Monitoring](stages.md#auto-monitoring) +1. [Auto Code Intelligence](stages.md#auto-code-intelligence) As Auto DevOps relies on many different components, you should have a basic knowledge of the following: diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md index 3fec3c4c621..d3f02889a2e 100644 --- a/doc/topics/autodevops/stages.md +++ b/doc/topics/autodevops/stages.md @@ -665,3 +665,7 @@ To use Auto Monitoring: whole Kubernetes cluster, navigate to **Operations > Metrics**. ![Auto Metrics](img/auto_monitoring.png) + +## Auto Code Intelligence + +Code Intelligence is powered by [LSIF](https://lsif.dev/) and available for Go at this stage. We'll support more languages as they become available. diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 6966ce88b30..cba13f374f4 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -20,6 +20,7 @@ # * dast: DAST_DISABLED # * review: REVIEW_DISABLED # * stop_review: REVIEW_DISABLED +# * code_intelligence: CODE_INTELLIGENCE_DISABLED # # In order to deploy, you must have a Kubernetes cluster configured either # via a project integration, or via group/project variables. @@ -159,6 +160,7 @@ include: - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml + - template: Jobs/Code-Intelligence.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml - template: Jobs/Deploy/ECS.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml - template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml diff --git a/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml new file mode 100644 index 00000000000..83bc5548614 --- /dev/null +++ b/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml @@ -0,0 +1,16 @@ +code_intelligence_go: + stage: test + needs: [] + allow_failure: true + image: sourcegraph/lsif-go:v1 + rules: + - if: $CODE_INTELLIGENCE_DISABLED + when: never + - if: $CI_COMMIT_BRANCH + exists: + - '**/*.go' + script: + - lsif-go + artifacts: + reports: + lsif: dump.lsif diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb index 99a44c5eeb9..f6bda0dbea4 100644 --- a/lib/gitlab/metrics/requests_rack_middleware.rb +++ b/lib/gitlab/metrics/requests_rack_middleware.rb @@ -3,15 +3,7 @@ module Gitlab module Metrics class RequestsRackMiddleware - HTTP_METHODS = { - "delete" => %w(200 202 204 303 400 401 403 404 500 503), - "get" => %w(200 204 301 302 303 304 307 400 401 403 404 410 422 429 500 503), - "head" => %w(200 204 301 302 303 401 403 404 410 500), - "options" => %w(200 404), - "patch" => %w(200 202 204 400 403 404 409 416 500), - "post" => %w(200 201 202 204 301 302 303 304 400 401 403 404 406 409 410 412 422 429 500 503), - "put" => %w(200 202 204 400 401 403 404 405 406 409 410 422 500) - }.freeze + HTTP_METHODS = %w(delete get head options patch post put).to_set.freeze HEALTH_ENDPOINT = /^\/-\/(liveness|readiness|health|metrics)\/?$/.freeze @@ -40,16 +32,14 @@ module Gitlab end def self.initialize_http_request_duration_seconds - HTTP_METHODS.each do |method, statuses| - statuses.each do |status| - http_request_duration_seconds.get({ method: method, status: status.to_s }) - end + HTTP_METHODS.each do |method| + http_request_duration_seconds.get({ method: method }) end end def call(env) method = env['REQUEST_METHOD'].downcase - method = 'INVALID' unless HTTP_METHODS.key?(method) + method = 'INVALID' unless HTTP_METHODS.include?(method) started = Time.now.to_f health_endpoint = health_endpoint?(env['PATH_INFO']) status = 'undefined' @@ -62,7 +52,7 @@ module Gitlab feature_category = headers&.fetch(FEATURE_CATEGORY_HEADER, nil) unless health_endpoint - RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method, status: status.to_s }, elapsed) + RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method }, elapsed) end [status, headers, body] diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 73f2116facc..2eff8d1365f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7513,6 +7513,9 @@ msgstr "" msgid "Could not load instance counts. Please refresh the page to try again." msgstr "" +msgid "Could not load the user chart. Please refresh the page to try again." +msgstr "" + msgid "Could not remove the trigger." msgstr "" @@ -26510,6 +26513,9 @@ msgstr "" msgid "There is no chart data available." msgstr "" +msgid "There is no data available." +msgstr "" + msgid "There is no data available. Please change your selection." msgstr "" @@ -27780,6 +27786,9 @@ msgstr "" msgid "Total test time for all commits/merges" msgstr "" +msgid "Total users" +msgstr "" + msgid "Total weight" msgstr "" diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index bfc825990b6..7e296528795 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -23,10 +23,6 @@ module QA element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern end - view 'app/views/projects/_import_project_pane.html.haml' do - element :import_github, "icon('github', text: 'GitHub')" # rubocop:disable QA/ElementWithPattern - end - view 'app/views/projects/project_templates/_template.html.haml' do element :use_template_button element :template_option_row diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index fb2019394b4..3a17acbb317 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -387,7 +387,7 @@ module QA end def verify_storage_move_to_praefect(repo_path, virtual_storage) - wait_until_shell_command("docker exec #{@gitlab} bash -c 'tail -n 50 /var/log/gitlab/praefect/current'") do |line| + wait_until_shell_command("docker exec #{@praefect} bash -c 'tail -n 50 /var/log/gitlab/praefect/current'") do |line| log = JSON.parse(line) log['grpc.method'] == 'ReplicateRepository' && log['virtual_storage'] == virtual_storage && log['relative_path'] == repo_path diff --git a/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb index 758ba582929..e96b9ad9258 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb @@ -4,7 +4,6 @@ module QA RSpec.describe 'Create' do describe 'Changing Gitaly repository storage', :requires_admin do praefect_manager = Service::PraefectManager.new - praefect_manager.gitlab = 'gitlab' shared_examples 'repository storage move' do it 'confirms a `finished` status after moving project repository storage' do @@ -28,7 +27,6 @@ module QA context 'when moving from one Gitaly storage to another', :orchestrated, :repository_storage, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/973' do let(:source_storage) { { type: :gitaly, name: 'default' } } let(:destination_storage) { { type: :gitaly, name: QA::Runtime::Env.additional_repository_storage } } - let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'repo-storage-move-status' @@ -37,6 +35,10 @@ module QA end end + before do + praefect_manager.gitlab = 'gitlab' + end + it_behaves_like 'repository storage move' end @@ -46,7 +48,6 @@ module QA context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/974' do let(:source_storage) { { type: :gitaly, name: QA::Runtime::Env.non_cluster_repository_storage } } let(:destination_storage) { { type: :praefect, name: QA::Runtime::Env.praefect_repository_storage } } - let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'repo-storage-move' @@ -56,6 +57,10 @@ module QA end end + before do + praefect_manager.gitlab = 'gitlab-gitaly-cluster' + end + it_behaves_like 'repository storage move' end end diff --git a/spec/features/incidents/user_creates_new_incident_spec.rb b/spec/features/incidents/user_creates_new_incident_spec.rb index 9b9cf396c0a..99a137b5852 100644 --- a/spec/features/incidents/user_creates_new_incident_spec.rb +++ b/spec/features/incidents/user_creates_new_incident_spec.rb @@ -5,34 +5,51 @@ require 'spec_helper' RSpec.describe 'Incident Management index', :js do let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user) } + let_it_be(:guest) { create(:user) } let_it_be(:incident) { create(:incident, project: project) } before_all do project.add_developer(developer) + project.add_guest(guest) end - before do - sign_in(developer) - - visit project_incidents_path(project) - wait_for_requests - end - - context 'when a developer displays the incident list' do + shared_examples 'create incident form' do it 'shows the create new issue button' do expect(page).to have_selector('.create-incident-button') end - it 'shows the create issue page with the Incident type pre-selected when clicked' do + it 'when clicked shows the create issue page with the Incident type pre-selected' do find('.create-incident-button').click - wait_for_requests + wait_for_all_requests - expect(page).to have_selector(".dropdown-menu-toggle") - expect(page).to have_selector(".js-issuable-type-filter-dropdown-wrap") + expect(page).to have_selector('.dropdown-menu-toggle') + expect(page).to have_selector('.js-issuable-type-filter-dropdown-wrap') page.within('.js-issuable-type-filter-dropdown-wrap') do expect(page).to have_content('Incident') end end end + + context 'when a developer displays the incident list' do + before do + sign_in(developer) + + visit project_incidents_path(project) + wait_for_all_requests + end + + it_behaves_like 'create incident form' + end + + context 'when a guest displays the incident list' do + before do + sign_in(guest) + + visit project_incidents_path(project) + wait_for_all_requests + end + + it_behaves_like 'create incident form' + end end diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index 243579ee2f7..c3eea0195a6 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -336,7 +336,7 @@ RSpec.shared_examples 'pages settings editing' do expect(page).not_to have_field(:project_pages_https_only) expect(page).not_to have_content('Force HTTPS (requires valid certificates)') - expect(page).not_to have_button('Save') + expect(page).to have_button('Save') end end end diff --git a/spec/frontend/analytics/instance_statistics/components/app_spec.js b/spec/frontend/analytics/instance_statistics/components/app_spec.js index 39f6d1b450a..df13c9f82a9 100644 --- a/spec/frontend/analytics/instance_statistics/components/app_spec.js +++ b/spec/frontend/analytics/instance_statistics/components/app_spec.js @@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import InstanceStatisticsApp from '~/analytics/instance_statistics/components/app.vue'; import InstanceCounts from '~/analytics/instance_statistics/components//instance_counts.vue'; import PipelinesChart from '~/analytics/instance_statistics/components/pipelines_chart.vue'; +import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue'; describe('InstanceStatisticsApp', () => { let wrapper; @@ -26,4 +27,8 @@ describe('InstanceStatisticsApp', () => { it('displays the pipelines chart component', () => { expect(wrapper.find(PipelinesChart).exists()).toBe(true); }); + + it('displays the users chart component', () => { + expect(wrapper.find(UsersChart).exists()).toBe(true); + }); }); diff --git a/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js b/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js new file mode 100644 index 00000000000..7509c1e6626 --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js @@ -0,0 +1,200 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlAreaChart } from '@gitlab/ui/dist/charts'; +import { GlAlert } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import { useFakeDate } from 'helpers/fake_date'; +import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue'; +import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; +import usersQuery from '~/analytics/instance_statistics/graphql/queries/users.query.graphql'; +import { mockCountsData2, roundedSortedCountsMonthlyChartData2, mockPageInfo } from '../mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('UsersChart', () => { + let wrapper; + let queryHandler; + + const mockApolloResponse = ({ loading = false, hasNextPage = false, users }) => ({ + data: { + users: { + pageInfo: { ...mockPageInfo, hasNextPage }, + nodes: users, + loading, + }, + }, + }); + + const mockQueryResponse = ({ users, loading = false, hasNextPage = false }) => { + const apolloQueryResponse = mockApolloResponse({ loading, hasNextPage, users }); + if (loading) { + return jest.fn().mockReturnValue(new Promise(() => {})); + } + if (hasNextPage) { + return jest + .fn() + .mockResolvedValueOnce(apolloQueryResponse) + .mockResolvedValueOnce( + mockApolloResponse({ + loading, + hasNextPage: false, + users: [{ recordedAt: '2020-07-21', count: 5 }], + }), + ); + } + return jest.fn().mockResolvedValue(apolloQueryResponse); + }; + + const createComponent = ({ + loadingError = false, + loading = false, + users = [], + hasNextPage = false, + } = {}) => { + queryHandler = mockQueryResponse({ users, loading, hasNextPage }); + + return shallowMount(UsersChart, { + props: { + startDate: useFakeDate(2020, 9, 26), + endDate: useFakeDate(2020, 10, 1), + totalDataPoints: mockCountsData2.length, + }, + localVue, + apolloProvider: createMockApollo([[usersQuery, queryHandler]]), + data() { + return { loadingError }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findLoader = () => wrapper.find(ChartSkeletonLoader); + const findAlert = () => wrapper.find(GlAlert); + const findChart = () => wrapper.find(GlAreaChart); + + describe('while loading', () => { + beforeEach(() => { + wrapper = createComponent({ loading: true }); + }); + + it('displays the skeleton loader', () => { + expect(findLoader().exists()).toBe(true); + }); + + it('hides the chart', () => { + expect(findChart().exists()).toBe(false); + }); + }); + + describe('without data', () => { + beforeEach(async () => { + wrapper = createComponent({ users: [] }); + await wrapper.vm.$nextTick(); + }); + + it('renders an no data message', () => { + expect(findAlert().text()).toBe('There is no data available.'); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(false); + }); + }); + + describe('with data', () => { + beforeEach(async () => { + wrapper = createComponent({ users: mockCountsData2 }); + await wrapper.vm.$nextTick(); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(true); + }); + + it('passes the data to the line chart', () => { + expect(findChart().props('data')).toEqual([ + { data: roundedSortedCountsMonthlyChartData2, name: 'Total users' }, + ]); + }); + }); + + describe('with errors', () => { + beforeEach(async () => { + wrapper = createComponent({ loadingError: true }); + await wrapper.vm.$nextTick(); + }); + + it('renders an error message', () => { + expect(findAlert().text()).toBe( + 'Could not load the user chart. Please refresh the page to try again.', + ); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(false); + }); + }); + + describe('when fetching more data', () => { + describe('when the fetchMore query returns data', () => { + beforeEach(async () => { + wrapper = createComponent({ + users: mockCountsData2, + hasNextPage: true, + }); + + jest.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore'); + await wrapper.vm.$nextTick(); + }); + + it('requests data twice', () => { + expect(queryHandler).toBeCalledTimes(2); + }); + + it('calls fetchMore', () => { + expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1); + }); + }); + + describe('when the fetchMore query throws an error', () => { + beforeEach(() => { + wrapper = createComponent({ + users: mockCountsData2, + hasNextPage: true, + }); + + jest + .spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore') + .mockImplementation(jest.fn().mockRejectedValue()); + return wrapper.vm.$nextTick(); + }); + + it('calls fetchMore', () => { + expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1); + }); + + it('renders an error message', () => { + expect(findAlert().text()).toBe( + 'Could not load the user chart. Please refresh the page to try again.', + ); + }); + }); + }); +}); diff --git a/spec/frontend/analytics/instance_statistics/mock_data.js b/spec/frontend/analytics/instance_statistics/mock_data.js index c3f5069da28..b737db4c55f 100644 --- a/spec/frontend/analytics/instance_statistics/mock_data.js +++ b/spec/frontend/analytics/instance_statistics/mock_data.js @@ -28,3 +28,15 @@ export const countsMonthlyChartData2 = [ ['2020-07-01', 9.5], // average of 2020-07-x items ['2020-06-01', 20.666666666666668], // average of 2020-06-x items ]; + +export const roundedSortedCountsMonthlyChartData2 = [ + ['2020-06-01', 21], // average of 2020-06-x items + ['2020-07-01', 10], // average of 2020-07-x items +]; + +export const mockPageInfo = { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, +}; diff --git a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb index d779a2227c1..2e5d41a8f1e 100644 --- a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb +++ b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb @@ -50,8 +50,8 @@ RSpec.describe Mutations::Discussions::ToggleResolve do it 'raises an error' do expect { subject }.to raise_error( - Gitlab::Graphql::Errors::ArgumentError, - "#{discussion.to_global_id} is not a valid ID for Discussion." + GraphQL::CoercionError, + "\"#{discussion.to_global_id}\" does not represent an instance of Discussion" ) end end diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb index 375f3351ea8..631325402d9 100644 --- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do end it 'measures execution time' do - expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: '200', method: 'get' }, a_positive_execution_time) + expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ method: 'get' }, a_positive_execution_time) Timecop.scale(3600) { subject.call(env) } end @@ -77,7 +77,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do it 'records the request duration' do expect(described_class) .to receive_message_chain(:http_request_duration_seconds, :observe) - .with({ method: 'get', status: '200' }, a_positive_execution_time) + .with({ method: 'get' }, a_positive_execution_time) subject.call(env) end @@ -137,10 +137,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do describe '.initialize_http_request_duration_seconds' do it "sets labels" do expected_labels = [] - described_class::HTTP_METHODS.each do |method, statuses| - statuses.each do |status| - expected_labels << { method: method, status: status.to_s } - end + described_class::HTTP_METHODS.each do |method| + expected_labels << { method: method } end described_class.initialize_http_request_duration_seconds diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 93eef8a2732..16433d49ca1 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -3,11 +3,14 @@ require 'spec_helper.rb' RSpec.describe Issues::BuildService do - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:developer) { create(:user) } + let_it_be(:guest) { create(:user) } + let(:user) { developer } - before do - project.add_developer(user) + before_all do + project.add_developer(developer) + project.add_guest(guest) end def build_issue(issue_params = {}) @@ -134,31 +137,56 @@ RSpec.describe Issues::BuildService do end describe '#execute' do - it 'builds a new issues with given params' do - milestone = create(:milestone, project: project) - issue = build_issue(milestone_id: milestone.id) + context 'as developer' do + it 'builds a new issues with given params' do + milestone = create(:milestone, project: project) + issue = build_issue(milestone_id: milestone.id) - expect(issue.milestone).to eq(milestone) - end - - it 'sets milestone to nil if it is not available for the project' do - milestone = create(:milestone, project: create(:project)) - issue = build_issue(milestone_id: milestone.id) - - expect(issue.milestone).to be_nil - end - - context 'setting issue type' do - it 'sets the issue_type on the issue' do - issue = build_issue(issue_type: 'incident') - - expect(issue.issue_type).to eq('incident') + expect(issue.milestone).to eq(milestone) end - it 'defaults to issue if issue_type not given' do - issue = build_issue + it 'sets milestone to nil if it is not available for the project' do + milestone = create(:milestone, project: create(:project)) + issue = build_issue(milestone_id: milestone.id) - expect(issue.issue_type).to eq('issue') + expect(issue.milestone).to be_nil + end + end + + context 'as guest' do + let(:user) { guest } + + it 'cannot set milestone' do + milestone = create(:milestone, project: project) + issue = build_issue(milestone_id: milestone.id) + + expect(issue.milestone).to be_nil + end + + context 'setting issue type' do + it 'defaults to issue if issue_type not given' do + issue = build_issue + + expect(issue).to be_issue + end + + it 'sets issue' do + issue = build_issue(issue_type: 'issue') + + expect(issue).to be_issue + end + + it 'sets incident' do + issue = build_issue(issue_type: 'incident') + + expect(issue).to be_incident + end + + it 'cannot set invalid type' do + expect do + build_issue(issue_type: 'invalid type') + end.to raise_error(ArgumentError, "'invalid type' is not a valid issue_type") + end end end end