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 00000000000..547c565c6f9
Binary files /dev/null and b/doc/operations/metrics/dashboards/img/prometheus_dashboard_gauge_panel_type_v13_3.png differ
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+ |
+
-## 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"