Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
37419c44f0
commit
0790cf032c
|
@ -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>
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add telemetry for projects inheriting instance settings
|
||||
merge_request: 38561
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add personal_access_tokens list to REST API
|
||||
merge_request: 37806
|
||||
author:
|
||||
type: added
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Make MergeService idempotent
|
||||
merge_request: 32456
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add gauge chart type to the monitoring dashboards
|
||||
merge_request: 36674
|
||||
author:
|
||||
type: added
|
|
@ -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) {
|
||||
|
|
|
@ -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) |
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
```
|
|
@ -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"
|
||||
],
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
@ -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).
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -32,8 +32,6 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
alias_method :octokit, :api
|
||||
|
||||
def client
|
||||
unless config
|
||||
raise Projects::ImportService::Error,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16616,9 +16616,6 @@ msgstr ""
|
|||
msgid "Number of files touched"
|
||||
msgstr ""
|
||||
|
||||
msgid "OAuth configuration for GitHub missing."
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from './commit';
|
||||
export * from './commit_id';
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export default server => {
|
||||
['get', 'post', 'put', 'delete', 'patch'].forEach(method => {
|
||||
server[method]('*', () => {
|
||||
return new Response(404);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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 [];
|
||||
});
|
||||
};
|
|
@ -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)),
|
||||
);
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import { createMockServer } from './index';
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window.mockServer = createMockServer();
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import '../../../frontend/test_setup';
|
||||
import './setup_globals';
|
||||
import './setup_axios';
|
||||
import './setup_serializers';
|
||||
import './setup_mock_server';
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
|
@ -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;
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import defaultSerializer from '../snapshot_serializer';
|
||||
|
||||
expect.addSnapshotSerializer(defaultSerializer);
|
|
@ -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');
|
||||
},
|
||||
};
|
|
@ -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]);
|
|
@ -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'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
import './test_helpers/setup';
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
151
yarn.lock
|
@ -1044,6 +1044,11 @@
|
|||
"@types/yargs" "^15.0.0"
|
||||
chalk "^3.0.0"
|
||||
|
||||
"@miragejs/pretender-node-polyfill@^0.1.0":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@miragejs/pretender-node-polyfill/-/pretender-node-polyfill-0.1.2.tgz#d26b6b7483fb70cd62189d05c95d2f67153e43f2"
|
||||
integrity sha512-M/BexG/p05C5lFfMunxo/QcgIJnMT2vDVCd00wNqK2ImZONIlEETZwWJu1QtLxtmYlSHlCFl3JNzp0tLe7OJ5g==
|
||||
|
||||
"@mrmlnc/readdir-enhanced@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
|
||||
|
@ -4903,6 +4908,11 @@ extsprintf@1.3.0, extsprintf@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
|
||||
|
||||
fake-xml-http-request@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.1.1.tgz#279fdac235840d7a4dff77d98ec44bce9fc690a6"
|
||||
integrity sha512-Kn2WYYS6cDBS5jq/voOfSGCA0TafOYAUPbEp8mUVpD/DVV5bQIDjlq+MLLvNUokkbTpjBVlLDaM5PnX+PwZMlw==
|
||||
|
||||
fast-deep-equal@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
|
||||
|
@ -6040,6 +6050,11 @@ infer-owner@^1.0.3, infer-owner@^1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
|
||||
integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
|
||||
|
||||
inflected@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inflected/-/inflected-2.0.4.tgz#323770961ccbe992a98ea930512e9a82d3d3ef77"
|
||||
integrity sha512-HQPzFLTTUvwfeUH6RAGjD8cHS069mBqXG5n4qaxX7sJXBhVQrsGgF+0ZJGkSuN6a8pcUWB/GXStta11kKi/WvA==
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
|
@ -7601,7 +7616,12 @@ locate-path@^5.0.0:
|
|||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
lodash.camelcase@4.3.0:
|
||||
lodash.assign@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
|
||||
integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=
|
||||
|
||||
lodash.camelcase@4.3.0, lodash.camelcase@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
|
||||
|
@ -7611,6 +7631,11 @@ lodash.clonedeep@^4.5.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
|
||||
|
||||
lodash.compact@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.compact/-/lodash.compact-3.0.1.tgz#540ce3837745975807471e16b4a2ba21e7256ca5"
|
||||
integrity sha1-VAzjg3dFl1gHRx4WtKK6IeclbKU=
|
||||
|
||||
lodash.differencewith@~4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.differencewith/-/lodash.differencewith-4.5.0.tgz#bafafbc918b55154e179176a00bb0aefaac854b7"
|
||||
|
@ -7621,16 +7646,56 @@ lodash.escaperegexp@^4.1.2:
|
|||
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
|
||||
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
|
||||
|
||||
lodash.flatten@~4.4.0:
|
||||
lodash.find@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
|
||||
integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=
|
||||
|
||||
lodash.flatten@^4.4.0, lodash.flatten@~4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
|
||||
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
|
||||
|
||||
lodash.forin@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.forin/-/lodash.forin-4.4.0.tgz#5d3f20ae564011fbe88381f7d98949c9c9519731"
|
||||
integrity sha1-XT8grlZAEfvog4H32YlJyclRlzE=
|
||||
|
||||
lodash.get@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
|
||||
|
||||
lodash.has@^4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
|
||||
integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=
|
||||
|
||||
lodash.invokemap@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz#1748cda5d8b0ef8369c4eb3ec54c21feba1f2d62"
|
||||
integrity sha1-F0jNpdiw74NpxOs+xUwh/rofLWI=
|
||||
|
||||
lodash.isempty@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
|
||||
integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
|
||||
|
||||
lodash.isfunction@^3.0.9:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
|
||||
integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==
|
||||
|
||||
lodash.isinteger@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
|
||||
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
|
||||
|
||||
lodash.isplainobject@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
|
@ -7646,12 +7711,32 @@ lodash.kebabcase@4.1.1:
|
|||
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
|
||||
integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY=
|
||||
|
||||
lodash.lowerfirst@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.lowerfirst/-/lodash.lowerfirst-4.3.1.tgz#de3c7b12e02c6524a0059c2f6cb7c5c52655a13d"
|
||||
integrity sha1-3jx7EuAsZSSgBZwvbLfFxSZVoT0=
|
||||
|
||||
lodash.map@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
|
||||
integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=
|
||||
|
||||
lodash.mapvalues@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c"
|
||||
integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=
|
||||
|
||||
lodash.mergewith@^4.6.1:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
|
||||
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
|
||||
|
||||
lodash.snakecase@4.1.1:
|
||||
lodash.pick@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
|
||||
integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
|
||||
|
||||
lodash.snakecase@4.1.1, lodash.snakecase@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d"
|
||||
integrity sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=
|
||||
|
@ -7661,11 +7746,26 @@ lodash.sortby@^4.7.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||
|
||||
lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
lodash.uniqby@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
|
||||
integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=
|
||||
|
||||
lodash.upperfirst@4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce"
|
||||
integrity sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=
|
||||
|
||||
lodash.values@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347"
|
||||
integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=
|
||||
|
||||
lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@~4.17.10:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
|
@ -8199,6 +8299,38 @@ minipass@^3.0.0, minipass@^3.1.1:
|
|||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
miragejs@^0.1.40:
|
||||
version "0.1.40"
|
||||
resolved "https://registry.yarnpkg.com/miragejs/-/miragejs-0.1.40.tgz#5bcba7634312c012748ae7f294e1516b74b37182"
|
||||
integrity sha512-7zxIcynzdS6425KZ2+TWD6F6DqESorulSDW2QBXf4iKyVn/J5vSielcubAK8sTKUefTPCrSRi7PwgNOb0JlmIg==
|
||||
dependencies:
|
||||
"@miragejs/pretender-node-polyfill" "^0.1.0"
|
||||
inflected "^2.0.4"
|
||||
lodash.assign "^4.2.0"
|
||||
lodash.camelcase "^4.3.0"
|
||||
lodash.clonedeep "^4.5.0"
|
||||
lodash.compact "^3.0.1"
|
||||
lodash.find "^4.6.0"
|
||||
lodash.flatten "^4.4.0"
|
||||
lodash.forin "^4.4.0"
|
||||
lodash.get "^4.4.2"
|
||||
lodash.has "^4.5.2"
|
||||
lodash.invokemap "^4.6.0"
|
||||
lodash.isempty "^4.4.0"
|
||||
lodash.isequal "^4.5.0"
|
||||
lodash.isfunction "^3.0.9"
|
||||
lodash.isinteger "^4.0.4"
|
||||
lodash.isplainobject "^4.0.6"
|
||||
lodash.lowerfirst "^4.3.1"
|
||||
lodash.map "^4.6.0"
|
||||
lodash.mapvalues "^4.6.0"
|
||||
lodash.pick "^4.4.0"
|
||||
lodash.snakecase "^4.1.1"
|
||||
lodash.uniq "^4.5.0"
|
||||
lodash.uniqby "^4.7.0"
|
||||
lodash.values "^4.3.0"
|
||||
pretender "^3.4.3"
|
||||
|
||||
mississippi@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
|
||||
|
@ -9377,6 +9509,14 @@ prepend-http@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||
|
||||
pretender@^3.4.3:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/pretender/-/pretender-3.4.3.tgz#a3b4160516007075d29127262f3a0063d19896e9"
|
||||
integrity sha512-AlbkBly9R8KR+R0sTCJ/ToOeEoUMtt52QVCetui5zoSmeLOU3S8oobFsyPLm1O2txR6t58qDNysqPnA1vVi8Hg==
|
||||
dependencies:
|
||||
fake-xml-http-request "^2.1.1"
|
||||
route-recognizer "^0.3.3"
|
||||
|
||||
prettier@1.16.3:
|
||||
version "1.16.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d"
|
||||
|
@ -10235,6 +10375,11 @@ rope-sequence@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.2.tgz#49c4e5c2f54a48e990b050926771e2871bcb31ce"
|
||||
integrity sha1-ScTlwvVKSOmQsFCSZ3HihxvLMc4=
|
||||
|
||||
route-recognizer@^0.3.3:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3"
|
||||
integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==
|
||||
|
||||
rsvp@^4.8.4:
|
||||
version "4.8.4"
|
||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.4.tgz#b50e6b34583f3dd89329a2f23a8a2be072845911"
|
||||
|
|
Loading…
Reference in New Issue