Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-08-06 21:10:15 +00:00
parent 37419c44f0
commit 0790cf032c
87 changed files with 1839 additions and 415 deletions

View File

@ -0,0 +1,123 @@
<script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlGaugeChart } from '@gitlab/ui/dist/charts';
import { graphDataValidatorForValues } from '../../utils';
import { getValidThresholds } from './options';
import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { isFinite, isArray, isInteger } from 'lodash';
export default {
components: {
GlGaugeChart,
},
directives: {
GlResizeObserverDirective,
},
props: {
graphData: {
type: Object,
required: true,
validator: graphDataValidatorForValues.bind(null, true),
},
},
data() {
return {
width: 0,
};
},
computed: {
rangeValues() {
let min = 0;
let max = 100;
const { minValue, maxValue } = this.graphData;
const isValidMinMax = () => {
return isFinite(minValue) && isFinite(maxValue) && minValue < maxValue;
};
if (isValidMinMax()) {
min = minValue;
max = maxValue;
}
return {
min,
max,
};
},
validThresholds() {
const { mode, values } = this.graphData?.thresholds || {};
const range = this.rangeValues;
if (!isArray(values)) {
return [];
}
return getValidThresholds({ mode, range, values });
},
queryResult() {
return this.graphData?.metrics[0]?.result[0]?.value[1];
},
splitValue() {
const { split } = this.graphData;
const defaultValue = 10;
return isInteger(split) && split > 0 ? split : defaultValue;
},
textValue() {
const formatFromPanel = this.graphData.format;
const defaultFormat = SUPPORTED_FORMATS.engineering;
const format = SUPPORTED_FORMATS[formatFromPanel] ?? defaultFormat;
const { queryResult } = this;
const formatter = getFormatter(format);
return isFinite(queryResult) ? formatter(queryResult) : '--';
},
thresholdsValue() {
/**
* If there are no valid thresholds, a default threshold
* will be set at 90% of the gauge arcs' max value
*/
const { min, max } = this.rangeValues;
const defaultThresholdValue = [(max - min) * 0.95];
return this.validThresholds.length ? this.validThresholds : defaultThresholdValue;
},
value() {
/**
* The gauge chart gitlab-ui component expects a value
* of type number.
*
* So, if the query result is undefined,
* we pass the gauge chart a value of NaN.
*/
return this.queryResult || NaN;
},
},
methods: {
onResize() {
if (!this.$refs.gaugeChart) return;
const { width } = this.$refs.gaugeChart.$el.getBoundingClientRect();
this.width = width;
},
},
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize">
<gl-gauge-chart
ref="gaugeChart"
v-bind="$attrs"
:value="value"
:min="rangeValues.min"
:max="rangeValues.max"
:thresholds="thresholdsValue"
:text="textValue"
:split-number="splitValue"
:width="width"
/>
</div>
</template>

View File

@ -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;
};

View File

@ -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() {

View File

@ -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',
};

View File

@ -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),
};

View File

@ -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' }

View File

@ -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

View File

@ -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
{

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.'

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Add telemetry for projects inheriting instance settings
merge_request: 38561
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add personal_access_tokens list to REST API
merge_request: 37806
author:
type: added

View File

@ -1,5 +0,0 @@
---
title: Make MergeService idempotent
merge_request: 32456
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add gauge chart type to the monitoring dashboards
merge_request: 36674
author:
type: added

View File

@ -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) {

View File

@ -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) |

View File

@ -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: <your_access_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: <your_access_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
}
]
```

View File

@ -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"
],

View File

@ -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.

View File

@ -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` | | | | |

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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.

View File

