From 0790cf032c70b3df250e1953a3a11b71d835c5a1 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 6 Aug 2020 21:10:15 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../monitoring/components/charts/gauge.vue | 123 +++++++++ .../monitoring/components/charts/options.js | 64 +++++ .../monitoring/components/dashboard_panel.vue | 7 +- .../javascripts/monitoring/constants.js | 9 + .../javascripts/monitoring/stores/utils.js | 8 + app/controllers/import/gitea_controller.rb | 10 - app/controllers/import/github_controller.rb | 66 +---- ...daily_build_group_report_results_finder.rb | 16 +- app/finders/personal_access_tokens_finder.rb | 13 +- app/policies/group_policy.rb | 1 + app/policies/user_policy.rb | 1 + app/services/import/github_service.rb | 2 +- app/services/merge_requests/merge_service.rb | 2 +- .../merge_requests/post_merge_service.rb | 25 +- app/services/merge_requests/squash_service.rb | 10 +- app/views/admin/runners/show.html.haml | 2 +- app/workers/all_queues.yml | 2 +- app/workers/merge_worker.rb | 1 - ...-projects-inheriting-instance-settings.yml | 5 + .../unreleased/227264-list-pats-rest-api.yml | 5 + .../32456-make-mergeservice-idempotent.yml | 5 - .../astoicescu-gaugeChartInDashboard.yml | 5 + config/webpack.config.js | 9 + doc/api/api_resources.md | 1 + doc/api/personal_access_tokens.md | 62 +++++ doc/api/users.md | 4 + doc/development/documentation/styleguide.md | 2 + doc/development/telemetry/event_dictionary.md | 41 ++- ...theus_dashboard_gauge_panel_type_v13_3.png | Bin 0 -> 17303 bytes .../metrics/dashboards/panel_types.md | 51 ++++ doc/user/packages/index.md | 119 ++------- .../img/package_activity_v12_10.png | Bin doc/user/packages/package_registry/index.md | 94 +++++++ jest.config.base.js | 5 +- lib/api/api.rb | 1 + lib/api/entities/personal_access_token.rb | 2 +- lib/api/import_github.rb | 6 +- lib/gitlab/ci/reports/test_suite.rb | 2 +- lib/gitlab/legacy_github_import/client.rb | 2 - lib/gitlab/usage_data.rb | 1 + locale/gitlab.pot | 3 - package.json | 1 + .../import/github_controller_spec.rb | 65 +---- spec/factories/usage_data.rb | 3 +- ..._build_group_report_results_finder_spec.rb | 2 + .../personal_access_tokens_finder_spec.rb | 42 ++- spec/frontend/environment.js | 2 +- spec/frontend/fixtures/api_merge_requests.rb | 24 ++ spec/frontend/fixtures/api_projects.rb | 35 +++ spec/frontend/fixtures/projects_json.rb | 47 ++++ .../components/charts/gauge_spec.js | 215 +++++++++++++++ .../components/charts/options_spec.js | 244 +++++++++++++++++- spec/frontend/monitoring/graph_data.js | 36 +++ .../ide/ide_integration_spec.js | 86 ++---- .../test_helpers/factories/commit.js | 15 ++ .../test_helpers/factories/commit_id.js | 21 ++ .../test_helpers/factories/index.js | 2 + .../test_helpers/fixtures.js | 10 + .../test_helpers/mock_server/graphql.js | 21 ++ .../test_helpers/mock_server/index.js | 45 ++++ .../test_helpers/mock_server/routes/404.js | 7 + .../test_helpers/mock_server/routes/ci.js | 11 + .../mock_server/routes/graphql.js | 11 + .../test_helpers/mock_server/routes/index.js | 12 + .../mock_server/routes/projects.js | 23 ++ .../mock_server/routes/repository.js | 38 +++ .../test_helpers/mock_server/use.js | 5 + .../test_helpers/setup/index.js | 5 + .../test_helpers/setup/setup_axios.js | 5 + .../test_helpers/setup/setup_globals.js | 15 ++ .../test_helpers/setup/setup_mock_server.js | 13 + .../test_helpers/setup/setup_serializers.js | 3 + .../test_helpers/snapshot_serializer.js | 18 ++ .../test_helpers/utils/obj.js | 36 +++ .../test_helpers/utils/obj_spec.js | 23 ++ .../test_helpers/utils/overclock_timers.js | 65 +++++ spec/frontend_integration/test_setup.js | 1 + spec/lib/gitlab/ci/reports/test_suite_spec.rb | 4 +- spec/lib/gitlab/usage_data_spec.rb | 3 +- spec/policies/user_policy_spec.rb | 28 ++ spec/requests/api/import_github_spec.rb | 2 +- spec/services/import/github_service_spec.rb | 6 +- .../merge_requests/merge_service_spec.rb | 38 --- .../merge_requests/post_merge_service_spec.rb | 1 + ...ubish_import_controller_shared_examples.rb | 9 +- spec/workers/merge_worker_spec.rb | 18 -- yarn.lock | 151 ++++++++++- 87 files changed, 1839 insertions(+), 415 deletions(-) create mode 100644 app/assets/javascripts/monitoring/components/charts/gauge.vue create mode 100644 changelogs/unreleased/204802-telemetry-projects-inheriting-instance-settings.yml create mode 100644 changelogs/unreleased/227264-list-pats-rest-api.yml delete mode 100644 changelogs/unreleased/32456-make-mergeservice-idempotent.yml create mode 100644 changelogs/unreleased/astoicescu-gaugeChartInDashboard.yml create mode 100644 doc/api/personal_access_tokens.md create mode 100644 doc/operations/metrics/dashboards/img/prometheus_dashboard_gauge_panel_type_v13_3.png rename doc/user/packages/{ => package_registry}/img/package_activity_v12_10.png (100%) create mode 100644 doc/user/packages/package_registry/index.md create mode 100644 spec/frontend/fixtures/api_merge_requests.rb create mode 100644 spec/frontend/fixtures/api_projects.rb create mode 100644 spec/frontend/fixtures/projects_json.rb create mode 100644 spec/frontend/monitoring/components/charts/gauge_spec.js create mode 100644 spec/frontend_integration/test_helpers/factories/commit.js create mode 100644 spec/frontend_integration/test_helpers/factories/commit_id.js create mode 100644 spec/frontend_integration/test_helpers/factories/index.js create mode 100644 spec/frontend_integration/test_helpers/fixtures.js create mode 100644 spec/frontend_integration/test_helpers/mock_server/graphql.js create mode 100644 spec/frontend_integration/test_helpers/mock_server/index.js create mode 100644 spec/frontend_integration/test_helpers/mock_server/routes/404.js create mode 100644 spec/frontend_integration/test_helpers/mock_server/routes/ci.js create mode 100644 spec/frontend_integration/test_helpers/mock_server/routes/graphql.js create mode 100644 spec/frontend_integration/test_helpers/mock_server/routes/index.js create mode 100644 spec/frontend_integration/test_helpers/mock_server/routes/projects.js create mode 100644 spec/frontend_integration/test_helpers/mock_server/routes/repository.js create mode 100644 spec/frontend_integration/test_helpers/mock_server/use.js create mode 100644 spec/frontend_integration/test_helpers/setup/index.js create mode 100644 spec/frontend_integration/test_helpers/setup/setup_axios.js create mode 100644 spec/frontend_integration/test_helpers/setup/setup_globals.js create mode 100644 spec/frontend_integration/test_helpers/setup/setup_mock_server.js create mode 100644 spec/frontend_integration/test_helpers/setup/setup_serializers.js create mode 100644 spec/frontend_integration/test_helpers/snapshot_serializer.js create mode 100644 spec/frontend_integration/test_helpers/utils/obj.js create mode 100644 spec/frontend_integration/test_helpers/utils/obj_spec.js create mode 100644 spec/frontend_integration/test_helpers/utils/overclock_timers.js create mode 100644 spec/frontend_integration/test_setup.js diff --git a/app/assets/javascripts/monitoring/components/charts/gauge.vue b/app/assets/javascripts/monitoring/components/charts/gauge.vue new file mode 100644 index 00000000000..32834a1cb45 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/charts/gauge.vue @@ -0,0 +1,123 @@ + + diff --git a/app/assets/javascripts/monitoring/components/charts/options.js b/app/assets/javascripts/monitoring/components/charts/options.js index 42252dd5897..5cb16ddaf17 100644 --- a/app/assets/javascripts/monitoring/components/charts/options.js +++ b/app/assets/javascripts/monitoring/components/charts/options.js @@ -1,6 +1,8 @@ import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format'; import { __, s__ } from '~/locale'; +import { isFinite, uniq, sortBy, includes } from 'lodash'; import { formatDate, timezones, formats } from '../../format_date'; +import { thresholdModeTypes } from '../../constants'; const yAxisBoundaryGap = [0.1, 0.1]; /** @@ -109,3 +111,65 @@ export const getTooltipFormatter = ({ const formatter = getFormatter(format); return num => formatter(num, precision); }; + +// Thresholds + +/** + * + * Used to find valid thresholds for the gauge chart + * + * An array of thresholds values is + * - duplicate values are removed; + * - filtered for invalid values; + * - sorted in ascending order; + * - only first two values are used. + */ +export const getValidThresholds = ({ mode, range = {}, values = [] } = {}) => { + const supportedModes = [thresholdModeTypes.ABSOLUTE, thresholdModeTypes.PERCENTAGE]; + const { min, max } = range; + + /** + * return early if min and max have invalid values + * or mode has invalid value + */ + if (!isFinite(min) || !isFinite(max) || min >= max || !includes(supportedModes, mode)) { + return []; + } + + const uniqueThresholds = uniq(values); + + const numberThresholds = uniqueThresholds.filter(threshold => isFinite(threshold)); + + const validThresholds = numberThresholds.filter(threshold => { + let isValid; + + if (mode === thresholdModeTypes.PERCENTAGE) { + isValid = threshold > 0 && threshold < 100; + } else if (mode === thresholdModeTypes.ABSOLUTE) { + isValid = threshold > min && threshold < max; + } + + return isValid; + }); + + const transformedThresholds = validThresholds.map(threshold => { + let transformedThreshold; + + if (mode === 'percentage') { + transformedThreshold = (threshold / 100) * (max - min); + } else { + transformedThreshold = threshold; + } + + return transformedThreshold; + }); + + const sortedThresholds = sortBy(transformedThresholds); + + const reducedThresholdsArray = + sortedThresholds.length > 2 + ? [sortedThresholds[0], sortedThresholds[1]] + : [...sortedThresholds]; + + return reducedThresholdsArray; +}; diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue index 610bef37fdb..a75f01c12df 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue @@ -22,6 +22,7 @@ import MonitorEmptyChart from './charts/empty_chart.vue'; import MonitorTimeSeriesChart from './charts/time_series.vue'; import MonitorAnomalyChart from './charts/anomaly.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; +import MonitorGaugeChart from './charts/gauge.vue'; import MonitorHeatmapChart from './charts/heatmap.vue'; import MonitorColumnChart from './charts/column.vue'; import MonitorBarChart from './charts/bar.vue'; @@ -170,6 +171,9 @@ export default { if (this.isPanelType(panelTypes.SINGLE_STAT)) { return MonitorSingleStatChart; } + if (this.isPanelType(panelTypes.GAUGE_CHART)) { + return MonitorGaugeChart; + } if (this.isPanelType(panelTypes.HEATMAP)) { return MonitorHeatmapChart; } @@ -215,7 +219,8 @@ export default { return ( this.isPanelType(panelTypes.AREA_CHART) || this.isPanelType(panelTypes.LINE_CHART) || - this.isPanelType(panelTypes.SINGLE_STAT) + this.isPanelType(panelTypes.SINGLE_STAT) || + this.isPanelType(panelTypes.GAUGE_CHART) ); }, editCustomMetricLink() { diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 2ddee67db8c..7835050d033 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -86,6 +86,10 @@ export const panelTypes = { * Single data point visualization */ SINGLE_STAT: 'single-stat', + /** + * Gauge + */ + GAUGE_CHART: 'gauge-chart', /** * Heatmap */ @@ -272,3 +276,8 @@ export const keyboardShortcutKeys = { DOWNLOAD_CSV: 'd', CHART_COPY: 'c', }; + +export const thresholdModeTypes = { + ABSOLUTE: 'absolute', + PERCENTAGE: 'percentage', +}; diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 29c477c2c41..df7f22e622f 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -176,7 +176,11 @@ export const mapPanelToViewModel = ({ field, metrics = [], links = [], + min_value, max_value, + split, + thresholds, + format, }) => { // Both `x_axis.name` and `x_label` are supported for now // https://gitlab.com/gitlab-org/gitlab/issues/210521 @@ -195,7 +199,11 @@ export const mapPanelToViewModel = ({ yAxis, xAxis, field, + minValue: min_value, maxValue: max_value, + split, + thresholds, + format, links: links.map(mapLinksToViewModel), metrics: mapToMetricsViewModel(metrics), }; diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index 4785a71b8a1..efeff8439e4 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -54,16 +54,6 @@ class Import::GiteaController < Import::GithubController end end - override :client_repos - def client_repos - @client_repos ||= filtered(client.repos) - end - - override :client - def client - @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) - end - override :client_options def client_options { host: provider_url, api_version: 'v1' } diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 998439b2120..ac6b8c06d66 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -10,9 +10,6 @@ class Import::GithubController < Import::BaseController before_action :provider_auth, only: [:status, :realtime_changes, :create] before_action :expire_etag_cache, only: [:status, :create] - OAuthConfigMissingError = Class.new(StandardError) - - rescue_from OAuthConfigMissingError, with: :missing_oauth_config rescue_from Octokit::Unauthorized, with: :provider_unauthorized rescue_from Octokit::TooManyRequests, with: :provider_rate_limit @@ -25,7 +22,7 @@ class Import::GithubController < Import::BaseController end def callback - session[access_token_key] = get_token(params[:code]) + session[access_token_key] = client.get_token(params[:code]) redirect_to status_import_url end @@ -80,7 +77,9 @@ class Import::GithubController < Import::BaseController override :provider_url def provider_url strong_memoize(:provider_url) do - oauth_config&.dig('url').presence || 'https://github.com' + provider = Gitlab::Auth::OAuth::Provider.config_for('github') + + provider&.dig('url').presence || 'https://github.com' end end @@ -105,56 +104,11 @@ class Import::GithubController < Import::BaseController end def client - @client ||= if Feature.enabled?(:remove_legacy_github_client, default_enabled: false) - Gitlab::GithubImport::Client.new(session[access_token_key]) - else - Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) - end + @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) end def client_repos - @client_repos ||= filtered(client.octokit.repos) - end - - def oauth_client - raise OAuthConfigMissingError unless oauth_config - - @oauth_client ||= ::OAuth2::Client.new( - oauth_config.app_id, - oauth_config.app_secret, - oauth_options.merge(ssl: { verify: oauth_config['verify_ssl'] }) - ) - end - - def oauth_config - @oauth_config ||= Gitlab::Auth::OAuth::Provider.config_for('github') - end - - def oauth_options - if oauth_config - oauth_config.dig('args', 'client_options').deep_symbolize_keys - else - OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys - end - end - - def authorize_url(redirect_uri) - if Feature.enabled?(:remove_legacy_github_client, default_enabled: false) - oauth_client.auth_code.authorize_url({ - redirect_uri: redirect_uri, - scope: 'repo, user, user:email' - }) - else - client.authorize_url(callback_import_url) - end - end - - def get_token(code) - if Feature.enabled?(:remove_legacy_github_client, default_enabled: false) - oauth_client.auth_code.get_token(code).token - else - client.get_token(code) - end + @client_repos ||= filtered(client.repos) end def verify_import_enabled @@ -162,7 +116,7 @@ class Import::GithubController < Import::BaseController end def go_to_provider_for_permissions - redirect_to authorize_url(callback_import_url) + redirect_to client.authorize_url(callback_import_url) end def import_enabled? @@ -198,12 +152,6 @@ class Import::GithubController < Import::BaseController alert: _("GitHub API rate limit exceeded. Try again after %{reset_time}") % { reset_time: reset_time } end - def missing_oauth_config - session[access_token_key] = nil - redirect_to new_import_url, - alert: _('OAuth configuration for GitHub missing.') - end - def access_token_key :"#{provider_name}_access_token" end diff --git a/app/finders/ci/daily_build_group_report_results_finder.rb b/app/finders/ci/daily_build_group_report_results_finder.rb index 774f08d1ff2..ec41d9d2c45 100644 --- a/app/finders/ci/daily_build_group_report_results_finder.rb +++ b/app/finders/ci/daily_build_group_report_results_finder.rb @@ -14,17 +14,25 @@ module Ci end def execute - return none unless can?(current_user, :read_build_report_results, project) + return none unless query_allowed? + query + end + + protected + + attr_reader :current_user, :project, :ref_path, :start_date, :end_date, :limit + + def query Ci::DailyBuildGroupReportResult.recent_results( query_params, limit: limit ) end - private - - attr_reader :current_user, :project, :ref_path, :start_date, :end_date, :limit + def query_allowed? + can?(current_user, :read_build_report_results, project) + end def query_params { diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index e3d5f2ae8de..93f8c520b63 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -5,12 +5,14 @@ class PersonalAccessTokensFinder delegate :build, :find, :find_by_id, :find_by_token, to: :execute - def initialize(params = {}) + def initialize(params = {}, current_user = nil) @params = params + @current_user = current_user end def execute tokens = PersonalAccessToken.all + tokens = by_current_user(tokens) tokens = by_user(tokens) tokens = by_impersonation(tokens) tokens = by_state(tokens) @@ -20,6 +22,15 @@ class PersonalAccessTokensFinder private + attr_reader :current_user + + def by_current_user(tokens) + return tokens if current_user.nil? || current_user.admin? + return PersonalAccessToken.none unless Ability.allowed?(current_user, :read_user_personal_access_tokens, params[:user]) + + tokens + end + def by_user(tokens) return tokens unless @params[:user] diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 62f66093875..42545cffc61 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -138,6 +138,7 @@ class GroupPolicy < BasePolicy enable :read_group_labels enable :read_group_milestones enable :read_group_merge_requests + enable :read_group_build_report_results end rule { can?(:read_cross_project) & can?(:read_group) }.policy do diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 43f472b4c1d..6ebafca9885 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -20,6 +20,7 @@ class UserPolicy < BasePolicy enable :destroy_user enable :update_user enable :update_user_status + enable :read_user_personal_access_tokens end rule { default }.enable :read_user_profile diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb index a2923b1e4f9..0cf17568c78 100644 --- a/app/services/import/github_service.rb +++ b/app/services/import/github_service.rb @@ -33,7 +33,7 @@ module Import end def repo - @repo ||= client.repository(params[:repo_id].to_i) + @repo ||= client.repo(params[:repo_id].to_i) end def project_name diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index e631bfc08e1..961a7cb1ef6 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -55,7 +55,7 @@ module MergeRequests error = if @merge_request.should_be_rebased? 'Only fast-forward merge is allowed for your project. Please update your source branch' - elsif !@merge_request.merged? && !@merge_request.mergeable? + elsif !@merge_request.mergeable? 'Merge request is not mergeable' elsif !@merge_request.squash && project.squash_always? 'This project requires squashing commits when merge requests are accepted.' diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index 2d45f2aceb8..fdf8f442297 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -8,31 +8,18 @@ module MergeRequests # class PostMergeService < MergeRequests::BaseService def execute(merge_request) - return if merge_request.merged? - - # These operations need to happen transactionally - ActiveRecord::Base.transaction(requires_new: true) do - merge_request.mark_as_merged - - # These options do not call external services and should be - # quick enough to put in a transaction - create_event(merge_request) - todo_service.merge_merge_request(merge_request, current_user) - end - - notification_service.merge_mr(merge_request, current_user) - create_note(merge_request) + merge_request.mark_as_merged close_issues(merge_request) + todo_service.merge_merge_request(merge_request, current_user) + create_event(merge_request) + create_note(merge_request) + notification_service.merge_mr(merge_request, current_user) + execute_hooks(merge_request, 'merge') invalidate_cache_counts(merge_request, users: merge_request.assignees) merge_request.update_project_counter_caches delete_non_latest_diffs(merge_request) cancel_review_app_jobs!(merge_request) cleanup_environments(merge_request) - - # Anything after this point will be executed at-most-once. Less important activity only - # TODO: make all the work in here a separate sidekiq job so it can go in the transaction - # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/228803 - execute_hooks(merge_request, 'merge') end private diff --git a/app/services/merge_requests/squash_service.rb b/app/services/merge_requests/squash_service.rb index 94ce7098d3a..faa2e921581 100644 --- a/app/services/merge_requests/squash_service.rb +++ b/app/services/merge_requests/squash_service.rb @@ -7,7 +7,9 @@ module MergeRequests def execute # If performing a squash would result in no change, then # immediately return a success message without performing a squash - return success(squash_sha: merge_request.diff_head_sha) if squash_redundant? + if merge_request.commits_count < 2 && message.nil? + return success(squash_sha: merge_request.diff_head_sha) + end return error(s_('MergeRequests|This project does not allow squashing commits when merge requests are accepted.')) if squash_forbidden? @@ -23,12 +25,6 @@ module MergeRequests private - def squash_redundant? - return true if merge_request.merged? - - merge_request.commits_count < 2 && message.nil? - end - def squash! squash_sha = repository.squash(current_user, merge_request, message || merge_request.default_squash_commit_message) diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 0c2b9bab357..cecf3f137ed 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -28,7 +28,7 @@ %p You can't make this a shared Runner. %hr -.append-bottom-20 +.gl-mb-6 = render 'shared/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner), in_gitlab_com_admin_context: Gitlab.com? .row diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 6c532a24ea2..3fbb81c6c81 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1442,7 +1442,7 @@ :urgency: :high :resource_boundary: :unknown :weight: 5 - :idempotent: true + :idempotent: :tags: [] - :name: merge_request_mergeability_check :feature_category: :source_code_management diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index fbebaa925b4..270bd831f96 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -7,7 +7,6 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker urgency :high weight 5 loggable_arguments 2 - idempotent! def perform(merge_request_id, current_user_id, params) params = params.with_indifferent_access diff --git a/changelogs/unreleased/204802-telemetry-projects-inheriting-instance-settings.yml b/changelogs/unreleased/204802-telemetry-projects-inheriting-instance-settings.yml new file mode 100644 index 00000000000..eb1ab99879d --- /dev/null +++ b/changelogs/unreleased/204802-telemetry-projects-inheriting-instance-settings.yml @@ -0,0 +1,5 @@ +--- +title: Add telemetry for projects inheriting instance settings +merge_request: 38561 +author: +type: other diff --git a/changelogs/unreleased/227264-list-pats-rest-api.yml b/changelogs/unreleased/227264-list-pats-rest-api.yml new file mode 100644 index 00000000000..1825cb20151 --- /dev/null +++ b/changelogs/unreleased/227264-list-pats-rest-api.yml @@ -0,0 +1,5 @@ +--- +title: Add personal_access_tokens list to REST API +merge_request: 37806 +author: +type: added diff --git a/changelogs/unreleased/32456-make-mergeservice-idempotent.yml b/changelogs/unreleased/32456-make-mergeservice-idempotent.yml deleted file mode 100644 index 62186122c69..00000000000 --- a/changelogs/unreleased/32456-make-mergeservice-idempotent.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make MergeService idempotent -merge_request: 32456 -author: -type: changed diff --git a/changelogs/unreleased/astoicescu-gaugeChartInDashboard.yml b/changelogs/unreleased/astoicescu-gaugeChartInDashboard.yml new file mode 100644 index 00000000000..ae3e268effb --- /dev/null +++ b/changelogs/unreleased/astoicescu-gaugeChartInDashboard.yml @@ -0,0 +1,5 @@ +--- +title: Add gauge chart type to the monitoring dashboards +merge_request: 36674 +author: +type: added diff --git a/config/webpack.config.js b/config/webpack.config.js index cdfe292ca70..374b8fdbbc3 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -119,6 +119,15 @@ if (IS_EE) { }); } +if (!IS_PRODUCTION) { + const fixtureDir = IS_EE ? 'fixtures-ee' : 'fixtures'; + + Object.assign(alias, { + test_fixtures: path.join(ROOT_PATH, `tmp/tests/frontend/${fixtureDir}`), + test_helpers: path.join(ROOT_PATH, 'spec/frontend_integration/test_helpers'), + }); +} + let dll; if (VENDOR_DLL && !IS_PRODUCTION) { diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index a96913dd0a7..886f2e990f0 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -140,6 +140,7 @@ The following API resources are available outside of project and group contexts | [Namespaces](namespaces.md) | `/namespaces` | | [Notification settings](notification_settings.md) | `/notification_settings` (also available for groups and projects) | | [Pages domains](pages_domains.md) | `/pages/domains` (also available for projects) | +| [Personal access tokens](personal_access_tokens.md) | `/personal_access_tokens` | | [Projects](projects.md) | `/users/:id/projects` (also available for projects) | | [Project repository storage moves](project_repository_storage_moves.md) **(CORE ONLY)** | `/project_repository_storage_moves` | | [Runners](runners.md) | `/runners` (also available for projects) | diff --git a/doc/api/personal_access_tokens.md b/doc/api/personal_access_tokens.md new file mode 100644 index 00000000000..162ba88f727 --- /dev/null +++ b/doc/api/personal_access_tokens.md @@ -0,0 +1,62 @@ +# Personal access tokens API **(ULTIMATE)** + +You can read more about [personal access tokens](../user/profile/personal_access_tokens.md#personal-access-tokens). + +## List personal access tokens + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22726) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3. + +Get a list of personal access tokens. + +```plaintext +GET /personal_access_tokens +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `user_id` | integer/string | no | The ID of the user to filter by | + +NOTE: **Note:** +Administrators can use the `user_id` parameter to filter by a user. Non-administrators cannot filter by any user except themselves. Attempting to do so will result in a `401 Unauthorized` response. + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/personal_access_tokens" +``` + +```json +[ + { + "id": 4, + "name": "Test Token", + "revoked": false, + "created_at": "2020-07-23T14:31:47.729Z", + "scopes": [ + "api" + ], + "active": true, + "user_id": 24, + "expires_at": null + } +] +``` + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/personal_access_tokens?user_id=3" +``` + +```json +[ + { + "id": 4, + "name": "Test Token", + "revoked": false, + "created_at": "2020-07-23T14:31:47.729Z", + "scopes": [ + "api" + ], + "active": true, + "user_id": 3, + "expires_at": null + } +] +``` diff --git a/doc/api/users.md b/doc/api/users.md index a8339c3b61a..76075e8b7be 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -1293,6 +1293,7 @@ Example response: [ { "active" : true, + "user_id" : 2, "scopes" : [ "api" ], @@ -1305,6 +1306,7 @@ Example response: }, { "active" : false, + "user_id" : 2, "scopes" : [ "read_user" ], @@ -1344,6 +1346,7 @@ Example response: ```json { "active" : true, + "user_id" : 2, "scopes" : [ "api" ], @@ -1387,6 +1390,7 @@ Example response: { "id" : 2, "revoked" : false, + "user_id" : 2, "scopes" : [ "api" ], diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index dbb93042827..cdf83d52c79 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -288,6 +288,7 @@ create an issue or an MR to propose a change to the UI text. - merge requests - milestones - reorder issues + - runner, runners, shared runners - **Some features are capitalized**, typically nouns naming GitLab-specific capabilities or tools. For example: - GitLab CI/CD - Repository Mirroring @@ -295,6 +296,7 @@ create an issue or an MR to propose a change to the UI text. - the To-Do List - the Web IDE - Geo + - GitLab Runner (see [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/233529) for details) Document any exceptions in this style guide. If you're not sure, ask a GitLab Technical Writer so that they can help decide and document the result. diff --git a/doc/development/telemetry/event_dictionary.md b/doc/development/telemetry/event_dictionary.md index b9bc38822be..4c39eb8259c 100644 --- a/doc/development/telemetry/event_dictionary.md +++ b/doc/development/telemetry/event_dictionary.md @@ -206,7 +206,7 @@ An event dictionary is a single source of truth that outlines what events and pr | `templates_mattermost_active` | `counts` | `create` | | CE + EE | Total Mattermost templates enabled | | `templates_mattermost_slash_commands_active` | `counts` | `create` | | CE + EE | Total Mattermost Slash Commands templates enabled | | `templates_microsoft_teams_active` | `counts` | `create` | | CE + EE | Total Microsoft Teams templates enabled | -| `templates_mock_ci_active` | `counts` | `create` | | CE + EE | Total Mock Ci templates enabled | +| `templates_mock_ci_active` | `counts` | `create` | | CE + EE | Total Mock CI templates enabled | | `templates_mock_deployment_active` | `counts` | `create` | | CE + EE | Total Mock Deployment templates enabled | | `templates_mock_monitoring_active` | `counts` | `create` | | CE + EE | Total Mock Monitoring templates enabled | | `templates_packagist_active` | `counts` | `create` | | CE + EE | Total Packagist templates enabled | @@ -221,6 +221,45 @@ An event dictionary is a single source of truth that outlines what events and pr | `templates_unify_circuit_active` | `counts` | `create` | | CE + EE | Total Unify Circuit templates enabled | | `templates_webex_teams_active` | `counts` | `create` | | CE + EE | Total Webex Teams templates enabled | | `templates_youtrack_active` | `counts` | `create` | | CE + EE | Total YouTrack templates enabled | +| `projects_inheriting_instance_alerts_active` | `counts` | `create` | | CE + EE | Total Alerts integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_asana_active` | `counts` | `create` | | CE + EE | Total Asana integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_assembla_active` | `counts` | `create` | | CE + EE | Total Assembla integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_bamboo_active` | `counts` | `create` | | CE + EE | Total Bamboo integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_bugzilla_active` | `counts` | `create` | | CE + EE | Total Bugzilla integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_buildkite_active` | `counts` | `create` | | CE + EE | Total Buildkite integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_campfire_active` | `counts` | `create` | | CE + EE | Total Campfire integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_confluence_active` | `counts` | `create` | | CE + EE | Total Confluence integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_custom_issue_tracker_active` | `counts` | `create` | | CE + EE | Total Custom Issue Tracker integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_discord_active` | `counts` | `create` | | CE + EE | Total Discord integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_drone_ci_active` | `counts` | `create` | | CE + EE | Total Drone CI integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_emails_on_push_active` | `counts` | `create` | | CE + EE | Total Emails On Push integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_external_wiki_active` | `counts` | `create` | | CE + EE | Total External Wiki integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_flowdock_active` | `counts` | `create` | | CE + EE | Total Flowdock integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_github_active` | `counts` | `create` | | CE + EE | Total GitHub integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_gitlab_slack_application_active` | `counts` | `create` | | CE + EE | Total GitLab Slack Application integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_hangouts_chat_active` | `counts` | `create` | | CE + EE | Total Hangouts Chat integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_hipchat_active` | `counts` | `create` | | CE + EE | Total HipChat integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_irker_active` | `counts` | `create` | | CE + EE | Total Irker integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_jenkins_active` | `counts` | `create` | | CE + EE | Total Jenkins integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_jira_active` | `counts` | `create` | | CE + EE | Total Jira integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_mattermost_active` | `counts` | `create` | | CE + EE | Total Mattermost integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_mattermost_slash_commands_active` | `counts` | `create` | | CE + EE | Total Mattermost Slash Commands integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_microsoft_teams_active` | `counts` | `create` | | CE + EE | Total Microsoft Teams integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_mock_ci_active` | `counts` | `create` | | CE + EE | Total Mock CI integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_mock_deployment_active` | `counts` | `create` | | CE + EE | Total Mock Deployment integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_mock_monitoring_active` | `counts` | `create` | | CE + EE | Total Mock Monitoring integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_packagist_active` | `counts` | `create` | | CE + EE | Total Packagist integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_pipelines_email_active` | `counts` | `create` | | CE + EE | Total Pipelines Email integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_pivotaltracker_active` | `counts` | `create` | | CE + EE | Total Pivotal Tracker integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_prometheus_active` | `counts` | `create` | | CE + EE | Total Prometheus integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_pushover_active` | `counts` | `create` | | CE + EE | Total Pushover integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_redmine_active` | `counts` | `create` | | CE + EE | Total Redmine integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_slack_active` | `counts` | `create` | | CE + EE | Total Slack integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_slack_slash_commands_active` | `counts` | `create` | | CE + EE | Total Slack Slash Commands integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_teamcity_active` | `counts` | `create` | | CE + EE | Total Teamcity integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_unify_circuit_active` | `counts` | `create` | | CE + EE | Total Unify Circuit integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_webex_teams_active` | `counts` | `create` | | CE + EE | Total Webex Teams integrations enabled inheriting instance-level settings | +| `projects_inheriting_instance_youtrack_active` | `counts` | `create` | | CE + EE | Total YouTrack integrations enabled inheriting instance-level settings | `projects_jira_server_active` | `counts` | | | | | | `projects_jira_cloud_active` | `counts` | | | | | | `projects_jira_dvcs_cloud_active` | `counts` | | | | | diff --git a/doc/operations/metrics/dashboards/img/prometheus_dashboard_gauge_panel_type_v13_3.png b/doc/operations/metrics/dashboards/img/prometheus_dashboard_gauge_panel_type_v13_3.png new file mode 100644 index 0000000000000000000000000000000000000000..547c565c6f9b393e9594c76da4de463df52b319e GIT binary patch literal 17303 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 DT$$kZ literal 0 HcmV?d00001 diff --git a/doc/operations/metrics/dashboards/panel_types.md b/doc/operations/metrics/dashboards/panel_types.md index 216ce139537..1afad9c6181 100644 --- a/doc/operations/metrics/dashboards/panel_types.md +++ b/doc/operations/metrics/dashboards/panel_types.md @@ -227,6 +227,57 @@ panel_groups: For example, if you have a query value of `53.6`, adding `%` as the unit results in a single stat value of `53.6%`, but if the maximum expected value of the query is `120`, the value would be `44.6%`. Adding the `max_value` causes the correct percentage value to display. +## Gauge + +CAUTION: **Warning:** +This panel type is an _alpha_ feature, and is subject to change at any time +without prior notice! + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207044) in GitLab 13.3. + +To add a gauge panel type to a dashboard, look at the following sample dashboard file: + +```yaml +dashboard: 'Dashboard Title' +panel_groups: + - group: 'Group Title' + panels: + - title: "Gauge" + type: "gauge-chart" + min_value: 0 + max_value: 1000 + split: 5 + thresholds: + values: [60, 90] + mode: "percentage" + format: "kilobytes" + metrics: + - id: 10 + query: 'floor(max(prometheus_http_response_size_bytes_bucket)/1000)' + unit: 'kb' +``` + +Note the following properties: + +| Property | Type | Required | Description | +| ------ | ------ | ------ | ------ | +| type | string | yes | Type of panel to be rendered. For gauge panel types, set to `gauge-chart`. | +| min_value | number | no, defaults to `0` | The minimum value of the gauge chart axis. If either of `min_value` or `max_value` are not set, they both get their default values. | +| max_value | number | no, defaults to `100` | The maximum value of the gauge chart axis. If either of `min_value` or `max_value` are not set, they both get their default values. | +| split | number | no, defaults to `10` | The amount of split segments on the gauge chart axis. | +| thresholds | object | no | Thresholds configuration for the gauge chart axis. | +| format | string | no, defaults to `engineering` | Unit format used. See the [full list of units](yaml_number_format.md). | +| query | string | yes | For gauge panel types, you must use an [instant query](https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries). | + +### Thresholds properties + +| Property | Type | Required | Description | +| ------ | ------ | ------ | ------ | +| values | array | no, defaults to 95% of the range between `min_value` and `max_value`| An array of gauge chart axis threshold values. | +| mode | string | no, defaults to `absolute` | The mode in which the thresholds are interpreted in relation to `min_value` and `max_value`. Can be either `percentage` or `absolute`. | + +![gauge chart panel type](img/prometheus_dashboard_gauge_panel_type_v13_3.png) + ## Heatmaps > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30581) in GitLab 12.5. diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index af2f3c572a6..9e037ef651b 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -4,108 +4,37 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers --- -# GitLab Package Registry +# Packages & Registries -> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3. - -With the GitLab Package Registry, you can use GitLab as a private or public repository -for a variety of common package managers. You can build and publish +The GitLab [Package Registry](package_registry/index.md) acts as a private or public registry +for a variety of common package managers. You can publish and share packages, which can be easily consumed as a dependency in downstream projects. -GitLab acts as a repository for the following: +The Package Registry supports the following formats: -| Software repository | Description | Available in GitLab version | -| ------------------- | ----------- | --------------------------- | -| [Container Registry](container_registry/index.md) | The GitLab Container Registry enables every project in GitLab to have its own space to store [Docker](https://www.docker.com/) images. | 8.8+ | -| [Dependency Proxy](dependency_proxy/index.md) **(PREMIUM)** | The GitLab Dependency Proxy sets up a local proxy for frequently used upstream images/packages. | 11.11+ | -| [Conan Repository](conan_repository/index.md) | The GitLab Conan Repository enables every project in GitLab to have its own space to store [Conan](https://conan.io/) packages. | 12.6+ | -| [Maven Repository](maven_repository/index.md) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ | -| [NPM Registry](npm_registry/index.md) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | -| [NuGet Repository](nuget_repository/index.md) | The GitLab NuGet Repository will enable every project in GitLab to have its own space to store [NuGet](https://www.nuget.org/) packages. | 12.8+ | -| [PyPi Repository](pypi_repository/index.md) | The GitLab PyPi Repository will enable every project in GitLab to have its own space to store [PyPi](https://pypi.org/) packages. | 12.10+ | -| [Go Proxy](go_proxy/index.md) | The Go proxy for GitLab enables every project in GitLab to be fetched with the [Go proxy protocol](https://proxy.golang.org/). | 13.1+ | -| [Composer Repository](composer_repository/index.md) | The GitLab Composer Repository will enable every project in GitLab to have its own space to store [Composer](https://getcomposer.org/) packages. | 13.2+ | +
+
+ + + + + + + + + +
Package typeGitLab version
Composer13.2+
Conan12.6+
Go13.1+
Maven11.3+
NPM11.7+
NuGet12.8+
PyPI12.10+
+
+
-## View packages +You can also use the [API](../../api/packages.md) to administer the Package Registry. -You can view packages for your project or group. +The GitLab [Container Registry](container_registry/index.md) is a secure and private registry for container images. +It's built on open source software and completely integrated within GitLab. +Use GitLab CI/CD to create and publish images. Use the GitLab [API](../../api/container_registry.md) to +manage the registry across groups and projects. -1. Go to the project or group. -1. Go to **{package}** **Packages & Registries > Package Registry**. - -You can search, sort, and filter packages on this page. - -For information on how to create and upload a package, view the GitLab documentation for your package type. - -## Use GitLab CI/CD to build packages - -You can use [GitLab CI/CD](./../../ci/README.md) to build packages. -For Maven and NPM packages, and Composer dependencies, you can -authenticate with GitLab by using the `CI_JOB_TOKEN`. - -CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates). - -Learn more about [using CI/CD to build Maven packages](maven_repository/index.md#creating-maven-packages-with-gitlab-cicd) -and [NPM packages](npm_registry/index.md#publishing-a-package-with-cicd). - -If you use CI/CD to build a package, extended activity -information is displayed when you view the package details: - -![Package CI/CD activity](img/package_activity_v12_10.png) - -You can view which pipeline published the package, as well as the commit and -user who triggered it. - -## Download a package - -To download a package: - -1. Go to **{package}** **Packages & Registries > Package Registry**. -1. Click the name of the package you want to download. -1. In the **Activity** section, click the name of the package you want to download. - -## Delete a package - -You cannot edit a package after you publish it in the Package Registry. Instead, you -must delete and recreate it. - -- You cannot delete packages from the group view. You must delete them from the project view instead. - See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/227714) for details. -- You must have suitable [permissions](../permissions.md). - -You can delete packages by using [the API](../../api/packages.md#delete-a-project-package) or the UI. - -To delete a package in the UI: - -1. Go to **{package}** **Packages & Registries > Package Registry**. -1. Find the name of the package you want to delete. -1. Click **Delete**. - -The package is permanently deleted. - -## Disable the Package Registry - -The Package Registry is automatically enabled. - -If you are using a self-managed instance of GitLab, your administrator can remove -the menu item, **{package}** **Packages & Registries**, from the GitLab sidebar. For more information, -see the [administration documentation](../../administration/packages/index.md). - -You can also remove the Package Registry for your project specifically: - -1. In your project, go to **Settings > General**. -1. Expand the **Visibility, project features, permissions** section and disable the - **Packages** feature. -1. Click **Save changes**. - -The **{package}** **Packages & Registries > Package Registry** entry is removed from the sidebar. - -## Package workflows - -Learn how to use the GitLab Package Registry to build your own custom package workflow. - -- [Use a project as a package registry](./workflows/project_registry.md) to publish all of your packages to one project. -- Publish multiple different packages from one [monorepo project](./workflows/monorepo.md). +The [Dependency Proxy](dependency_proxy/index.md) is a local proxy for frequently-used upstream images and packages. ## Suggested contributions diff --git a/doc/user/packages/img/package_activity_v12_10.png b/doc/user/packages/package_registry/img/package_activity_v12_10.png similarity index 100% rename from doc/user/packages/img/package_activity_v12_10.png rename to doc/user/packages/package_registry/img/package_activity_v12_10.png diff --git a/doc/user/packages/package_registry/index.md b/doc/user/packages/package_registry/index.md new file mode 100644 index 00000000000..1636c3057e7 --- /dev/null +++ b/doc/user/packages/package_registry/index.md @@ -0,0 +1,94 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + +# Package Registry + +> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3. + +With the GitLab Package Registry, you can use GitLab as a private or public registry +for a variety of common package managers. You can publish and share +packages, which can be easily consumed as a dependency in downstream projects. + +## View packages + +You can view packages for your project or group. + +1. Go to the project or group. +1. Go to **{package}** **Packages & Registries > Package Registry**. + +You can search, sort, and filter packages on this page. + +For information on how to create and upload a package, view the GitLab documentation for your package type. + +## Use GitLab CI/CD to build packages + +You can use [GitLab CI/CD](../../../ci/README.md) to build packages. +For Maven and NPM packages, and Composer dependencies, you can +authenticate with GitLab by using the `CI_JOB_TOKEN`. + +CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates). + +Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#creating-maven-packages-with-gitlab-cicd) +and [NPM packages](../npm_registry/index.md#publishing-a-package-with-cicd). + +If you use CI/CD to build a package, extended activity +information is displayed when you view the package details: + +![Package CI/CD activity](img/package_activity_v12_10.png) + +You can view which pipeline published the package, as well as the commit and +user who triggered it. + +## Download a package + +To download a package: + +1. Go to **{package}** **Packages & Registries > Package Registry**. +1. Click the name of the package you want to download. +1. In the **Activity** section, click the name of the package you want to download. + +## Delete a package + +You cannot edit a package after you publish it in the Package Registry. Instead, you +must delete and recreate it. + +- You cannot delete packages from the group view. You must delete them from the project view instead. + See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/227714) for details. +- You must have suitable [permissions](../../permissions.md). + +You can delete packages by using [the API](../../../api/packages.md#delete-a-project-package) or the UI. + +To delete a package in the UI: + +1. Go to **{package}** **Packages & Registries > Package Registry**. +1. Find the name of the package you want to delete. +1. Click **Delete**. + +The package is permanently deleted. + +## Disable the Package Registry + +The Package Registry is automatically enabled. + +If you are using a self-managed instance of GitLab, your administrator can remove +the menu item, **{package}** **Packages & Registries**, from the GitLab sidebar. For more information, +see the [administration documentation](../../../administration/packages/index.md). + +You can also remove the Package Registry for your project specifically: + +1. In your project, go to **Settings > General**. +1. Expand the **Visibility, project features, permissions** section and disable the + **Packages** feature. +1. Click **Save changes**. + +The **{package}** **Packages & Registries > Package Registry** entry is removed from the sidebar. + +## Package workflows + +Learn how to use the GitLab Package Registry to build your own custom package workflow. + +- [Use a project as a package registry](../workflows/project_registry.md) to publish all of your packages to one project. +- Publish multiple different packages from one [monorepo project](../workflows/monorepo.md). diff --git a/jest.config.base.js b/jest.config.base.js index 422b6779af4..ea2ebadd578 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -40,6 +40,8 @@ module.exports = path => { 'emojis(/.*).json': '/fixtures/emojis$1.json', '^spec/test_constants$': '/spec/frontend/helpers/test_constants', '^jest/(.*)$': '/spec/frontend/$1', + 'test_helpers(/.*)$': '/spec/frontend_integration/test_helpers$1', + 'test_fixtures(/.*)$': '/tmp/tests/frontend/fixtures$1', }; const collectCoverageFrom = ['/app/assets/javascripts/**/*.{js,vue}']; @@ -51,6 +53,7 @@ module.exports = path => { '^ee_component(/.*)$': rootDirEE, '^ee_else_ce(/.*)$': rootDirEE, '^ee_jest/(.*)$': '/ee/spec/frontend/$1', + 'test_fixtures(/.*)$': '/tmp/tests/frontend/fixtures-ee$1', }); collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}')); @@ -75,7 +78,7 @@ module.exports = path => { cacheDirectory: '/tmp/cache/jest', modulePathIgnorePatterns: ['/.yarn-cache/'], reporters, - setupFilesAfterEnv: ['/spec/frontend/test_setup.js', 'jest-canvas-mock'], + setupFilesAfterEnv: [`/${path}/test_setup.js`, 'jest-canvas-mock'], restoreMocks: true, transform: { '^.+\\.(gql|graphql)$': 'jest-transform-graphql', diff --git a/lib/api/api.rb b/lib/api/api.rb index ef8c2b2cde5..9a64ef6b8c3 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -194,6 +194,7 @@ module API mount ::API::GoProxy mount ::API::Pages mount ::API::PagesDomains + mount ::API::PersonalAccessTokens mount ::API::ProjectClusters mount ::API::ProjectContainerRepositories mount ::API::ProjectEvents diff --git a/lib/api/entities/personal_access_token.rb b/lib/api/entities/personal_access_token.rb index d6fb9af6ab3..3846929c903 100644 --- a/lib/api/entities/personal_access_token.rb +++ b/lib/api/entities/personal_access_token.rb @@ -3,7 +3,7 @@ module API module Entities class PersonalAccessToken < Grape::Entity - expose :id, :name, :revoked, :created_at, :scopes + expose :id, :name, :revoked, :created_at, :scopes, :user_id expose :active?, as: :active expose :expires_at do |personal_access_token| personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index e6b21c47741..1e839816006 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -10,11 +10,7 @@ module API helpers do def client - @client ||= if Feature.enabled?(:remove_legacy_github_client, default_enabled: false) - Gitlab::GithubImport::Client.new(params[:personal_access_token]) - else - Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options) - end + @client ||= Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options) end def access_params diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb index 28b81e7a471..5ee779227ec 100644 --- a/lib/gitlab/ci/reports/test_suite.rb +++ b/lib/gitlab/ci/reports/test_suite.rb @@ -28,7 +28,7 @@ module Gitlab def total_count return 0 if suite_error - test_cases.values.sum(&:count) + [success_count, failed_count, skipped_count, error_count].sum end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/gitlab/legacy_github_import/client.rb b/lib/gitlab/legacy_github_import/client.rb index aefbebe139c..f7eaafeb446 100644 --- a/lib/gitlab/legacy_github_import/client.rb +++ b/lib/gitlab/legacy_github_import/client.rb @@ -32,8 +32,6 @@ module Gitlab ) end - alias_method :octokit, :api - def client unless config raise Projects::ImportService::Error, diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 5c38b37c3c6..c75f893990d 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -358,6 +358,7 @@ module Gitlab response["projects_#{service_name}_active".to_sym] = count(Service.active.where(template: false, instance: false, type: "#{service_name}_service".camelize)) response["templates_#{service_name}_active".to_sym] = count(Service.active.where(template: true, type: "#{service_name}_service".camelize)) response["instances_#{service_name}_active".to_sym] = count(Service.active.where(instance: true, type: "#{service_name}_service".camelize)) + response["projects_inheriting_instance_#{service_name}_active".to_sym] = count(Service.active.where.not(inherit_from_id: nil).where(type: "#{service_name}_service".camelize)) end.merge(jira_usage, jira_import_usage) # rubocop: enable UsageData/LargeTable: end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e287cd9d8cb..4114d77773f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16616,9 +16616,6 @@ msgstr "" msgid "Number of files touched" msgstr "" -msgid "OAuth configuration for GitHub missing." -msgstr "" - msgid "OK" msgstr "" diff --git a/package.json b/package.json index a8e10279796..2d8ba1d1463 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "mermaid": "^8.5.2", "mersenne-twister": "1.1.0", "minimatch": "^3.0.4", + "miragejs": "^0.1.40", "monaco-editor": "^0.20.0", "monaco-editor-webpack-plugin": "^1.9.0", "monaco-yaml": "^2.4.1", diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 0775903cff6..a5a3dc463d3 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -15,7 +15,10 @@ RSpec.describe Import::GithubController do it "redirects to GitHub for an access token if logged in with GitHub" do allow(controller).to receive(:logged_in_with_provider?).and_return(true) expect(controller).to receive(:go_to_provider_for_permissions).and_call_original - allow(controller).to receive(:authorize_url).with(users_import_github_callback_url).and_call_original + allow_any_instance_of(Gitlab::LegacyGithubImport::Client) + .to receive(:authorize_url) + .with(users_import_github_callback_url) + .and_call_original get :new @@ -43,15 +46,13 @@ RSpec.describe Import::GithubController do end describe "GET callback" do - before do - allow(controller).to receive(:get_token).and_return(token) - allow(controller).to receive(:oauth_options).and_return({}) - - stub_omniauth_provider('github') - end - it "updates access token" do token = "asdasd12345" + allow_any_instance_of(Gitlab::LegacyGithubImport::Client) + .to receive(:get_token).and_return(token) + allow_any_instance_of(Gitlab::LegacyGithubImport::Client) + .to receive(:github_options).and_return({}) + stub_omniauth_provider('github') get :callback @@ -66,54 +67,6 @@ RSpec.describe Import::GithubController do describe "GET status" do it_behaves_like 'a GitHub-ish import controller: GET status' - - context 'when using OAuth' do - before do - allow(controller).to receive(:logged_in_with_provider?).and_return(true) - end - - context 'when OAuth config is missing' do - let(:new_import_url) { public_send("new_import_#{provider}_url") } - - before do - allow(controller).to receive(:oauth_config).and_return(nil) - end - - it 'returns missing config error' do - expect(controller).to receive(:go_to_provider_for_permissions).and_call_original - - get :status - - expect(session[:"#{provider}_access_token"]).to be_nil - expect(controller).to redirect_to(new_import_url) - expect(flash[:alert]).to eq('OAuth configuration for GitHub missing.') - end - end - end - - context 'when feature remove_legacy_github_client is disabled' do - before do - stub_feature_flags(remove_legacy_github_client: false) - end - - it 'uses Gitlab::LegacyGitHubImport::Client' do - expect(controller.send(:client)).to be_instance_of(Gitlab::LegacyGithubImport::Client) - - get :status - end - end - - context 'when feature remove_legacy_github_client is enabled' do - before do - stub_feature_flags(remove_legacy_github_client: true) - end - - it 'uses Gitlab::GithubImport::Client' do - expect(controller.send(:client)).to be_instance_of(Gitlab::GithubImport::Client) - - get :status - end - end end describe "POST create" do diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb index 942dc3a237f..2b2275ba93a 100644 --- a/spec/factories/usage_data.rb +++ b/spec/factories/usage_data.rb @@ -24,7 +24,8 @@ FactoryBot.define do create(:service, project: projects[2], type: 'SlackService', active: true) create(:service, project: projects[2], type: 'MattermostService', active: false) create(:service, :template, type: 'MattermostService', active: true) - create(:service, :instance, type: 'MattermostService', active: true) + matermost_instance = create(:service, :instance, type: 'MattermostService', active: true) + create(:service, project: projects[1], type: 'MattermostService', active: true, inherit_from_id: matermost_instance.id) create(:service, project: projects[2], type: 'CustomIssueTrackerService', active: true) create(:project_error_tracking_setting, project: projects[0]) create(:project_error_tracking_setting, project: projects[1], enabled: false) diff --git a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb index bdb0bc9b561..c0434b5f371 100644 --- a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb +++ b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb @@ -59,6 +59,8 @@ RSpec.describe Ci::DailyBuildGroupReportResultsFinder do end end + private + def create_daily_coverage(group_name, coverage, date) create( :ci_daily_build_group_report_result, diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb index 94954f4153b..c8913329839 100644 --- a/spec/finders/personal_access_tokens_finder_spec.rb +++ b/spec/finders/personal_access_tokens_finder_spec.rb @@ -3,13 +3,14 @@ require 'spec_helper' RSpec.describe PersonalAccessTokensFinder do - def finder(options = {}) - described_class.new(options) + def finder(options = {}, current_user = nil) + described_class.new(options, current_user) end describe '#execute' do let(:user) { create(:user) } let(:params) { {} } + let(:current_user) { nil } let!(:active_personal_access_token) { create(:personal_access_token, user: user) } let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) } let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) } @@ -17,7 +18,42 @@ RSpec.describe PersonalAccessTokensFinder do let!(:expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user) } let!(:revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user) } - subject { finder(params).execute } + subject { finder(params, current_user).execute } + + context 'when current_user is defined' do + let(:current_user) { create(:admin) } + let(:params) { { user: user } } + + context 'current_user is allowed to read PATs' do + it do + is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token, + revoked_personal_access_token, expired_personal_access_token, + revoked_impersonation_token, expired_impersonation_token) + end + end + + context 'current_user is not allowed to read PATs' do + let(:current_user) { create(:user) } + + it { is_expected.to be_empty } + end + + context 'when user param is not set' do + let(:params) { {} } + + it do + is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token, + revoked_personal_access_token, expired_personal_access_token, + revoked_impersonation_token, expired_impersonation_token) + end + + context 'when current_user is not an administrator' do + let(:current_user) { create(:user) } + + it { is_expected.to be_empty } + end + end + end describe 'without user' do it do diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js index 674e89dfef8..35ca323f5a9 100644 --- a/spec/frontend/environment.js +++ b/spec/frontend/environment.js @@ -51,7 +51,7 @@ class CustomEnvironment extends JSDOMEnvironment { this.global.fetch = () => {}; // Expose the jsdom (created in super class) to the global so that we can call reconfigure({ url: '' }) to properly set `window.location` - this.global.dom = this.dom; + this.global.jsdom = this.dom; Object.assign(this.global.performance, { mark: () => null, diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb new file mode 100644 index 00000000000..f3280e216ff --- /dev/null +++ b/spec/frontend/fixtures/api_merge_requests.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do + include ApiHelpers + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin, name: 'root') } + let(:namespace) { create(:namespace, name: 'gitlab-test' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') } + + before(:all) do + clean_frontend_fixtures('api/merge_requests') + end + + it 'api/merge_requests/get.json' do + 4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") } + + get api("/projects/#{project.id}/merge_requests", admin) + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb new file mode 100644 index 00000000000..fa77ca1c0cf --- /dev/null +++ b/spec/frontend/fixtures/api_projects.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do + include ApiHelpers + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin, name: 'root') } + let(:namespace) { create(:namespace, name: 'gitlab-test' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') } + let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') } + + before(:all) do + clean_frontend_fixtures('api/projects') + end + + it 'api/projects/get.json' do + get api("/projects/#{project.id}", admin) + + expect(response).to be_successful + end + + it 'api/projects/get_empty.json' do + get api("/projects/#{project_empty.id}", admin) + + expect(response).to be_successful + end + + it 'api/projects/branches/get.json' do + get api("/projects/#{project.id}/repository/branches/#{project.default_branch}", admin) + + expect(response).to be_successful + end +end diff --git a/spec/frontend/fixtures/projects_json.rb b/spec/frontend/fixtures/projects_json.rb new file mode 100644 index 00000000000..c081d4f08dc --- /dev/null +++ b/spec/frontend/fixtures/projects_json.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects JSON endpoints (JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin, name: 'root') } + let(:project) { create(:project, :repository) } + + before(:all) do + clean_frontend_fixtures('projects_json/') + end + + before do + project.add_maintainer(admin) + sign_in(admin) + end + + describe Projects::FindFileController, '(JavaScript fixtures)', type: :controller do + it 'projects_json/files.json' do + get :list, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: project.default_branch + }, + format: 'json' + + expect(response).to be_successful + end + end + + describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do + it 'projects_json/pipelines_empty.json' do + get :pipelines, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: project.commit(project.default_branch).id, + format: 'json' + } + + expect(response).to be_successful + end + end +end diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js new file mode 100644 index 00000000000..850e2ca87db --- /dev/null +++ b/spec/frontend/monitoring/components/charts/gauge_spec.js @@ -0,0 +1,215 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlGaugeChart } from '@gitlab/ui/dist/charts'; +import GaugeChart from '~/monitoring/components/charts/gauge.vue'; +import { gaugeChartGraphData } from '../../graph_data'; + +describe('Gauge Chart component', () => { + const defaultGraphData = gaugeChartGraphData(); + + let wrapper; + + const findGaugeChart = () => wrapper.find(GlGaugeChart); + + const createWrapper = ({ ...graphProps } = {}) => { + wrapper = shallowMount(GaugeChart, { + propsData: { + graphData: { + ...defaultGraphData, + ...graphProps, + }, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('chart component', () => { + it('is rendered when props are passed', () => { + createWrapper(); + + expect(findGaugeChart().exists()).toBe(true); + }); + }); + + describe('min and max', () => { + const MIN_DEFAULT = 0; + const MAX_DEFAULT = 100; + + it('are passed to chart component', () => { + createWrapper(); + + expect(findGaugeChart().props('min')).toBe(100); + expect(findGaugeChart().props('max')).toBe(1000); + }); + + const invalidCases = [undefined, NaN, 'a string']; + + it.each(invalidCases)( + 'if min has invalid value, defaults are used for both min and max', + invalidValue => { + createWrapper({ minValue: invalidValue }); + + expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT); + expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT); + }, + ); + + it.each(invalidCases)( + 'if max has invalid value, defaults are used for both min and max', + invalidValue => { + createWrapper({ minValue: invalidValue }); + + expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT); + expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT); + }, + ); + + it('if min is bigger than max, defaults are used for both min and max', () => { + createWrapper({ minValue: 100, maxValue: 0 }); + + expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT); + expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT); + }); + }); + + describe('thresholds', () => { + it('thresholds are set on chart', () => { + createWrapper(); + + expect(findGaugeChart().props('thresholds')).toEqual([500, 800]); + }); + + it('when no thresholds are defined, a default threshold is defined at 95% of max_value', () => { + createWrapper({ + minValue: 0, + maxValue: 100, + thresholds: {}, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([95]); + }); + + it('when out of min-max bounds thresholds are defined, a default threshold is defined at 95% of the range between min_value and max_value', () => { + createWrapper({ + thresholds: { + values: [-10, 1500], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([855]); + }); + + describe('when mode is absolute', () => { + it('only valid threshold values are used', () => { + createWrapper({ + thresholds: { + mode: 'absolute', + values: [undefined, 10, 110, NaN, 'a string', 400], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([110, 400]); + }); + + it('if all threshold values are invalid, a default threshold is defined at 95% of the range between min_value and max_value', () => { + createWrapper({ + thresholds: { + mode: 'absolute', + values: [NaN, undefined, 'a string', 1500], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([855]); + }); + }); + + describe('when mode is percentage', () => { + it('when values outside of 0-100 bounds are used, a default threshold is defined at 95% of max_value', () => { + createWrapper({ + thresholds: { + mode: 'percentage', + values: [110], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([855]); + }); + + it('if all threshold values are invalid, a default threshold is defined at 95% of max_value', () => { + createWrapper({ + thresholds: { + mode: 'percentage', + values: [NaN, undefined, 'a string', 1500], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([855]); + }); + }); + }); + + describe('split (the number of ticks on the chart arc)', () => { + const SPLIT_DEFAULT = 10; + + it('is passed to chart as prop', () => { + createWrapper(); + + expect(findGaugeChart().props('splitNumber')).toBe(20); + }); + + it('if not explicitly set, passes a default value to chart', () => { + createWrapper({ split: '' }); + + expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT); + }); + + it('if set as a number that is not an integer, passes the default value to chart', () => { + createWrapper({ split: 10.5 }); + + expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT); + }); + + it('if set as a negative number, passes the default value to chart', () => { + createWrapper({ split: -10 }); + + expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT); + }); + }); + + describe('text (the text displayed on the gauge for the current value)', () => { + it('displays the query result value when format is not set', () => { + createWrapper({ format: '' }); + + expect(findGaugeChart().props('text')).toBe('3'); + }); + + it('displays the query result value when format is set to invalid value', () => { + createWrapper({ format: 'invalid' }); + + expect(findGaugeChart().props('text')).toBe('3'); + }); + + it('displays a formatted query result value when format is set', () => { + createWrapper(); + + expect(findGaugeChart().props('text')).toBe('3kB'); + }); + + it('displays a placeholder value when metric is empty', () => { + createWrapper({ metrics: [] }); + + expect(findGaugeChart().props('text')).toBe('--'); + }); + }); + + describe('value', () => { + it('correct value is passed', () => { + createWrapper(); + + expect(findGaugeChart().props('value')).toBe(3); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js index 1c8fdc01e3e..3372d27e4f9 100644 --- a/spec/frontend/monitoring/components/charts/options_spec.js +++ b/spec/frontend/monitoring/components/charts/options_spec.js @@ -1,5 +1,9 @@ import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; -import { getYAxisOptions, getTooltipFormatter } from '~/monitoring/components/charts/options'; +import { + getYAxisOptions, + getTooltipFormatter, + getValidThresholds, +} from '~/monitoring/components/charts/options'; describe('options spec', () => { describe('getYAxisOptions', () => { @@ -82,4 +86,242 @@ describe('options spec', () => { expect(formatter(1)).toBe('1.000B'); }); }); + + describe('getValidThresholds', () => { + const invalidCases = [null, undefined, NaN, 'a string', true, false]; + + let thresholds; + + afterEach(() => { + thresholds = null; + }); + + it('returns same thresholds when passed values within range', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([10, 50]); + }); + + it('filters out thresholds that are out of range', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [-5, 10, 110], + }); + + expect(thresholds).toEqual([10]); + }); + it('filters out duplicate thresholds', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [5, 5, 10, 10], + }); + + expect(thresholds).toEqual([5, 10]); + }); + + it('sorts passed thresholds and applies only the first two in ascending order', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [10, 1, 35, 20, 5], + }); + + expect(thresholds).toEqual([1, 5]); + }); + + it('thresholds equal to min or max are filtered out', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [0, 100], + }); + + expect(thresholds).toEqual([]); + }); + + it.each(invalidCases)('invalid values for thresholds are filtered out', invalidValue => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [10, invalidValue], + }); + + expect(thresholds).toEqual([10]); + }); + + describe('range', () => { + it('when range is not defined, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }); + + it('when min is not defined, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { max: 100 }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }); + + it('when max is not defined, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0 }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }); + + it('when min is larger than max, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 100, max: 0 }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }); + + it.each(invalidCases)( + 'when min has invalid value, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: invalidValue, max: 100 }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }, + ); + + it.each(invalidCases)( + 'when max has invalid value, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: invalidValue }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }, + ); + }); + + describe('values', () => { + it('if values parameter is omitted, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + }); + + expect(thresholds).toEqual([]); + }); + + it('if there are no values passed, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [], + }); + + expect(thresholds).toEqual([]); + }); + + it.each(invalidCases)( + 'if invalid values are passed, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [invalidValue], + }); + + expect(thresholds).toEqual([]); + }, + ); + }); + + describe('mode', () => { + it.each(invalidCases)( + 'if invalid values are passed, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: invalidValue, + range: { min: 0, max: 100 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([]); + }, + ); + + it('if mode is not passed, empty result is returned', () => { + thresholds = getValidThresholds({ + range: { min: 0, max: 100 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([]); + }); + + describe('absolute mode', () => { + it('absolute mode behaves correctly', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([10, 50]); + }); + }); + + describe('percentage mode', () => { + it('percentage mode behaves correctly', () => { + thresholds = getValidThresholds({ + mode: 'percentage', + range: { min: 0, max: 1000 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([100, 500]); + }); + + const outOfPercentBoundsValues = [-1, 0, 100, 101]; + it.each(outOfPercentBoundsValues)( + 'when values out of 0-100 range are passed, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: 'percentage', + range: { min: 0, max: 1000 }, + values: [invalidValue], + }); + + expect(thresholds).toEqual([]); + }, + ); + }); + }); + + it('calling without passing object parameter returns empty array', () => { + thresholds = getValidThresholds(); + + expect(thresholds).toEqual([]); + }); + }); }); diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js index 7e26db52132..f85351e55d7 100644 --- a/spec/frontend/monitoring/graph_data.js +++ b/spec/frontend/monitoring/graph_data.js @@ -210,3 +210,39 @@ export const heatmapGraphData = (panelOptions = {}, dataOptions = {}) => { ...panelOptions, }); }; + +/** + * Generate gauge chart mock graph data according to options + * + * @param {Object} panelOptions - Panel options as in YML. + * + */ +export const gaugeChartGraphData = (panelOptions = {}) => { + const { + minValue = 100, + maxValue = 1000, + split = 20, + thresholds = { + mode: 'absolute', + values: [500, 800], + }, + format = 'kilobytes', + } = panelOptions; + + return mapPanelToViewModel({ + title: 'Gauge Chart Panel', + type: panelTypes.GAUGE_CHART, + min_value: minValue, + max_value: maxValue, + split, + thresholds, + format, + metrics: [ + { + label: `Metric`, + state: metricStates.OK, + result: matrixSingleResult(), + }, + ], + }); +}; diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js index 7e8fb3a32ee..eb5a9f25f80 100644 --- a/spec/frontend_integration/ide/ide_integration_spec.js +++ b/spec/frontend_integration/ide/ide_integration_spec.js @@ -8,93 +8,55 @@ * * See https://gitlab.com/gitlab-org/gitlab/-/issues/208800 for more information. */ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; import { initIde } from '~/ide'; +import extendStore from '~/ide/stores/extend'; +import { TEST_HOST } from 'helpers/test_constants'; +import { useOverclockTimers } from 'test_helpers/utils/overclock_timers'; -jest.mock('~/api', () => { - return { - project: jest.fn().mockImplementation(() => new Promise(() => {})), - }; -}); - -jest.mock('~/ide/services/gql', () => { - return { - query: jest.fn().mockImplementation(() => new Promise(() => {})), - }; -}); +const TEST_DATASET = { + emptyStateSvgPath: '/test/empty_state.svg', + noChangesStateSvgPath: '/test/no_changes_state.svg', + committedStateSvgPath: '/test/committed_state.svg', + pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg', + promotionSvgPath: '/test/promotion.svg', + ciHelpPagePath: '/test/ci_help_page', + webIDEHelpPagePath: '/test/web_ide_help_page', + clientsidePreviewEnabled: 'true', + renderWhitespaceInCode: 'false', + codesandboxBundlerUrl: 'test/codesandbox_bundler', +}; describe('WebIDE', () => { + useOverclockTimers(); + let vm; let root; - let mock; - let initData; - let location; beforeEach(() => { root = document.createElement('div'); - initData = { - emptyStateSvgPath: '/test/empty_state.svg', - noChangesStateSvgPath: '/test/no_changes_state.svg', - committedStateSvgPath: '/test/committed_state.svg', - pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg', - promotionSvgPath: '/test/promotion.svg', - ciHelpPagePath: '/test/ci_help_page', - webIDEHelpPagePath: '/test/web_ide_help_page', - clientsidePreviewEnabled: 'true', - renderWhitespaceInCode: 'false', - codesandboxBundlerUrl: 'test/codesandbox_bundler', - }; + document.body.appendChild(root); - mock = new MockAdapter(axios); - mock.onAny('*').reply(() => new Promise(() => {})); - - location = { pathname: '/-/ide/project/gitlab-test/test', search: '', hash: '' }; - Object.defineProperty(window, 'location', { - get() { - return location; - }, + global.jsdom.reconfigure({ + url: `${TEST_HOST}/-/ide/project/gitlab-test/lorem-ipsum`, }); }); afterEach(() => { vm.$destroy(); vm = null; - - mock.restore(); + root.remove(); }); const createComponent = () => { const el = document.createElement('div'); - Object.assign(el.dataset, initData); + Object.assign(el.dataset, TEST_DATASET); root.appendChild(el); - vm = initIde(el); + vm = initIde(el, { extendStore }); }; - expect.addSnapshotSerializer({ - test(value) { - return value instanceof HTMLElement && !value.$_hit; - }, - print(element, serialize) { - element.$_hit = true; - element.querySelectorAll('[style]').forEach(el => { - el.$_hit = true; - if (el.style.display === 'none') { - el.textContent = '(jest: contents hidden)'; - } - }); - - return serialize(element) - .replace(/^\s*$/gm, '') - .replace(/\n\s*\n/gm, '\n'); - }, - }); - it('runs', () => { createComponent(); - return vm.$nextTick().then(() => { - expect(root).toMatchSnapshot(); - }); + expect(root).toMatchSnapshot(); }); }); diff --git a/spec/frontend_integration/test_helpers/factories/commit.js b/spec/frontend_integration/test_helpers/factories/commit.js new file mode 100644 index 00000000000..1ee82e74ffe --- /dev/null +++ b/spec/frontend_integration/test_helpers/factories/commit.js @@ -0,0 +1,15 @@ +import { withValues } from '../utils/obj'; +import { getCommit } from '../fixtures'; +import { createCommitId } from './commit_id'; + +// eslint-disable-next-line import/prefer-default-export +export const createNewCommit = ({ id = createCommitId(), message }, orig = getCommit()) => { + return withValues(orig, { + id, + short_id: id.substr(0, 8), + message, + title: message, + web_url: orig.web_url.replace(orig.id, id), + parent_ids: [orig.id], + }); +}; diff --git a/spec/frontend_integration/test_helpers/factories/commit_id.js b/spec/frontend_integration/test_helpers/factories/commit_id.js new file mode 100644 index 00000000000..9fa278c9dde --- /dev/null +++ b/spec/frontend_integration/test_helpers/factories/commit_id.js @@ -0,0 +1,21 @@ +const COMMIT_ID_LENGTH = 40; +const DEFAULT_COMMIT_ID = Array(COMMIT_ID_LENGTH) + .fill('0') + .join(''); + +export const createCommitId = (index = 0) => + `${index}${DEFAULT_COMMIT_ID}`.substr(0, COMMIT_ID_LENGTH); + +export const createCommitIdGenerator = () => { + let prevCommitId = 0; + + const next = () => { + prevCommitId += 1; + + return createCommitId(prevCommitId); + }; + + return { + next, + }; +}; diff --git a/spec/frontend_integration/test_helpers/factories/index.js b/spec/frontend_integration/test_helpers/factories/index.js new file mode 100644 index 00000000000..0f28830b236 --- /dev/null +++ b/spec/frontend_integration/test_helpers/factories/index.js @@ -0,0 +1,2 @@ +export * from './commit'; +export * from './commit_id'; diff --git a/spec/frontend_integration/test_helpers/fixtures.js b/spec/frontend_integration/test_helpers/fixtures.js new file mode 100644 index 00000000000..5f9c0e8dcba --- /dev/null +++ b/spec/frontend_integration/test_helpers/fixtures.js @@ -0,0 +1,10 @@ +/* eslint-disable global-require */ +import { memoize } from 'lodash'; + +export const getProject = () => require('test_fixtures/api/projects/get.json'); +export const getBranch = () => require('test_fixtures/api/projects/branches/get.json'); +export const getMergeRequests = () => require('test_fixtures/api/merge_requests/get.json'); +export const getRepositoryFiles = () => require('test_fixtures/projects_json/files.json'); +export const getPipelinesEmptyResponse = () => + require('test_fixtures/projects_json/pipelines_empty.json'); +export const getCommit = memoize(() => getBranch().commit); diff --git a/spec/frontend_integration/test_helpers/mock_server/graphql.js b/spec/frontend_integration/test_helpers/mock_server/graphql.js new file mode 100644 index 00000000000..6dcc4798378 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/graphql.js @@ -0,0 +1,21 @@ +import { buildSchema, graphql } from 'graphql'; +import gitlabSchemaStr from '../../../../doc/api/graphql/reference/gitlab_schema.graphql'; + +const graphqlSchema = buildSchema(gitlabSchemaStr.loc.source.body); +const graphqlResolvers = { + project({ fullPath }, schema) { + const result = schema.projects.findBy({ path_with_namespace: fullPath }); + const userPermission = schema.db.userPermissions[0]; + + return { + ...result.attrs, + userPermissions: { + ...userPermission, + }, + }; + }, +}; + +// eslint-disable-next-line import/prefer-default-export +export const graphqlQuery = (query, variables, schema) => + graphql(graphqlSchema, query, graphqlResolvers, schema, variables); diff --git a/spec/frontend_integration/test_helpers/mock_server/index.js b/spec/frontend_integration/test_helpers/mock_server/index.js new file mode 100644 index 00000000000..b3979d05ea5 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/index.js @@ -0,0 +1,45 @@ +import { Server, Model, RestSerializer } from 'miragejs'; +import { getProject, getBranch, getMergeRequests, getRepositoryFiles } from 'test_helpers/fixtures'; +import setupRoutes from './routes'; + +export const createMockServerOptions = () => ({ + models: { + project: Model, + branch: Model, + mergeRequest: Model, + file: Model, + userPermission: Model, + }, + serializers: { + application: RestSerializer.extend({ + root: false, + }), + }, + seeds(schema) { + schema.db.loadData({ + files: getRepositoryFiles().map(path => ({ path })), + projects: [getProject()], + branches: [getBranch()], + mergeRequests: getMergeRequests(), + userPermissions: [ + { + createMergeRequestIn: true, + readMergeRequest: true, + pushCode: true, + }, + ], + }); + }, + routes() { + this.namespace = ''; + this.urlPrefix = '/'; + + setupRoutes(this); + }, +}); + +export const createMockServer = () => { + const server = new Server(createMockServerOptions()); + + return server; +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/404.js b/spec/frontend_integration/test_helpers/mock_server/routes/404.js new file mode 100644 index 00000000000..9e08016577b --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/404.js @@ -0,0 +1,7 @@ +export default server => { + ['get', 'post', 'put', 'delete', 'patch'].forEach(method => { + server[method]('*', () => { + return new Response(404); + }); + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/ci.js b/spec/frontend_integration/test_helpers/mock_server/routes/ci.js new file mode 100644 index 00000000000..83951f09c56 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/ci.js @@ -0,0 +1,11 @@ +import { getPipelinesEmptyResponse } from 'test_helpers/fixtures'; + +export default server => { + server.get('*/commit/:id/pipelines', () => { + return getPipelinesEmptyResponse(); + }); + + server.get('/api/v4/projects/:id/runners', () => { + return []; + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js b/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js new file mode 100644 index 00000000000..ebb5415ba97 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js @@ -0,0 +1,11 @@ +import { graphqlQuery } from '../graphql'; + +export default server => { + server.post('/api/graphql', (schema, request) => { + const batches = JSON.parse(request.requestBody); + + return Promise.all( + batches.map(({ query, variables }) => graphqlQuery(query, variables, schema)), + ); + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/index.js b/spec/frontend_integration/test_helpers/mock_server/routes/index.js new file mode 100644 index 00000000000..eea196b5158 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/index.js @@ -0,0 +1,12 @@ +/* eslint-disable global-require */ +export default server => { + [ + require('./graphql'), + require('./projects'), + require('./repository'), + require('./ci'), + require('./404'), + ].forEach(({ default: setup }) => { + setup(server); + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/projects.js b/spec/frontend_integration/test_helpers/mock_server/routes/projects.js new file mode 100644 index 00000000000..f4d8ce4b23d --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/projects.js @@ -0,0 +1,23 @@ +import { withKeys } from 'test_helpers/utils/obj'; + +export default server => { + server.get('/api/v4/projects/:id', (schema, request) => { + const { id } = request.params; + + const proj = + schema.projects.findBy({ id }) ?? schema.projects.findBy({ path_with_namespace: id }); + + return proj.attrs; + }); + + server.get('/api/v4/projects/:id/merge_requests', (schema, request) => { + const result = schema.mergeRequests.where( + withKeys(request.queryParams, { + source_project_id: 'project_id', + source_branch: 'source_branch', + }), + ); + + return result.models; + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/repository.js b/spec/frontend_integration/test_helpers/mock_server/routes/repository.js new file mode 100644 index 00000000000..c5e91c9e87e --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/repository.js @@ -0,0 +1,38 @@ +import { createNewCommit, createCommitIdGenerator } from 'test_helpers/factories'; + +export default server => { + const commitIdGenerator = createCommitIdGenerator(); + + server.get('/api/v4/projects/:id/repository/branches', schema => { + return schema.db.branches; + }); + + server.get('/api/v4/projects/:id/repository/branches/:name', (schema, request) => { + const { name } = request.params; + + const branch = schema.branches.findBy({ name }); + + return branch.attrs; + }); + + server.get('*/-/files/:id', schema => { + return schema.db.files.map(({ path }) => path); + }); + + server.post('/api/v4/projects/:id/repository/commits', (schema, request) => { + const { branch: branchName, commit_message: message, actions } = JSON.parse( + request.requestBody, + ); + + const branch = schema.branches.findBy({ name: branchName }); + + const commit = { + ...createNewCommit({ id: commitIdGenerator.next(), message }, branch.attrs.commit), + __actions: actions, + }; + + branch.update({ commit }); + + return commit; + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/use.js b/spec/frontend_integration/test_helpers/mock_server/use.js new file mode 100644 index 00000000000..84597d57584 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/use.js @@ -0,0 +1,5 @@ +import { createMockServer } from './index'; + +if (process.env.NODE_ENV === 'development') { + window.mockServer = createMockServer(); +} diff --git a/spec/frontend_integration/test_helpers/setup/index.js b/spec/frontend_integration/test_helpers/setup/index.js new file mode 100644 index 00000000000..ba1d256e16e --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/index.js @@ -0,0 +1,5 @@ +import '../../../frontend/test_setup'; +import './setup_globals'; +import './setup_axios'; +import './setup_serializers'; +import './setup_mock_server'; diff --git a/spec/frontend_integration/test_helpers/setup/setup_axios.js b/spec/frontend_integration/test_helpers/setup/setup_axios.js new file mode 100644 index 00000000000..f27b04c759e --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/setup_axios.js @@ -0,0 +1,5 @@ +import axios from '~/lib/utils/axios_utils'; +import adapter from 'axios/lib/adapters/xhr'; + +// We're removing our default axios adapter because this is handled by our mock server now +axios.defaults.adapter = adapter; diff --git a/spec/frontend_integration/test_helpers/setup/setup_globals.js b/spec/frontend_integration/test_helpers/setup/setup_globals.js new file mode 100644 index 00000000000..2b0e8f76c3c --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/setup_globals.js @@ -0,0 +1,15 @@ +import { setTestTimeout } from 'helpers/timeout'; + +beforeEach(() => { + window.gon = { + api_version: 'v4', + relative_url_root: '', + }; + + setTestTimeout(5000); + jest.useRealTimers(); +}); + +afterEach(() => { + jest.useFakeTimers(); +}); diff --git a/spec/frontend_integration/test_helpers/setup/setup_mock_server.js b/spec/frontend_integration/test_helpers/setup/setup_mock_server.js new file mode 100644 index 00000000000..343aeebf88e --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/setup_mock_server.js @@ -0,0 +1,13 @@ +import { createMockServer } from '../mock_server'; + +beforeEach(() => { + const server = createMockServer(); + server.logging = false; + + global.mockServer = server; +}); + +afterEach(() => { + global.mockServer.shutdown(); + global.mockServer = null; +}); diff --git a/spec/frontend_integration/test_helpers/setup/setup_serializers.js b/spec/frontend_integration/test_helpers/setup/setup_serializers.js new file mode 100644 index 00000000000..6c1de853129 --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/setup_serializers.js @@ -0,0 +1,3 @@ +import defaultSerializer from '../snapshot_serializer'; + +expect.addSnapshotSerializer(defaultSerializer); diff --git a/spec/frontend_integration/test_helpers/snapshot_serializer.js b/spec/frontend_integration/test_helpers/snapshot_serializer.js new file mode 100644 index 00000000000..8c4f95a9156 --- /dev/null +++ b/spec/frontend_integration/test_helpers/snapshot_serializer.js @@ -0,0 +1,18 @@ +export default { + test(value) { + return value instanceof HTMLElement && !value.$_hit; + }, + print(element, serialize) { + element.$_hit = true; + element.querySelectorAll('[style]').forEach(el => { + el.$_hit = true; + if (el.style.display === 'none') { + el.textContent = '(jest: contents hidden)'; + } + }); + + return serialize(element) + .replace(/^\s*$/gm, '') + .replace(/\n\s*\n/gm, '\n'); + }, +}; diff --git a/spec/frontend_integration/test_helpers/utils/obj.js b/spec/frontend_integration/test_helpers/utils/obj.js new file mode 100644 index 00000000000..6c301798489 --- /dev/null +++ b/spec/frontend_integration/test_helpers/utils/obj.js @@ -0,0 +1,36 @@ +import { has, mapKeys, pick } from 'lodash'; + +/** + * This method is used to type-safely set values on the given object + * + * @template T + * @returns {T} A shallow copy of `obj`, with the values from `values` + * @throws {Error} If `values` contains a key that isn't already on `obj` + * @param {T} source + * @param {Object} values + */ +export const withValues = (source, values) => + Object.entries(values).reduce( + (acc, [key, value]) => { + if (!has(acc, key)) { + throw new Error( + `[mock_server] Cannot write property that does not exist on object '${key}'`, + ); + } + + return { + ...acc, + [key]: value, + }; + }, + { ...source }, + ); + +/** + * This method returns a subset of the given object and maps the key names based on the + * given `keys`. + * + * @param {Object} obj The source object. + * @param {Object} map The object which contains the keys to use and mapped key names. + */ +export const withKeys = (obj, map) => mapKeys(pick(obj, Object.keys(map)), (val, key) => map[key]); diff --git a/spec/frontend_integration/test_helpers/utils/obj_spec.js b/spec/frontend_integration/test_helpers/utils/obj_spec.js new file mode 100644 index 00000000000..0ad7b4a1a4c --- /dev/null +++ b/spec/frontend_integration/test_helpers/utils/obj_spec.js @@ -0,0 +1,23 @@ +import { withKeys, withValues } from './obj'; + +describe('frontend_integration/test_helpers/utils/obj', () => { + describe('withKeys', () => { + it('picks and maps keys', () => { + expect(withKeys({ a: '123', b: 456, c: 'd' }, { b: 'lorem', c: 'ipsum', z: 'zed ' })).toEqual( + { lorem: 456, ipsum: 'd' }, + ); + }); + }); + + describe('withValues', () => { + it('sets values', () => { + expect(withValues({ a: '123', b: 456 }, { b: 789 })).toEqual({ a: '123', b: 789 }); + }); + + it('throws if values has non-existent key', () => { + expect(() => withValues({ a: '123', b: 456 }, { b: 789, bogus: 'throws' })).toThrow( + `[mock_server] Cannot write property that does not exist on object 'bogus'`, + ); + }); + }); +}); diff --git a/spec/frontend_integration/test_helpers/utils/overclock_timers.js b/spec/frontend_integration/test_helpers/utils/overclock_timers.js new file mode 100644 index 00000000000..046c7f8e527 --- /dev/null +++ b/spec/frontend_integration/test_helpers/utils/overclock_timers.js @@ -0,0 +1,65 @@ +/** + * This function replaces the existing `setTimeout` and `setInterval` with wrappers that + * discount the `ms` passed in by `boost`. + * + * For example, if a module has: + * + * ``` + * setTimeout(cb, 100); + * ``` + * + * But a test has: + * + * ``` + * useOverclockTimers(25); + * ``` + * + * Then the module's call to `setTimeout` effectively becomes: + * + * ``` + * setTimeout(cb, 4); + * ``` + * + * It's important to note that the timing for `setTimeout` and order of execution is non-deterministic + * and discounting the `ms` passed could make this very obvious and expose some underlying issues + * with flaky failures. + * + * WARNING: If flaky spec failures show up in a spec that is using this helper, please consider either: + * + * - Refactoring the production code so that it's reactive to state changes, not dependent on timers. + * - Removing the call to this helper from the spec. + * + * @param {Number} boost + */ +// eslint-disable-next-line import/prefer-default-export +export const useOverclockTimers = (boost = 50) => { + if (boost <= 0) { + throw new Error(`[overclock_timers] boost (${boost}) cannot be <= 0`); + } + + let origSetTimeout; + let origSetInterval; + const newSetTimeout = (fn, msParam = 0) => { + const ms = msParam > 0 ? Math.floor(msParam / boost) : msParam; + + return origSetTimeout(fn, ms); + }; + const newSetInterval = (fn, msParam = 0) => { + const ms = msParam > 0 ? Math.floor(msParam / boost) : msParam; + + return origSetInterval(fn, ms); + }; + + beforeEach(() => { + origSetTimeout = global.setTimeout; + origSetInterval = global.setInterval; + + global.setTimeout = newSetTimeout; + global.setInterval = newSetInterval; + }); + + afterEach(() => { + global.setTimeout = origSetTimeout; + global.setInterval = origSetInterval; + }); +}; diff --git a/spec/frontend_integration/test_setup.js b/spec/frontend_integration/test_setup.js new file mode 100644 index 00000000000..8db22c56245 --- /dev/null +++ b/spec/frontend_integration/test_setup.js @@ -0,0 +1 @@ +import './test_helpers/setup'; diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb index c4c4d2c3704..fbe3473f6b0 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -50,9 +50,11 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do before do test_suite.add_test_case(test_case_success) test_suite.add_test_case(test_case_failed) + test_suite.add_test_case(test_case_skipped) + test_suite.add_test_case(test_case_error) end - it { is_expected.to eq(2) } + it { is_expected.to eq(4) } end describe '#total_status' do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 4c145bf2acc..fc7d2caf162 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -344,9 +344,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(count_data[:projects_slack_active]).to eq(2) expect(count_data[:projects_slack_slash_commands_active]).to eq(1) expect(count_data[:projects_custom_issue_tracker_active]).to eq(1) - expect(count_data[:projects_mattermost_active]).to eq(0) + expect(count_data[:projects_mattermost_active]).to eq(1) expect(count_data[:templates_mattermost_active]).to eq(1) expect(count_data[:instances_mattermost_active]).to eq(1) + expect(count_data[:projects_inheriting_instance_mattermost_active]).to eq(1) expect(count_data[:projects_with_repositories_enabled]).to eq(3) expect(count_data[:projects_with_error_tracking_enabled]).to eq(1) expect(count_data[:projects_with_alerts_service_enabled]).to eq(1) diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index 1cc3581ebdd..d7338622c86 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -12,6 +12,34 @@ RSpec.describe UserPolicy do it { is_expected.to be_allowed(:read_user) } end + describe "reading a different user's Personal Access Tokens" do + let(:token) { create(:personal_access_token, user: user) } + + context 'when user is admin' do + let(:current_user) { create(:user, :admin) } + + context 'when admin mode is enabled', :enable_admin_mode do + it { is_expected.to be_allowed(:read_user_personal_access_tokens) } + end + + context 'when admin mode is disabled' do + it { is_expected.not_to be_allowed(:read_user_personal_access_tokens) } + end + end + + context 'when user is not an admin' do + context 'requesting their own personal access tokens' do + subject { described_class.new(current_user, current_user) } + + it { is_expected.to be_allowed(:read_user_personal_access_tokens) } + end + + context "requesting a different user's personal access tokens" do + it { is_expected.not_to be_allowed(:read_user_personal_access_tokens) } + end + end + end + shared_examples 'changing a user' do |ability| context "when a regular user tries to destroy another regular user" do it { is_expected.not_to be_allowed(ability) } diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index bbfb17fe753..f026314f7a8 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -22,7 +22,7 @@ RSpec.describe API::ImportGithub do before do Grape::Endpoint.before_each do |endpoint| - allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repository: provider_repo).as_null_object) + allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repo: provider_repo).as_null_object) end end diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb index 713f8546d99..266ff309662 100644 --- a/spec/services/import/github_service_spec.rb +++ b/spec/services/import/github_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Import::GithubService do let_it_be(:user) { create(:user) } let_it_be(:token) { 'complex-token' } let_it_be(:access_params) { { github_access_token: 'github-complex-token' } } - let_it_be(:client) { Gitlab::GithubImport::Client.new(token) } + let_it_be(:client) { Gitlab::LegacyGithubImport::Client.new(token) } let_it_be(:params) { { repo_id: 123, new_name: 'new_repo', target_namespace: 'root' } } let(:subject) { described_class.new(client, user, params) } @@ -19,7 +19,7 @@ RSpec.describe Import::GithubService do let(:exception) { Octokit::ClientError.new(status: 404, body: 'Not Found') } before do - expect(client).to receive(:repository).and_raise(exception) + expect(client).to receive(:repo).and_raise(exception) end it 'logs the original error' do @@ -46,7 +46,7 @@ RSpec.describe Import::GithubService do it 'raises an exception for unknown error causes' do exception = StandardError.new('Not Implemented') - expect(client).to receive(:repository).and_raise(exception) + expect(client).to receive(:repo).and_raise(exception) expect(Gitlab::Import::Logger).not_to receive(:error) diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 8f3d38bd334..11e341994f7 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -64,23 +64,6 @@ RSpec.describe MergeRequests::MergeService do end end - it 'is idempotent' do - repository = project.repository - commit_count = repository.commit_count - merge_commit = merge_request.merge_commit.id - - # a first invocation of execute is performed on the before block - service.execute(merge_request) - - expect(merge_request.merge_error).to be_falsey - expect(merge_request).to be_valid - expect(merge_request).to be_merged - - expect(repository.commits_by(oids: [merge_commit]).size).to eq(1) - expect(repository.commit_count).to eq(commit_count) - expect(merge_request.in_progress_merge_commit_sha).to be_nil - end - context 'when squashing' do let(:merge_params) do { commit_message: 'Merge commit message', @@ -305,27 +288,6 @@ RSpec.describe MergeRequests::MergeService do .and_call_original service.execute(merge_request) end - - it 'does not fail to be idempotent when there is a Gitaly error' do - # This arose from an issue where Gitaly failed at a certain point - # and MergeService kept running PostMergeService and creating - # additional notifications. This spec makes sure if a Gitaly error - # does happen, MergeService will just quietly keep trying until - # the branch is removed. - # https://gitlab.com/gitlab-org/gitlab/-/issues/213620#note_331782036 - - # This simulates a Gitaly error when trying to delete a branch - expect_any_instance_of(Gitlab::GitalyClient::OperationService) - .to receive(:user_delete_branch).exactly(4).times - .and_raise(GRPC::FailedPrecondition) - - # Only one notification should be sent out: - expect(NotificationRecipients::BuildService) - .to receive(:build_recipients) - .exactly(:once).and_call_original - - 4.times { expect { service.execute(merge_request) }.to raise_error(Gitlab::Git::CommandError) } - end end end end diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index 58e8c250d95..a51a896ca96 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -19,6 +19,7 @@ RSpec.describe MergeRequests::PostMergeService do it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do # Cache the counter before the MR changed state. project.open_merge_requests_count + merge_request.update!(state: 'merged') expect { subject }.to change { project.open_merge_requests_count }.from(1).to(0) end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index 312dcc4b03c..a01fa49d701 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -111,11 +111,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do end it "handles an invalid access token" do - allow_any_instance_of(Gitlab::LegacyGithubImport::Client).to receive(:repos).and_raise(Octokit::Unauthorized) - - allow_next_instance_of(Octokit::Client) do |client| - allow(client).to receive(:repos).and_raise(Octokit::Unauthorized) - end + allow_any_instance_of(Gitlab::LegacyGithubImport::Client) + .to receive(:repos).and_raise(Octokit::Unauthorized) get :status @@ -190,7 +187,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do end before do - stub_client(user: provider_user, repo: provider_repo, repository: provider_repo) + stub_client(user: provider_user, repo: provider_repo) assign_session_token(provider) end diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb index fc050870225..97e8aeb616e 100644 --- a/spec/workers/merge_worker_spec.rb +++ b/spec/workers/merge_worker_spec.rb @@ -29,23 +29,5 @@ RSpec.describe MergeWorker do source_project.repository.expire_branches_cache expect(source_project.repository.branch_names).not_to include('markdown') end - - it_behaves_like 'an idempotent worker' do - let(:job_args) do - [ - merge_request.id, - merge_request.author_id, - commit_message: 'wow such merge', - sha: merge_request.diff_head_sha - ] - end - - it 'the merge request is still shown as merged' do - subject - - merge_request.reload - expect(merge_request).to be_merged - end - end end end diff --git a/yarn.lock b/yarn.lock index 391ee036ef2..65496b9eff1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1044,6 +1044,11 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@miragejs/pretender-node-polyfill@^0.1.0": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@miragejs/pretender-node-polyfill/-/pretender-node-polyfill-0.1.2.tgz#d26b6b7483fb70cd62189d05c95d2f67153e43f2" + integrity sha512-M/BexG/p05C5lFfMunxo/QcgIJnMT2vDVCd00wNqK2ImZONIlEETZwWJu1QtLxtmYlSHlCFl3JNzp0tLe7OJ5g== + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -4903,6 +4908,11 @@ extsprintf@1.3.0, extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= +fake-xml-http-request@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.1.1.tgz#279fdac235840d7a4dff77d98ec44bce9fc690a6" + integrity sha512-Kn2WYYS6cDBS5jq/voOfSGCA0TafOYAUPbEp8mUVpD/DVV5bQIDjlq+MLLvNUokkbTpjBVlLDaM5PnX+PwZMlw== + fast-deep-equal@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" @@ -6040,6 +6050,11 @@ infer-owner@^1.0.3, infer-owner@^1.0.4: resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== +inflected@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inflected/-/inflected-2.0.4.tgz#323770961ccbe992a98ea930512e9a82d3d3ef77" + integrity sha512-HQPzFLTTUvwfeUH6RAGjD8cHS069mBqXG5n4qaxX7sJXBhVQrsGgF+0ZJGkSuN6a8pcUWB/GXStta11kKi/WvA== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -7601,7 +7616,12 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash.camelcase@4.3.0: +lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= + +lodash.camelcase@4.3.0, lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= @@ -7611,6 +7631,11 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.compact@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.compact/-/lodash.compact-3.0.1.tgz#540ce3837745975807471e16b4a2ba21e7256ca5" + integrity sha1-VAzjg3dFl1gHRx4WtKK6IeclbKU= + lodash.differencewith@~4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.differencewith/-/lodash.differencewith-4.5.0.tgz#bafafbc918b55154e179176a00bb0aefaac854b7" @@ -7621,16 +7646,56 @@ lodash.escaperegexp@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= -lodash.flatten@~4.4.0: +lodash.find@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1" + integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E= + +lodash.flatten@^4.4.0, lodash.flatten@~4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= +lodash.forin@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.forin/-/lodash.forin-4.4.0.tgz#5d3f20ae564011fbe88381f7d98949c9c9519731" + integrity sha1-XT8grlZAEfvog4H32YlJyclRlzE= + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.has@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" + integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= + +lodash.invokemap@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz#1748cda5d8b0ef8369c4eb3ec54c21feba1f2d62" + integrity sha1-F0jNpdiw74NpxOs+xUwh/rofLWI= + +lodash.isempty@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4= + lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= +lodash.isfunction@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" + integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -7646,12 +7711,32 @@ lodash.kebabcase@4.1.1: resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY= +lodash.lowerfirst@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/lodash.lowerfirst/-/lodash.lowerfirst-4.3.1.tgz#de3c7b12e02c6524a0059c2f6cb7c5c52655a13d" + integrity sha1-3jx7EuAsZSSgBZwvbLfFxSZVoT0= + +lodash.map@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" + integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= + +lodash.mapvalues@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c" + integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw= + lodash.mergewith@^4.6.1: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== -lodash.snakecase@4.1.1: +lodash.pick@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= + +lodash.snakecase@4.1.1, lodash.snakecase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" integrity sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40= @@ -7661,11 +7746,26 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash.uniqby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" + integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= + lodash.upperfirst@4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" integrity sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984= +lodash.values@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" + integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c= + lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@~4.17.10: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -8199,6 +8299,38 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" +miragejs@^0.1.40: + version "0.1.40" + resolved "https://registry.yarnpkg.com/miragejs/-/miragejs-0.1.40.tgz#5bcba7634312c012748ae7f294e1516b74b37182" + integrity sha512-7zxIcynzdS6425KZ2+TWD6F6DqESorulSDW2QBXf4iKyVn/J5vSielcubAK8sTKUefTPCrSRi7PwgNOb0JlmIg== + dependencies: + "@miragejs/pretender-node-polyfill" "^0.1.0" + inflected "^2.0.4" + lodash.assign "^4.2.0" + lodash.camelcase "^4.3.0" + lodash.clonedeep "^4.5.0" + lodash.compact "^3.0.1" + lodash.find "^4.6.0" + lodash.flatten "^4.4.0" + lodash.forin "^4.4.0" + lodash.get "^4.4.2" + lodash.has "^4.5.2" + lodash.invokemap "^4.6.0" + lodash.isempty "^4.4.0" + lodash.isequal "^4.5.0" + lodash.isfunction "^3.0.9" + lodash.isinteger "^4.0.4" + lodash.isplainobject "^4.0.6" + lodash.lowerfirst "^4.3.1" + lodash.map "^4.6.0" + lodash.mapvalues "^4.6.0" + lodash.pick "^4.4.0" + lodash.snakecase "^4.1.1" + lodash.uniq "^4.5.0" + lodash.uniqby "^4.7.0" + lodash.values "^4.3.0" + pretender "^3.4.3" + mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -9377,6 +9509,14 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +pretender@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/pretender/-/pretender-3.4.3.tgz#a3b4160516007075d29127262f3a0063d19896e9" + integrity sha512-AlbkBly9R8KR+R0sTCJ/ToOeEoUMtt52QVCetui5zoSmeLOU3S8oobFsyPLm1O2txR6t58qDNysqPnA1vVi8Hg== + dependencies: + fake-xml-http-request "^2.1.1" + route-recognizer "^0.3.3" + prettier@1.16.3: version "1.16.3" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d" @@ -10235,6 +10375,11 @@ rope-sequence@^1.2.0: resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.2.tgz#49c4e5c2f54a48e990b050926771e2871bcb31ce" integrity sha1-ScTlwvVKSOmQsFCSZ3HihxvLMc4= +route-recognizer@^0.3.3: + version "0.3.4" + resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3" + integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g== + rsvp@^4.8.4: version "4.8.4" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.4.tgz#b50e6b34583f3dd89329a2f23a8a2be072845911"