@ -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+ |
<div class="row">
<div class="col-md-9">
<table align="left" style="width:50%">
<tr style="background:#dfdfdf"><th>Package type</th><th>GitLab version</th></tr>
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/composer_repository/index.html">Composer</a></td><td>13.2+</td></tr>
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/conan_repository/index.html">Conan</a></td><td>12.6+</td></tr>
<tr><td><a href="http://docs.gitlab.com/ee/user/packages/go_proxy/index.html">Go</a></td><td>13.1+</td></tr>
<tr><td><a href="http://docs.gitlab.com/ee/user/packages/maven_repository/index.html">Maven</a></td><td>11.3+</td></tr>
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/npm_registry/index.html">NPM</a></td><td>11.7+</td></tr>
<tr><td><a href="http://docs.gitlab.com/ee/user/packages/nuget_repository/index.html">NuGet</a></td><td>12.8+</td></tr>
<tr><td><a href="http://docs.gitlab.com/ee/user/packages/pypi_repository/index.html">PyPI</a></td><td>12.10+</td></tr>
</table>
</div>
</div>
## 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

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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).

View File

@ -40,6 +40,8 @@ module.exports = path => {
'emojis(/.*).json': '<rootDir>/fixtures/emojis$1.json',
'^spec/test_constants$': '<rootDir>/spec/frontend/helpers/test_constants',
'^jest/(.*)$': '<rootDir>/spec/frontend/$1',
'test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
'test_fixtures(/.*)$': '<rootDir>/tmp/tests/frontend/fixtures$1',
};
const collectCoverageFrom = ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'];
@ -51,6 +53,7 @@ module.exports = path => {
'^ee_component(/.*)$': rootDirEE,
'^ee_else_ce(/.*)$': rootDirEE,
'^ee_jest/(.*)$': '<rootDir>/ee/spec/frontend/$1',
'test_fixtures(/.*)$': '<rootDir>/tmp/tests/frontend/fixtures-ee$1',
});
collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}'));
@ -75,7 +78,7 @@ module.exports = path => {
cacheDirectory: '<rootDir>/tmp/cache/jest',
modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
reporters,
setupFilesAfterEnv: ['<rootDir>/spec/frontend/test_setup.js', 'jest-canvas-mock'],
setupFilesAfterEnv: [`<rootDir>/${path}/test_setup.js`, 'jest-canvas-mock'],
restoreMocks: true,
transform: {
'^.+\\.(gql|graphql)$': 'jest-transform-graphql',

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -32,8 +32,6 @@ module Gitlab
)
end
alias_method :octokit, :api
def client
unless config
raise Projects::ImportService::Error,

View File

@ -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

View File

@ -16616,9 +16616,6 @@ msgstr ""
msgid "Number of files touched"
msgstr ""
msgid "OAuth configuration for GitHub missing."
msgstr ""
msgid "OK"
msgstr ""

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
});
});
});

View File

@ -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([]);
});
});
});

View File

@ -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(),
},
],
});
};

View File

@ -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();
});
});

View File

@ -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],
});
};

View File

@ -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,
};
};

View File

@ -0,0 +1,2 @@
export * from './commit';
export * from './commit_id';

View File

@ -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);

View File

@ -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);

View File

@ -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;
};

View File

@ -0,0 +1,7 @@
export default server => {
['get', 'post', 'put', 'delete', 'patch'].forEach(method => {
server[method]('*', () => {
return new Response(404);
});
});
};

View File

@ -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 [];
});
};

View File

@ -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)),
);
});
};

View File

@ -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);
});
};

View File

@ -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;
});
};

View File

@ -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;
});
};

View File

@ -0,0 +1,5 @@
import { createMockServer } from './index';
if (process.env.NODE_ENV === 'development') {
window.mockServer = createMockServer();
}

View File

@ -0,0 +1,5 @@
import '../../../frontend/test_setup';
import './setup_globals';
import './setup_axios';
import './setup_serializers';
import './setup_mock_server';

View File

@ -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;

View File

@ -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();
});

View File

@ -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;
});

View File

@ -0,0 +1,3 @@
import defaultSerializer from '../snapshot_serializer';
expect.addSnapshotSerializer(defaultSerializer);

View File

@ -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');
},
};

View File

@ -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]);

View File

@ -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'`,
);
});
});
});

View File

@ -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;
});
};

View File

@ -0,0 +1 @@
import './test_helpers/setup';

View File

@ -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

View File

@ -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)

View File

@ -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) }

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

151
yarn.lock
View File

@ -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"