Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
06b4bed158
commit
7b1fa4c1a1
|
|
@ -55,7 +55,12 @@ include:
|
|||
GEO_SECONDARY_PROXY: 0
|
||||
RSPEC_TESTS_FILTER_FILE: "${RSPEC_MATCHING_TESTS_PATH}"
|
||||
SUCCESSFULLY_RETRIED_TEST_EXIT_CODE: 137
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets", "detect-tests"]
|
||||
needs:
|
||||
- job: "setup-test-env"
|
||||
- job: "retrieve-tests-metadata"
|
||||
- job: "compile-test-assets"
|
||||
- job: "detect-tests"
|
||||
optional: true
|
||||
script:
|
||||
- !reference [.base-script, script]
|
||||
# We need to exclude background migration because unit tests run with
|
||||
|
|
|
|||
|
|
@ -1550,9 +1550,9 @@
|
|||
.rails:rules:detect-tests:
|
||||
rules:
|
||||
- <<: *if-merge-request-labels-run-all-rspec
|
||||
- <<: *if-default-refs
|
||||
- <<: *if-merge-request
|
||||
changes: *code-backstage-qa-patterns
|
||||
- <<: *if-default-refs
|
||||
- <<: *if-merge-request
|
||||
changes: *workhorse-patterns
|
||||
|
||||
.rails:rules:detect-previous-failed-tests:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
import { getCookie, setCookie } from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
import {
|
||||
allEnvironments,
|
||||
|
|
@ -64,7 +65,7 @@ export default {
|
|||
GlModal,
|
||||
GlSprintf,
|
||||
},
|
||||
mixins: [trackingMixin],
|
||||
mixins: [glFeatureFlagsMixin(), trackingMixin],
|
||||
inject: [
|
||||
'awsLogoSvgPath',
|
||||
'awsTipCommandsLink',
|
||||
|
|
@ -74,6 +75,7 @@ export default {
|
|||
'environmentScopeLink',
|
||||
'isProtectedByDefault',
|
||||
'maskedEnvironmentVariablesLink',
|
||||
'maskableRawRegex',
|
||||
'maskableRegex',
|
||||
'protectedEnvironmentVariablesLink',
|
||||
],
|
||||
|
|
@ -121,7 +123,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
canMask() {
|
||||
const regex = RegExp(this.maskableRegex);
|
||||
const regex = RegExp(this.useRawMaskableRegexp ? this.maskableRawRegex : this.maskableRegex);
|
||||
return regex.test(this.variable.value);
|
||||
},
|
||||
canSubmit() {
|
||||
|
|
@ -134,11 +136,17 @@ export default {
|
|||
displayMaskedError() {
|
||||
return !this.canMask && this.variable.masked;
|
||||
},
|
||||
isUsingRawRegexFlag() {
|
||||
return this.glFeatures.ciRemoveCharacterLimitationRawMaskedVar;
|
||||
},
|
||||
isEditing() {
|
||||
return this.mode === EDIT_VARIABLE_ACTION;
|
||||
},
|
||||
isExpanded() {
|
||||
return !this.variable.raw;
|
||||
return !this.isRaw;
|
||||
},
|
||||
isRaw() {
|
||||
return this.variable.raw;
|
||||
},
|
||||
isTipVisible() {
|
||||
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
|
||||
|
|
@ -174,6 +182,9 @@ export default {
|
|||
|
||||
return true;
|
||||
},
|
||||
useRawMaskableRegexp() {
|
||||
return this.isRaw && this.isUsingRawRegexFlag;
|
||||
},
|
||||
variableValidationFeedback() {
|
||||
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
|
||||
},
|
||||
|
|
@ -315,11 +326,7 @@ export default {
|
|||
class="gl-font-monospace!"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<p
|
||||
v-if="variable.raw"
|
||||
class="gl-mt-2 gl-mb-0 text-secondary"
|
||||
data-testid="raw-variable-tip"
|
||||
>
|
||||
<p v-if="isRaw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
|
||||
{{ __('Variable value will be evaluated as raw string.') }}
|
||||
</p>
|
||||
</gl-form-group>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const mountCiVariableListApp = (containerEl) => {
|
|||
isGroup,
|
||||
isProject,
|
||||
maskedEnvironmentVariablesLink,
|
||||
maskableRawRegex,
|
||||
maskableRegex,
|
||||
projectFullPath,
|
||||
projectId,
|
||||
|
|
@ -63,6 +64,7 @@ const mountCiVariableListApp = (containerEl) => {
|
|||
isProject: parsedIsProject,
|
||||
isProtectedByDefault,
|
||||
maskedEnvironmentVariablesLink,
|
||||
maskableRawRegex,
|
||||
maskableRegex,
|
||||
projectFullPath,
|
||||
projectId,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
<script>
|
||||
import { GlTable, GlLink, GlPagination, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { getParameterValues, setUrlParams } from '~/lib/utils/url_utility';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import {
|
||||
LIST_KEY_CREATED_AT,
|
||||
BASE_SORT_FIELDS,
|
||||
METRIC_KEY_PREFIX,
|
||||
} from '~/ml/experiment_tracking/constants';
|
||||
import { s__ } from '~/locale';
|
||||
import { queryToObject, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import IncubationAlert from './incubation_alert.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -13,18 +21,36 @@ export default {
|
|||
TimeAgo,
|
||||
IncubationAlert,
|
||||
GlPagination,
|
||||
RegistrySearch,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['candidates', 'metricNames', 'paramNames', 'pagination'],
|
||||
data() {
|
||||
const query = queryToObject(window.location.search);
|
||||
|
||||
const filter = query.name ? [{ value: { data: query.name }, type: FILTERED_SEARCH_TERM }] : [];
|
||||
|
||||
let orderBy = query.orderBy || LIST_KEY_CREATED_AT;
|
||||
|
||||
if (query.orderByType === 'metric') {
|
||||
orderBy = `${METRIC_KEY_PREFIX}${orderBy}`;
|
||||
}
|
||||
|
||||
return {
|
||||
page: parseInt(getParameterValues('page')[0], 10) || 1,
|
||||
page: parseInt(query.page, 10) || 1,
|
||||
filters: filter,
|
||||
sorting: {
|
||||
orderBy,
|
||||
sort: (query.sort || 'desc').toLowerCase(),
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
if (this.candidates.length === 0) return [];
|
||||
|
||||
return [
|
||||
{ key: 'name', label: this.$options.i18n.nameLabel },
|
||||
{ key: 'created_at', label: this.$options.i18n.createdAtLabel },
|
||||
|
|
@ -44,21 +70,63 @@ export default {
|
|||
nextPage() {
|
||||
return !this.pagination.isLastPage ? this.pagination.page + 1 : null;
|
||||
},
|
||||
sortableFields() {
|
||||
return [
|
||||
...BASE_SORT_FIELDS,
|
||||
...this.metricNames.map((name) => ({
|
||||
orderBy: `${METRIC_KEY_PREFIX}${name}`,
|
||||
label: capitalizeFirstCharacter(name),
|
||||
})),
|
||||
];
|
||||
},
|
||||
parsedQuery() {
|
||||
const name = this.filters
|
||||
.map((f) => f.value.data)
|
||||
.join(' ')
|
||||
.trim();
|
||||
|
||||
const filterByQuery = name === '' ? {} : { name };
|
||||
|
||||
let orderByType = 'column';
|
||||
let { orderBy } = this.sorting;
|
||||
const { sort } = this.sorting;
|
||||
|
||||
if (orderBy.startsWith(METRIC_KEY_PREFIX)) {
|
||||
orderBy = this.sorting.orderBy.slice(METRIC_KEY_PREFIX.length);
|
||||
orderByType = 'metric';
|
||||
}
|
||||
|
||||
return { ...filterByQuery, orderBy, orderByType, sort };
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
generateLink(page) {
|
||||
return setUrlParams({ page });
|
||||
return setUrlParams({ ...this.parsedQuery, page });
|
||||
},
|
||||
submitFilters() {
|
||||
return visitUrl(setUrlParams({ ...this.parsedQuery, page: this.page }));
|
||||
},
|
||||
updateFilters(newValue) {
|
||||
this.filters = newValue;
|
||||
},
|
||||
updateSorting(newValue) {
|
||||
this.sorting = { ...this.sorting, ...newValue };
|
||||
},
|
||||
updateSortingAndEmitUpdate(newValue) {
|
||||
this.updateSorting(newValue);
|
||||
this.submitFilters();
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
titleLabel: __('Experiment candidates'),
|
||||
emptyStateLabel: __('This experiment has no logged candidates'),
|
||||
artifactsLabel: __('Artifacts'),
|
||||
detailsLabel: __('Details'),
|
||||
userLabel: __('User'),
|
||||
createdAtLabel: __('Created at'),
|
||||
nameLabel: __('Name'),
|
||||
noDataContent: __('-'),
|
||||
titleLabel: s__('MlExperimentTracking|Experiment candidates'),
|
||||
emptyStateLabel: s__('MlExperimentTracking|No candidates to display'),
|
||||
artifactsLabel: s__('MlExperimentTracking|Artifacts'),
|
||||
detailsLabel: s__('MlExperimentTracking|Details'),
|
||||
userLabel: s__('MlExperimentTracking|User'),
|
||||
createdAtLabel: s__('MlExperimentTracking|Created at'),
|
||||
nameLabel: s__('MlExperimentTracking|Name'),
|
||||
noDataContent: s__('MlExperimentTracking|-'),
|
||||
filterCandidatesLabel: s__('MlExperimentTracking|Filter candidates'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -71,6 +139,16 @@ export default {
|
|||
{{ $options.i18n.titleLabel }}
|
||||
</h3>
|
||||
|
||||
<registry-search
|
||||
:filters="filters"
|
||||
:sorting="sorting"
|
||||
:sortable-fields="sortableFields"
|
||||
@sorting:changed="updateSortingAndEmitUpdate"
|
||||
@filter:changed="updateFilters"
|
||||
@filter:submit="submitFilters"
|
||||
@filter:clear="filters = []"
|
||||
/>
|
||||
|
||||
<gl-table
|
||||
:fields="fields"
|
||||
:items="candidates"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const METRIC_KEY_PREFIX = 'metric.';
|
||||
|
||||
export const LIST_KEY_CREATED_AT = 'created_at';
|
||||
|
||||
export const BASE_SORT_FIELDS = Object.freeze([
|
||||
{
|
||||
orderBy: 'name',
|
||||
label: __('Name'),
|
||||
},
|
||||
{
|
||||
orderBy: LIST_KEY_CREATED_AT,
|
||||
label: __('Created at'),
|
||||
},
|
||||
]);
|
||||
|
|
@ -13,6 +13,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
|
||||
before_action :disable_query_limiting, only: [:usage_data]
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:ci_remove_character_limitation_raw_masked_var, type: :development)
|
||||
end
|
||||
|
||||
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
|
||||
:general, :reporting, :metrics_and_profiling, :network,
|
||||
:preferences, :update, :reset_health_check_token
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
module IssuableCollectionsAction
|
||||
extend ActiveSupport::Concern
|
||||
include GracefulTimeoutHandling
|
||||
include IssuableCollections
|
||||
include IssuesCalendar
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ module Groups
|
|||
before_action :push_licensed_features, only: [:show]
|
||||
before_action :assign_variables_to_gon, only: [:show]
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:ci_remove_character_limitation_raw_masked_var, type: :development)
|
||||
end
|
||||
|
||||
feature_category :continuous_integration
|
||||
urgency :low
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
module Projects
|
||||
module Ml
|
||||
class ExperimentsController < ::Projects::ApplicationController
|
||||
include Projects::Ml::ExperimentsHelper
|
||||
|
||||
before_action :check_feature_flag
|
||||
|
||||
feature_category :mlops
|
||||
|
|
@ -19,24 +21,19 @@ module Projects
|
|||
|
||||
return redirect_to project_ml_experiments_path(@project) unless @experiment.present?
|
||||
|
||||
page = params[:page].to_i
|
||||
page = 1 if page == 0
|
||||
find_params = params
|
||||
.transform_keys(&:underscore)
|
||||
.permit(:name, :order_by, :sort, :order_by_type)
|
||||
|
||||
@candidates = @experiment.candidates
|
||||
.including_relationships
|
||||
.page(page)
|
||||
.per(MAX_CANDIDATES_PER_PAGE)
|
||||
@candidates = CandidateFinder.new(@experiment, find_params).execute
|
||||
|
||||
return unless @candidates
|
||||
page = [params[:page].to_i, 1].max
|
||||
|
||||
return redirect_to(url_for(page: @candidates.total_pages)) if @candidates.out_of_range?
|
||||
@candidates, @pagination_info = paginate_candidates(@candidates, page, MAX_CANDIDATES_PER_PAGE)
|
||||
|
||||
@pagination = {
|
||||
page: page,
|
||||
is_last_page: @candidates.last_page?,
|
||||
per_page: MAX_CANDIDATES_PER_PAGE,
|
||||
total_items: @candidates.total_count
|
||||
}
|
||||
return if @pagination_info[:total_pages] == 0
|
||||
|
||||
redirect_to(url_for(safe_params.merge(page: @pagination_info[:total_pages]))) if @pagination_info[:out_of_range]
|
||||
|
||||
@candidates.each(&:artifact_lazy)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ module Projects
|
|||
before_action :check_builds_available!
|
||||
before_action :define_variables
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:ci_remove_character_limitation_raw_masked_var, type: :development)
|
||||
end
|
||||
|
||||
helper_method :highlight_badge
|
||||
|
||||
feature_category :continuous_integration
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
module Ml
|
||||
class CandidateFinder
|
||||
VALID_ORDER_BY_TYPES = %w[column metric].freeze
|
||||
VALID_ORDER_BY_COLUMNS = %w[name created_at id].freeze
|
||||
VALID_SORT = %w[asc desc].freeze
|
||||
|
||||
def initialize(experiment, params = {})
|
||||
@experiment = experiment
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
candidates = @experiment.candidates.including_relationships
|
||||
|
||||
candidates = by_name(candidates)
|
||||
order(candidates)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def by_name(candidates)
|
||||
return candidates unless @params[:name].present?
|
||||
|
||||
candidates.by_name(@params[:name])
|
||||
end
|
||||
|
||||
def order(candidates)
|
||||
return candidates.order_by_metric(metric_order_by, sort) if order_by_metric?
|
||||
|
||||
candidates.order_by("#{column_order_by}_#{sort}").with_order_id_desc
|
||||
end
|
||||
|
||||
def order_by_metric?
|
||||
order_by_type == 'metric'
|
||||
end
|
||||
|
||||
def order_by_type
|
||||
valid_or_default(@params[:order_by_type], VALID_ORDER_BY_TYPES, 'column')
|
||||
end
|
||||
|
||||
def column_order_by
|
||||
valid_or_default(@params[:order_by], VALID_ORDER_BY_COLUMNS, 'created_at')
|
||||
end
|
||||
|
||||
def metric_order_by
|
||||
@params[:order_by] || ''
|
||||
end
|
||||
|
||||
def sort
|
||||
valid_or_default(@params[:sort]&.downcase, VALID_SORT, 'desc')
|
||||
end
|
||||
|
||||
def valid_or_default(value, valid_values, default)
|
||||
return value if valid_values.include?(value)
|
||||
|
||||
default
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -47,6 +47,10 @@ module Ci
|
|||
]
|
||||
end
|
||||
|
||||
def ci_variable_maskable_raw_regex
|
||||
Ci::Maskable::MASK_AND_RAW_REGEX.inspect.sub('\\A', '^').sub('\\z', '$')[1...-1]
|
||||
end
|
||||
|
||||
def ci_variable_maskable_regex
|
||||
Ci::Maskable::REGEX.inspect.sub('\\A', '^').sub('\\z', '$').sub(%r{^/}, '').sub(%r{/[a-z]*$}, '').gsub('\/', '/')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -42,6 +42,23 @@ module Projects
|
|||
Gitlab::Json.generate(data)
|
||||
end
|
||||
|
||||
def paginate_candidates(candidates, page, max_per_page)
|
||||
return [candidates, nil] unless candidates
|
||||
|
||||
candidates = candidates.page(page).per(max_per_page)
|
||||
|
||||
pagination_info = {
|
||||
page: page,
|
||||
is_last_page: candidates.last_page?,
|
||||
per_page: max_per_page,
|
||||
total_items: candidates.total_count,
|
||||
total_pages: candidates.total_pages,
|
||||
out_of_range: candidates.out_of_range?
|
||||
}
|
||||
|
||||
[candidates, pagination_info]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_to_artifact(candidate)
|
||||
|
|
|
|||
|
|
@ -12,10 +12,30 @@ module Ci
|
|||
# * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':', '.', and '~'
|
||||
# * Absolutely no fun is allowed
|
||||
REGEX = %r{\A[a-zA-Z0-9_+=/@:.~-]{8,}\z}.freeze
|
||||
# * Single line
|
||||
# * No spaces
|
||||
# * Minimal length of 8 characters
|
||||
# * Some fun is allowed
|
||||
MASK_AND_RAW_REGEX = %r{\A\S{8,}\z}.freeze
|
||||
|
||||
included do
|
||||
validates :masked, inclusion: { in: [true, false] }
|
||||
validates :value, format: { with: REGEX }, if: :masked?
|
||||
validates :value, format: { with: REGEX }, if: :masked_and_expanded?
|
||||
validates :value, format: { with: MASK_AND_RAW_REGEX }, if: :masked_and_raw?
|
||||
end
|
||||
|
||||
def masked_and_raw?
|
||||
return false unless Feature.enabled?(:ci_remove_character_limitation_raw_masked_var)
|
||||
return false unless self.class.method_defined?(:raw)
|
||||
|
||||
masked? && raw?
|
||||
end
|
||||
|
||||
def masked_and_expanded?
|
||||
return true unless Feature.enabled?(:ci_remove_character_limitation_raw_masked_var)
|
||||
return true unless self.class.method_defined?(:raw)
|
||||
|
||||
masked? && !raw?
|
||||
end
|
||||
|
||||
def to_runner_variable
|
||||
|
|
|
|||
|
|
@ -4,8 +4,14 @@ module IncidentManagement
|
|||
class TimelineEventTag < ApplicationRecord
|
||||
self.table_name = 'incident_management_timeline_event_tags'
|
||||
|
||||
START_TIME_TAG_NAME = 'Start time'
|
||||
END_TIME_TAG_NAME = 'End time'
|
||||
PREDEFINED_TAGS = [
|
||||
'Start time',
|
||||
'End time',
|
||||
'Impact detected',
|
||||
'Response initiated',
|
||||
'Impact mitigated',
|
||||
'Cause identified'
|
||||
].freeze
|
||||
|
||||
belongs_to :project, inverse_of: :incident_management_timeline_event_tags
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module Ml
|
||||
class Candidate < ApplicationRecord
|
||||
include Sortable
|
||||
|
||||
PACKAGE_PREFIX = 'ml_candidate_'
|
||||
|
||||
enum status: { running: 0, scheduled: 1, finished: 2, failed: 3, killed: 4 }
|
||||
|
|
@ -19,6 +21,12 @@ module Ml
|
|||
attribute :iid, default: -> { SecureRandom.uuid }
|
||||
|
||||
scope :including_relationships, -> { includes(:latest_metrics, :params, :user) }
|
||||
scope :by_name, ->(name) { where("ml_candidates.name LIKE ?", "%#{sanitize_sql_like(name)}%") } # rubocop:disable GitlabSecurity/SqlInjection
|
||||
scope :order_by_metric, ->(metric, direction) do
|
||||
subquery = Ml::CandidateMetric.latest.where(name: metric)
|
||||
joins("INNER JOIN (#{subquery.to_sql}) latest ON latest.candidate_id = ml_candidates.id")
|
||||
.order("latest.value #{direction}, ml_candidates.id DESC")
|
||||
end
|
||||
|
||||
delegate :project_id, :project, to: :experiment
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class User < ApplicationRecord
|
|||
|
||||
MINIMUM_DAYS_CREATED = 7
|
||||
|
||||
ignore_columns %i[linkedin twitter skype website_url location organization], remove_with: '15.8', remove_after: '2023-01-22'
|
||||
ignore_columns %i[linkedin twitter skype website_url location organization], remove_with: '15.10', remove_after: '2023-02-22'
|
||||
|
||||
# Override Devise::Models::Trackable#update_tracked_fields!
|
||||
# to limit database writes to at most once every hour
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ module IncidentManagement
|
|||
class BaseService
|
||||
include Gitlab::Utils::UsageData
|
||||
|
||||
AUTOCREATE_TAGS = [TimelineEventTag::START_TIME_TAG_NAME, TimelineEventTag::END_TIME_TAG_NAME].freeze
|
||||
|
||||
def allowed?
|
||||
user&.can?(:admin_incident_management_timeline_event, incident)
|
||||
end
|
||||
|
|
@ -47,7 +45,7 @@ module IncidentManagement
|
|||
def auto_create_predefined_tags(new_tags)
|
||||
new_tags = new_tags.map(&:downcase)
|
||||
|
||||
tags_to_create = AUTOCREATE_TAGS.select { |tag| tag.downcase.in?(new_tags) }
|
||||
tags_to_create = TimelineEventTag::PREDEFINED_TAGS.select { |tag| tag.downcase.in?(new_tags) }
|
||||
|
||||
tags_to_create.each do |name|
|
||||
project.incident_management_timeline_event_tags.create(name: name)
|
||||
|
|
|
|||
|
|
@ -155,15 +155,14 @@ module IncidentManagement
|
|||
def validate_tags(project, tag_names)
|
||||
return [] unless tag_names&.any?
|
||||
|
||||
start_time_tag = AUTOCREATE_TAGS[0].downcase
|
||||
end_time_tag = AUTOCREATE_TAGS[1].downcase
|
||||
predefined_tags = TimelineEventTag::PREDEFINED_TAGS.map(&:downcase)
|
||||
|
||||
tag_names_downcased = tag_names.map(&:downcase)
|
||||
|
||||
tags = project.incident_management_timeline_event_tags.by_names(tag_names).pluck_names.map(&:downcase)
|
||||
|
||||
# remove tags from given tag_names and also remove predefined tags which can be auto created
|
||||
tag_names_downcased - tags - [start_time_tag, end_time_tag]
|
||||
tag_names_downcased - tags - predefined_tags
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
is_group: is_group.to_s,
|
||||
group_id: @group&.id || '',
|
||||
group_path: @group&.full_path,
|
||||
maskable_raw_regex: ci_variable_maskable_raw_regex,
|
||||
maskable_regex: ci_variable_maskable_regex,
|
||||
protected_by_default: ci_variable_protected_by_default?.to_s,
|
||||
aws_logo_svg_path: image_path('aws_logo.svg'),
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@
|
|||
|
||||
.dropdown.gl-display-inline.gl-md-ml-3.issue-sort-dropdown.gl-mt-3.gl-md-mt-0
|
||||
.btn-group{ role: 'group' }
|
||||
.btn-group{ role: 'group' }
|
||||
= gl_redirect_listbox_tag [created_at, activity], @sort
|
||||
= gl_redirect_listbox_tag [created_at, activity], @sort
|
||||
= forks_sort_direction_button(sort_value)
|
||||
|
||||
- if current_user && can?(current_user, :fork_project, @project)
|
||||
|
|
|
|||
|
|
@ -12,5 +12,5 @@
|
|||
candidates: items,
|
||||
metrics: metrics,
|
||||
params: params,
|
||||
pagination: @pagination.to_json
|
||||
pagination: @pagination_info.to_json
|
||||
} }
|
||||
|
|
|
|||
|
|
@ -38,12 +38,11 @@ module Projects
|
|||
|
||||
def create_incident_management_timeline_event_tags(project)
|
||||
tags = project.incident_management_timeline_event_tags.pluck_names
|
||||
start_time_name = ::IncidentManagement::TimelineEventTag::START_TIME_TAG_NAME
|
||||
end_time_name = ::IncidentManagement::TimelineEventTag::END_TIME_TAG_NAME
|
||||
predefined_tags = ::IncidentManagement::TimelineEventTag::PREDEFINED_TAGS
|
||||
|
||||
project.incident_management_timeline_event_tags.new(name: start_time_name) unless tags.include?(start_time_name)
|
||||
|
||||
project.incident_management_timeline_event_tags.new(name: end_time_name) unless tags.include?(end_time_name)
|
||||
predefined_tags.each do |tag|
|
||||
project.incident_management_timeline_event_tags.new(name: tag) unless tags.include?(tag)
|
||||
end
|
||||
|
||||
project.save!
|
||||
rescue StandardError => e
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_batch_request_for_local_and_project_includes
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108826
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/387974
|
||||
milestone: '15.9'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_remove_character_limitation_raw_masked_var
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109008
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388414
|
||||
milestone: '15.9'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
|
|
@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56707
|
|||
rollout_issue_url:
|
||||
milestone: '13.11'
|
||||
type: ops
|
||||
group: group::authentication and authorization
|
||||
group: group::anti-abuse
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveUserDetailsFieldsFromUser < Gitlab::Database::Migration[2.1]
|
||||
enable_lock_retries!
|
||||
|
||||
def change
|
||||
%i[linkedin twitter skype website_url].each do |column|
|
||||
remove_column :users, column, :string, null: false, default: ''
|
||||
end
|
||||
%i[location organization].each do |column|
|
||||
remove_column :users, column, :string, null: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
b699539dfc4453d93c64b6b3532531ec9000d61cfc81ae5267c2c52eb489632f
|
||||
|
|
@ -22893,9 +22893,6 @@ CREATE TABLE users (
|
|||
name character varying,
|
||||
admin boolean DEFAULT false NOT NULL,
|
||||
projects_limit integer NOT NULL,
|
||||
skype character varying DEFAULT ''::character varying NOT NULL,
|
||||
linkedin character varying DEFAULT ''::character varying NOT NULL,
|
||||
twitter character varying DEFAULT ''::character varying NOT NULL,
|
||||
failed_attempts integer DEFAULT 0,
|
||||
locked_at timestamp without time zone,
|
||||
username character varying,
|
||||
|
|
@ -22912,12 +22909,10 @@ CREATE TABLE users (
|
|||
confirmation_sent_at timestamp without time zone,
|
||||
unconfirmed_email character varying,
|
||||
hide_no_ssh_key boolean DEFAULT false,
|
||||
website_url character varying DEFAULT ''::character varying NOT NULL,
|
||||
admin_email_unsubscribed_at timestamp without time zone,
|
||||
notification_email character varying,
|
||||
hide_no_password boolean DEFAULT false,
|
||||
password_automatically_set boolean DEFAULT false,
|
||||
location character varying,
|
||||
encrypted_otp_secret character varying,
|
||||
encrypted_otp_secret_iv character varying,
|
||||
encrypted_otp_secret_salt character varying,
|
||||
|
|
@ -22934,7 +22929,6 @@ CREATE TABLE users (
|
|||
otp_grace_period_started_at timestamp without time zone,
|
||||
external boolean DEFAULT false,
|
||||
incoming_email_token character varying,
|
||||
organization character varying,
|
||||
auditor boolean DEFAULT false NOT NULL,
|
||||
require_two_factor_authentication_from_group boolean DEFAULT false NOT NULL,
|
||||
two_factor_grace_period integer DEFAULT 48 NOT NULL,
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ For more information, see the links shown on this page for each external provide
|
|||
|
||||
| Capability | SaaS | Self-managed |
|
||||
|-------------------------------------------------|-----------------------------------------|------------------------------------|
|
||||
| **User Provisioning** | SCIM<br>SAML <sup>1</sup> | LDAP <sup>1</sup><br>SAML <sup>1</sup><br>[OmniAuth Providers](../../integration/omniauth.md#supported-providers) <sup>1</sup> |
|
||||
| **User Provisioning** | SCIM<br>SAML <sup>1</sup> | LDAP <sup>1</sup><br>SAML <sup>1</sup><br>[OmniAuth Providers](../../integration/omniauth.md#supported-providers) <sup>1</sup><br>SCIM |
|
||||
| **User Detail Updating** (not group management) | Not Available | LDAP Sync |
|
||||
| **Authentication** | SAML at top-level group (1 provider) | LDAP (multiple providers)<br>Generic OAuth 2.0<br>SAML (only 1 permitted per unique provider)<br>Kerberos<br>JWT<br>Smartcard<br>[OmniAuth Providers](../../integration/omniauth.md#supported-providers) (only 1 permitted per unique provider) |
|
||||
| **Provider-to-GitLab Role Sync** | SAML Group Sync | LDAP Group Sync<br>SAML Group Sync ([GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/285150) and later) |
|
||||
| **User Removal** | SCIM (remove user from top-level group) | LDAP (remove user from groups and block from the instance) |
|
||||
| **User Removal** | SCIM (remove user from top-level group) | LDAP (remove user from groups and block from the instance)<br>SCIM |
|
||||
|
||||
1. Using Just-In-Time (JIT) provisioning, user accounts are created when the user first signs in.
|
||||
|
|
|
|||
|
|
@ -147,6 +147,20 @@ http://secondary.example.com/
|
|||
Last status report was: 1 minute ago
|
||||
```
|
||||
|
||||
There are up to three statuses for each item. For example, for `Repositories`, you see the following lines:
|
||||
|
||||
```plaintext
|
||||
Repositories: succeeded 12345 / total 12345 (100%)
|
||||
Verified Repositories: succeeded 12345 / total 12345 (100%)
|
||||
Repositories Checked: failed 5 / succeeded 0 / total 5 (0%)
|
||||
```
|
||||
|
||||
The 3 status items are defined as follows:
|
||||
|
||||
- The `Repositories` output shows how many repositories are synced from the primary to the secondary.
|
||||
- The `Verified Repositories` output shows how many repositories on this secondary have a matching repository checksum with the Primary.
|
||||
- The `Repositories Checked` output shows how many repositories have passed a local Git repository check (`git fsck`) on the secondary.
|
||||
|
||||
To find more details about failed items, check
|
||||
[the `gitlab-rails/geo.log` file](../../logs/log_parsing.md#find-most-common-geo-sync-errors)
|
||||
|
||||
|
|
|
|||
|
|
@ -703,7 +703,7 @@ Possible solutions:
|
|||
|
||||
## Profiling Gitaly
|
||||
|
||||
Gitaly exposes several of Golang's built-in performance profiling tools on the Prometheus listen port. For example, if Prometheus is listening
|
||||
Gitaly exposes several of the Golang built-in performance profiling tools on the Prometheus listen port. For example, if Prometheus is listening
|
||||
on port `9236` of the GitLab server:
|
||||
|
||||
- Get a list of running `goroutines` and their backtraces:
|
||||
|
|
@ -724,7 +724,7 @@ on port `9236` of the GitLab server:
|
|||
curl --output heap.bin "http://<gitaly_server>:9236/debug/pprof/heap"
|
||||
```
|
||||
|
||||
- Record a 5 second execution trace. This will impact Gitaly's performance while running:
|
||||
- Record a 5 second execution trace. This impacts the Gitaly performance while running:
|
||||
|
||||
```shell
|
||||
curl --output trace.bin "http://<gitaly_server>:9236/debug/pprof/trace?seconds=5"
|
||||
|
|
|
|||
|
|
@ -951,13 +951,25 @@ You can view descriptions of fields and arguments in:
|
|||
|
||||
#### Language and punctuation
|
||||
|
||||
Use `{x} of the {y}` where possible, where `{x}` is the item you're describing,
|
||||
and `{y}` is the resource it applies to. For example:
|
||||
To describe fields and arguments, use `{x} of the {y}` where possible,
|
||||
where `{x}` is the item you're describing, and `{y}` is the resource it applies to. For example:
|
||||
|
||||
```plaintext
|
||||
ID of the issue.
|
||||
```
|
||||
|
||||
```plaintext
|
||||
Author of the epics.
|
||||
```
|
||||
|
||||
For arguments that sort or search, start with the appropriate verb.
|
||||
To indicate the specified values, for conciseness, you can use `this` instead of
|
||||
`the given` or `the specified`. For example:
|
||||
|
||||
```plaintext
|
||||
Sort issues by this criteria.
|
||||
```
|
||||
|
||||
Do not start descriptions with `The` or `A`, for consistency and conciseness.
|
||||
|
||||
End all descriptions with a period (`.`).
|
||||
|
|
|
|||
|
|
@ -51,10 +51,25 @@ Known issues are gathered from within GitLab and from customer reported issues.
|
|||
|
||||
See the [GitLab AWS known issues list](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues?label_name%5B%5D=AWS+Known+Issue) for a complete list.
|
||||
|
||||
## Official GitLab releases as AMIs
|
||||
## Provision a single GitLab instance on AWS
|
||||
|
||||
If you want to provision a single GitLab instance on AWS, you have two options:
|
||||
|
||||
- The marketplace subscription
|
||||
- The official GitLab AMIs
|
||||
|
||||
### Marketplace subscription
|
||||
|
||||
GitLab provides a 5 user subscription as an AWS Marketplace subscription to help teams of all sizes to get started with an Ultimate licensed instance in record time. The Marketplace subscription can be easily upgraded to any GitLab licensing via an AWS Marketplace Private Offer, with the convenience of continued AWS billing. No migration is necessary to obtain a larger, non-time based license from GitLab. Per-minute licensing is automatically removed when you accept the private offer.
|
||||
|
||||
For a tutorial on provisioning a GitLab Instance via a Marketplace Subscription, [use this tutorial](https://gitlab.awsworkshop.io/040_partner_setup.html). The tutorial links to the [GitLab Ultimate Marketplace Listing](https://aws.amazon.com/marketplace/pp/prodview-g6ktjmpuc33zk), but you can also use the [GitLab Premium Marketplace Listing](https://aws.amazon.com/marketplace/pp/prodview-amk6tacbois2k) to provision an instance.
|
||||
|
||||
### Official GitLab releases as AMIs
|
||||
|
||||
GitLab produces Amazon Machine Images (AMI) during the regular release process. The AMIs can be used for single instance GitLab installation or, by configuring `/etc/gitlab/gitlab.rb`, can be specialized for specific GitLab service roles (for example a Gitaly server). Older releases remain available and can be used to migrate an older GitLab server to AWS.
|
||||
|
||||
Initial licensing can either be the Free Enterprise License (EE) or the open source Community Edition (CE). The Enterprise Edition provides the easiest path forward to a licensed version if the need arises.
|
||||
|
||||
Currently the Amazon AMI uses the Amazon prepared Ubuntu AMI (x86 and ARM are available) as its starting point.
|
||||
|
||||
NOTE:
|
||||
|
|
|
|||
|
|
@ -234,6 +234,6 @@ Once these steps are complete, GitLab has local copies of the Secure analyzers a
|
|||
them instead of an Internet-hosted container image. This allows you to run Secure in AutoDevOps in
|
||||
an offline environment.
|
||||
|
||||
Note that these steps are specific to GitLab Secure with AutoDevOps. Using other stages with
|
||||
These steps are specific to GitLab Secure with AutoDevOps. Using other stages with
|
||||
AutoDevOps may require other steps covered in the
|
||||
[Auto DevOps documentation](../../../topics/autodevops/index.md).
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ the repository. For details on the Solution format, see the Microsoft reference
|
|||
|
||||
> Introduced in GitLab 14.2.
|
||||
|
||||
Vulnerabilities that have been detected and are false positives will be flagged as false positives in the security dashboard.
|
||||
Vulnerabilities that have been detected and are false positives are flagged as false positives in the security dashboard.
|
||||
|
||||
False positive detection is available in a subset of the [supported languages](#supported-languages-and-frameworks) and [analyzers](analyzers.md):
|
||||
|
||||
|
|
@ -350,13 +350,13 @@ To override the automatic update behavior, set the `SAST_ANALYZER_IMAGE_TAG` CI/
|
|||
in your CI/CD configuration file after you include the [`SAST.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml).
|
||||
|
||||
Only set this variable within a specific job.
|
||||
If you set it [at the top level](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file), the version you set will be used for other SAST analyzers.
|
||||
If you set it [at the top level](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file), the version you set is used for other SAST analyzers.
|
||||
|
||||
You can set the tag to:
|
||||
|
||||
- A major version, like `3`. Your pipelines will use any minor or patch updates that are released within this major version.
|
||||
- A minor version, like `3.7`. Your pipelines will use any patch updates that are released within this minor version.
|
||||
- A patch version, like `3.7.0`. Your pipelines won't receive any updates.
|
||||
- A major version, like `3`. Your pipelines use any minor or patch updates that are released within this major version.
|
||||
- A minor version, like `3.7`. Your pipelines use any patch updates that are released within this minor version.
|
||||
- A patch version, like `3.7.0`. Your pipelines don't receive any updates.
|
||||
|
||||
This example uses a specific minor version of the `semgrep` analyzer and a specific patch version of the `brakeman` analyzer:
|
||||
|
||||
|
|
@ -552,7 +552,7 @@ Some analyzers make it possible to filter out vulnerabilities under a given thre
|
|||
|
||||
| CI/CD variable | Default value | Description |
|
||||
|------------------------------|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `SAST_EXCLUDED_PATHS` | `spec, test, tests, tmp` | Exclude vulnerabilities from output based on the paths. This is a comma-separated list of patterns. Patterns can be globs (see [`doublestar.Match`](https://pkg.go.dev/github.com/bmatcuk/doublestar/v4@v4.0.2#Match) for supported patterns), or file or folder paths (for example, `doc,spec`). Parent directories also match patterns. You might need to exclude temporary directories used by your build tool as these can generate false positives. To exclude paths, copy and paste the default excluded paths, then **add** your own paths to be excluded. If you don't specify the default excluded paths, you will override the defaults and _only_ paths you specify will be excluded from the SAST scans. |
|
||||
| `SAST_EXCLUDED_PATHS` | `spec, test, tests, tmp` | Exclude vulnerabilities from output based on the paths. This is a comma-separated list of patterns. Patterns can be globs (see [`doublestar.Match`](https://pkg.go.dev/github.com/bmatcuk/doublestar/v4@v4.0.2#Match) for supported patterns), or file or folder paths (for example, `doc,spec`). Parent directories also match patterns. You might need to exclude temporary directories used by your build tool as these can generate false positives. To exclude paths, copy and paste the default excluded paths, then **add** your own paths to be excluded. If you don't specify the default excluded paths, you override the defaults and _only_ paths you specify are excluded from the SAST scans. |
|
||||
| `SEARCH_MAX_DEPTH` | 4 | SAST searches the repository to detect the programming languages used, and selects the matching analyzers. Set the value of `SEARCH_MAX_DEPTH` to specify how many directory levels the search phase should span. After the analyzers have been selected, the _entire_ repository is analyzed. |
|
||||
| `SAST_BANDIT_EXCLUDED_PATHS` | | Comma-separated list of paths to exclude from scan. Uses Python's [`fnmatch` syntax](https://docs.python.org/2/library/fnmatch.html); For example: `'*/tests/*, */venv/*'`. [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/352554) in GitLab 15.4. |
|
||||
| `SAST_BRAKEMAN_LEVEL` | 1 | Ignore Brakeman vulnerabilities under given confidence level. Integer, 1=Low 3=High. |
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ To sort vulnerabilities by the date each vulnerability was detected, select the
|
|||
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/213013) to the group-level Vulnerability Report in GitLab 13.1.
|
||||
|
||||
You can export details of the vulnerabilities listed in the Vulnerability Report. The export format
|
||||
is CSV (comma separated values). Note that all vulnerabilities are included because filters do not
|
||||
is CSV (comma separated values). All vulnerabilities are included because filters do not
|
||||
apply to the export.
|
||||
|
||||
Fields included are:
|
||||
|
|
@ -263,7 +263,7 @@ To add a new vulnerability finding from your project level Vulnerability Report
|
|||
1. Select **Submit vulnerability**.
|
||||
1. Complete the fields and submit the form.
|
||||
|
||||
You will be brought to the newly created vulnerability's detail page. Manually created records appear in the
|
||||
You are brought to the newly created vulnerability's detail page. Manually created records appear in the
|
||||
Group, Project, and Security Center Vulnerability Reports. To filter them, use the Generic Tool filter.
|
||||
|
||||
## Operational vulnerabilities
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ Before GitLab displays results, the vulnerability findings in all pipeline repor
|
|||
GitLab displays one row of information for each [scan type](../terminology/index.md#scan-type-report-type) artifact present in
|
||||
the pipeline.
|
||||
|
||||
Note that each scan type's total number of vulnerabilities includes dismissed findings. If the number of findings
|
||||
Each scan type's total number of vulnerabilities includes dismissed findings. If the number of findings
|
||||
in the report doesn't match the number in **Scan details**, ensure that **Hide dismissed** is disabled.
|
||||
|
||||
### Download security scan outputs
|
||||
|
|
@ -77,7 +77,7 @@ incorporated once the pipeline finishes.
|
|||
| Confirmed | No | Confirmed |
|
||||
| Needs triage (Detected) | No | Needs triage (Detected) |
|
||||
| Resolved | No | Needs triage (Detected) |
|
||||
| N/A (i.e.: new vulnerability) | No | Needs triage (Detected) |
|
||||
| N/A (That is: new vulnerability) | No | Needs triage (Detected) |
|
||||
|
||||
## Retention period for vulnerabilities
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ observability:
|
|||
grpc_level: warn
|
||||
```
|
||||
|
||||
When `grpc_level` is set to `info` or below, there will be a lot of gRPC logs.
|
||||
When `grpc_level` is set to `info` or below, there are a lot of gRPC logs.
|
||||
|
||||
Commit the configuration changes and inspect the agent service logs:
|
||||
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ The list of GitLab.com specific settings (and their defaults) is as follows:
|
|||
| `autovacuum_vacuum_scale_factor` | 0.01 | 0.02 |
|
||||
| `checkpoint_completion_target` | 0.7 | 0.9 |
|
||||
| `checkpoint_segments` | 32 | 10 |
|
||||
| `effective_cache_size` | 338688MB | Based on how much memory is available |
|
||||
| `effective_cache_size` | 338688 MB | Based on how much memory is available |
|
||||
| `hot_standby` | on | off |
|
||||
| `hot_standby_feedback` | on | off |
|
||||
| `log_autovacuum_min_duration` | 0 | -1 |
|
||||
|
|
@ -309,19 +309,19 @@ The list of GitLab.com specific settings (and their defaults) is as follows:
|
|||
| `log_line_prefix` | `%t [%p]: [%l-1]` | empty |
|
||||
| `log_min_duration_statement` | 1000 | -1 |
|
||||
| `log_temp_files` | 0 | -1 |
|
||||
| `maintenance_work_mem` | 2048MB | 16 MB |
|
||||
| `maintenance_work_mem` | 2048 MB | 16 MB |
|
||||
| `max_replication_slots` | 5 | 0 |
|
||||
| `max_wal_senders` | 32 | 0 |
|
||||
| `max_wal_size` | 5GB | 1GB |
|
||||
| `shared_buffers` | 112896MB | Based on how much memory is available |
|
||||
| `max_wal_size` | 5 GB | 1 GB |
|
||||
| `shared_buffers` | 112896 MB | Based on how much memory is available |
|
||||
| `shared_preload_libraries` | `pg_stat_statements` | empty |
|
||||
| `shmall` | 30146560 | Based on the server's capabilities |
|
||||
| `shmmax` | 123480309760 | Based on the server's capabilities |
|
||||
| `wal_buffers` | 16MB | -1 |
|
||||
| `wal_buffers` | 16 MB | -1 |
|
||||
| `wal_keep_segments` | 512 | 10 |
|
||||
| `wal_level` | replica | minimal |
|
||||
| `statement_timeout` | 15s | 60s |
|
||||
| `idle_in_transaction_session_timeout` | 60s | 60s |
|
||||
| `statement_timeout` | 15 s | 60 s |
|
||||
| `idle_in_transaction_session_timeout` | 60 s | 60 s |
|
||||
|
||||
Some of these settings are in the process being adjusted. For example, the value
|
||||
for `shared_buffers` is quite high, and we are
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ By default, projects in a group can be forked.
|
|||
Optionally, on [GitLab Premium](https://about.gitlab.com/pricing/) or higher tiers,
|
||||
you can prevent the projects in a group from being forked outside of the current top-level group.
|
||||
|
||||
This setting will be removed from the SAML setting page, and migrated to the
|
||||
This setting is removed from the SAML setting page, and migrated to the
|
||||
group settings page. In the interim period, both of these settings are taken into consideration.
|
||||
If even one is set to `true`, then the group does not allow outside forks.
|
||||
|
||||
|
|
@ -296,7 +296,7 @@ Now you can edit the user's permissions from the **Members** page.
|
|||
|
||||
### Verify if access is blocked by IP restriction
|
||||
|
||||
If a user sees a 404 when they would normally expect access, and the problem is limited to a specific group, search the `auth.log` rails log for one or more of the following:
|
||||
If a user sees a 404 when they would usually expect access, and the problem is limited to a specific group, search the `auth.log` rails log for one or more of the following:
|
||||
|
||||
- `json.message`: `'Attempting to access IP restricted group'`
|
||||
- `json.allowed`: `false`
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ This table shows granted privileges for jobs triggered by specific types of user
|
|||
| Push source and LFS | | | | |
|
||||
|
||||
1. Only if the triggering user is not an external one.
|
||||
1. Only if the triggering user is a member of the project. See also [Usage of private Docker images with `if-not-present` pull policy](http://docs.gitlabl.com/runner/security/index.html#usage-of-private-docker-images-with-if-not-present-pull-policy).
|
||||
1. Only if the triggering user is a member of the project. See also [Usage of private Docker images with `if-not-present` pull policy](http://docs.gitlab.com/runner/security/index.html#usage-of-private-docker-images-with-if-not-present-pull-policy).
|
||||
|
||||
## Group members permissions
|
||||
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ If you regenerate 2FA recovery codes, save them. You can't use any previously cr
|
|||
|
||||
## Sign in with two-factor authentication enabled
|
||||
|
||||
Signing in with 2FA enabled is only slightly different than the normal sign-in process. Enter your username and password
|
||||
Signing in with 2FA enabled is only slightly different than the typical sign-in process. Enter your username and password
|
||||
and you're presented with a second prompt, depending on which type of 2FA you've enabled.
|
||||
|
||||
### Sign in using a one-time password
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ Wikipedia article on [comparing the different version control software](https://
|
|||
|
||||
CVS is old with no new release since 2008. Git provides more tools to work
|
||||
with (`git bisect` for one) which makes for a more productive workflow.
|
||||
Migrating to Git/GitLab will benefit you:
|
||||
Migrating to Git/GitLab benefits you:
|
||||
|
||||
- **Shorter learning curve**, Git has a big community and a vast number of
|
||||
tutorials to get you started (see our [Git topic](../../../topics/git/index.md)).
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ You can import projects from:
|
|||
- [Uploading a manifest file (AOSP)](manifest.md)
|
||||
- [Jira (issues only)](jira.md)
|
||||
|
||||
You can also import any Git repository through HTTP from the **New Project** page. Note that if the
|
||||
repository is too large, the import can timeout.
|
||||
You can also import any Git repository through HTTP from the **New Project** page. If the repository
|
||||
is too large, the import can timeout.
|
||||
|
||||
You can then [connect your external repository to get CI/CD benefits](../../../ci/ci_cd_for_external_repos/index.md).
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ The backups produced don't depend on the operating system running GitLab. You ca
|
|||
the restore method to switch between different operating system distributions or versions, as long
|
||||
as the same GitLab version [is available for installation](../../../administration/package_information/supported_os.md).
|
||||
|
||||
Also note that administrators can use the [Users API](../../../api/users.md) to migrate users.
|
||||
Administrators can use the [Users API](../../../api/users.md) to migrate users.
|
||||
|
||||
## View project import history
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,6 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_context!
|
||||
context.logger.instrument(:config_file_artifact_validate_context) do
|
||||
if !creating_child_pipeline?
|
||||
|
|
@ -54,6 +52,8 @@ module Gitlab
|
|||
errors.push("File `#{masked_location}` is empty!") unless content.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def artifact_job
|
||||
strong_memoize(:artifact_job) do
|
||||
next unless creating_child_pipeline?
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ module Gitlab
|
|||
expanded_content_hash
|
||||
end
|
||||
|
||||
# Will be removed with the FF ci_batch_request_for_local_and_project_includes
|
||||
def validate!
|
||||
validate_location!
|
||||
validate_context! if valid?
|
||||
|
|
@ -68,22 +69,16 @@ module Gitlab
|
|||
[params, context.project&.full_path, context.sha].hash
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def expanded_content_hash
|
||||
return unless content_hash
|
||||
|
||||
strong_memoize(:expanded_content_yaml) do
|
||||
expand_includes(content_hash)
|
||||
def load_and_validate_expanded_hash!
|
||||
context.logger.instrument(:config_file_fetch_content_hash) do
|
||||
content_hash # calling the method loads then memoizes the result
|
||||
end
|
||||
end
|
||||
|
||||
def content_hash
|
||||
strong_memoize(:content_yaml) do
|
||||
::Gitlab::Ci::Config::Yaml.load!(content)
|
||||
context.logger.instrument(:config_file_expand_content_includes) do
|
||||
expanded_content_hash # calling the method expands then memoizes the result
|
||||
end
|
||||
rescue Gitlab::Config::Loader::FormatError
|
||||
nil
|
||||
|
||||
validate_hash!
|
||||
end
|
||||
|
||||
def validate_location!
|
||||
|
|
@ -98,6 +93,31 @@ module Gitlab
|
|||
raise NotImplementedError, 'subclass must implement validate_context'
|
||||
end
|
||||
|
||||
def validate_content!
|
||||
if content.blank?
|
||||
errors.push("Included file `#{masked_location}` is empty or does not exist!")
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def expanded_content_hash
|
||||
return unless content_hash
|
||||
|
||||
strong_memoize(:expanded_content_hash) do
|
||||
expand_includes(content_hash)
|
||||
end
|
||||
end
|
||||
|
||||
def content_hash
|
||||
strong_memoize(:content_hash) do
|
||||
::Gitlab::Ci::Config::Yaml.load!(content)
|
||||
end
|
||||
rescue Gitlab::Config::Loader::FormatError
|
||||
nil
|
||||
end
|
||||
|
||||
# Will be removed with the FF ci_batch_request_for_local_and_project_includes
|
||||
def fetch_and_validate_content!
|
||||
context.logger.instrument(:config_file_fetch_content) do
|
||||
content # calling the method fetches then memoizes the result
|
||||
|
|
@ -110,24 +130,6 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def load_and_validate_expanded_hash!
|
||||
context.logger.instrument(:config_file_fetch_content_hash) do
|
||||
content_hash # calling the method loads then memoizes the result
|
||||
end
|
||||
|
||||
context.logger.instrument(:config_file_expand_content_includes) do
|
||||
expanded_content_hash # calling the method expands then memoizes the result
|
||||
end
|
||||
|
||||
validate_hash!
|
||||
end
|
||||
|
||||
def validate_content!
|
||||
if content.blank?
|
||||
errors.push("Included file `#{masked_location}` is empty or does not exist!")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_hash!
|
||||
if to_hash.blank?
|
||||
errors.push("Included file `#{masked_location}` does not have valid YAML syntax!")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ module Gitlab
|
|||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(params, context)
|
||||
@location = params[:local]
|
||||
@location = if ::Feature.enabled?(:ci_batch_request_for_local_and_project_includes, context.project)
|
||||
# `Repository#blobs_at` does not support files with the `/` prefix.
|
||||
Gitlab::Utils.remove_leading_slashes(params[:local])
|
||||
else
|
||||
params[:local]
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
|
@ -29,8 +34,6 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_context!
|
||||
return if context.project&.repository
|
||||
|
||||
|
|
@ -45,7 +48,27 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_local_content
|
||||
if ::Feature.disabled?(:ci_batch_request_for_local_and_project_includes, context.project)
|
||||
return legacy_fetch_local_content
|
||||
end
|
||||
|
||||
BatchLoader.for([context.sha, location])
|
||||
.batch(key: context.project) do |locations, loader, args|
|
||||
context.logger.instrument(:config_file_fetch_local_content) do
|
||||
args[:key].repository.blobs_at(locations).each do |blob|
|
||||
loader.call([blob.commit_id, blob.path], blob.data)
|
||||
end
|
||||
end
|
||||
rescue GRPC::InvalidArgument
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
||||
# Will be removed with the FF ci_batch_request_for_local_and_project_includes
|
||||
def legacy_fetch_local_content
|
||||
context.logger.instrument(:config_file_fetch_local_content) do
|
||||
context.project.repository.blob_data_at(context.sha, location)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_context!
|
||||
if !can_access_local_content?
|
||||
errors.push("Project `#{masked_project_name}` not found or access denied! Make sure any includes in the pipeline configuration are correctly defined.")
|
||||
|
|
@ -55,6 +53,8 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
strong_memoize(:project) do
|
||||
::Project.find_by_full_path(project_name)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_context!
|
||||
# no-op
|
||||
end
|
||||
|
|
@ -42,6 +40,8 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_remote_content
|
||||
begin
|
||||
response = context.logger.instrument(:config_file_fetch_remote_content) do
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_context!
|
||||
# no-op
|
||||
end
|
||||
|
|
@ -45,6 +43,8 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def template_name
|
||||
return unless template_name_valid?
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,34 @@ module Gitlab
|
|||
private
|
||||
|
||||
def process_without_instrumentation(files)
|
||||
if ::Feature.disabled?(:ci_batch_request_for_local_and_project_includes, context.project)
|
||||
return legacy_process_without_instrumentation(files)
|
||||
end
|
||||
|
||||
files.each do |file|
|
||||
verify_execution_time!
|
||||
|
||||
file.validate_location!
|
||||
file.validate_context! if file.valid?
|
||||
file.content if file.valid?
|
||||
end
|
||||
|
||||
# We do not combine the loops because we need to load the content of all files before continuing
|
||||
# to call `BatchLoader` for all locations.
|
||||
files.each do |file| # rubocop:disable Style/CombinableLoops
|
||||
# Checking the max includes will be changed with https://gitlab.com/gitlab-org/gitlab/-/issues/367150
|
||||
verify_max_includes!
|
||||
verify_execution_time!
|
||||
|
||||
file.validate_content! if file.valid?
|
||||
file.load_and_validate_expanded_hash! if file.valid?
|
||||
|
||||
context.expandset.add(file)
|
||||
end
|
||||
end
|
||||
|
||||
# Will be removed with the FF ci_batch_request_for_local_and_project_includes
|
||||
def legacy_process_without_instrumentation(files)
|
||||
files.select do |file|
|
||||
verify_max_includes!
|
||||
verify_execution_time!
|
||||
|
|
|
|||
|
|
@ -82,7 +82,11 @@ module Gitlab
|
|||
|
||||
# Append path to host, making sure there's one single / in between
|
||||
def append_path(host, path)
|
||||
"#{host.to_s.sub(%r{\/+$}, '')}/#{path.to_s.sub(%r{^\/+}, '')}"
|
||||
"#{host.to_s.sub(%r{\/+$}, '')}/#{remove_leading_slashes(path)}"
|
||||
end
|
||||
|
||||
def remove_leading_slashes(str)
|
||||
str.to_s.sub(%r{^/+}, '')
|
||||
end
|
||||
|
||||
# A slugified version of the string, suitable for inclusion in URLs and
|
||||
|
|
|
|||
|
|
@ -1374,9 +1374,6 @@ msgstr ""
|
|||
msgid ", or "
|
||||
msgstr ""
|
||||
|
||||
msgid "-"
|
||||
msgstr ""
|
||||
|
||||
msgid "- %{policy_name} (notifying after %{elapsed_time} minutes unless %{status})"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16705,9 +16702,6 @@ msgstr ""
|
|||
msgid "Experiment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Experiment candidates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Experiments"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27198,6 +27192,33 @@ msgstr ""
|
|||
msgid "MissingSSHKeyWarningLink|You won't be able to pull or push repositories via SSH until you add an SSH key to your profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|-"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Created at"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Details"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Experiment candidates"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Filter candidates"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|No candidates to display"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|User"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentsEmptyState|No Experiments to Show"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43156,9 +43177,6 @@ msgstr ""
|
|||
msgid "This epic would exceed maximum number of related epics."
|
||||
msgstr ""
|
||||
|
||||
msgid "This experiment has no logged candidates"
|
||||
msgstr ""
|
||||
|
||||
msgid "This feature requires local storage to be enabled"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::Ml::CandidateFinder, feature_category: :mlops do
|
||||
let_it_be(:experiment) { create(:ml_experiments, user: nil) }
|
||||
|
||||
let_it_be(:candidates) do
|
||||
%w[c a da b].zip([3, 2, 4, 1]).map do |name, auc|
|
||||
make_candidate_and_metric(name, auc, experiment)
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:another_candidate) { create(:ml_candidates) }
|
||||
let_it_be(:first_candidate) { candidates.first }
|
||||
|
||||
let(:finder) { described_class.new(experiment, params) }
|
||||
let(:page) { 1 }
|
||||
let(:default_params) { { page: page } }
|
||||
let(:params) { default_params }
|
||||
|
||||
subject { finder.execute }
|
||||
|
||||
describe '.execute' do
|
||||
describe 'by name' do
|
||||
context 'when params has no name' do
|
||||
it 'fetches all candidates in the experiment' do
|
||||
expect(subject).to match_array(candidates)
|
||||
end
|
||||
|
||||
it 'does not fetch candidate not in experiment' do
|
||||
expect(subject).not_to include(another_candidate)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when name is included in params' do
|
||||
let(:params) { { name: 'a' } }
|
||||
|
||||
it 'fetches the correct candidates' do
|
||||
expect(subject).to match_array(candidates.values_at(2, 1))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'sorting' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:test_case, :order_by, :order_by_type, :direction, :expected_order) do
|
||||
'default params' | nil | nil | nil | [3, 2, 1, 0]
|
||||
'ascending order' | nil | nil | 'ASC' | [0, 1, 2, 3]
|
||||
'column is passed' | 'name' | 'column' | 'ASC' | [1, 3, 0, 2]
|
||||
'column is a metric' | 'auc' | 'metric' | nil | [2, 0, 1, 3]
|
||||
'invalid sort' | nil | nil | 'INVALID' | [3, 2, 1, 0]
|
||||
'invalid order by' | 'INVALID' | 'column' | 'desc' | [3, 2, 1, 0]
|
||||
'invalid order by metric' | nil | 'metric' | 'desc' | []
|
||||
end
|
||||
with_them do
|
||||
let(:params) { { order_by: order_by, order_by_type: order_by_type, sort: direction } }
|
||||
|
||||
it { expect(subject).to eq(candidates.values_at(*expected_order)) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when name and sort by metric is passed' do
|
||||
let(:params) { { order_by: 'auc', order_by_type: 'metric', sort: 'DESC', name: 'a' } }
|
||||
|
||||
it { expect(subject).to eq(candidates.values_at(2, 1)) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def make_candidate_and_metric(name, auc_value, experiment)
|
||||
create(:ml_candidates, name: name, experiment: experiment, user: nil).tap do |c|
|
||||
create(:ml_candidate_metrics, name: 'auc', candidate_id: c.id, value: 10)
|
||||
create(:ml_candidate_metrics, name: 'auc', candidate_id: c.id, value: auc_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -21,6 +21,8 @@ describe('Ci variable modal', () => {
|
|||
let trackingSpy;
|
||||
|
||||
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
|
||||
const maskableRawRegex = '^\\S{8,}$';
|
||||
|
||||
const mockVariables = mockVariablesWithScopes(instanceString);
|
||||
|
||||
const defaultProvide = {
|
||||
|
|
@ -30,8 +32,12 @@ describe('Ci variable modal', () => {
|
|||
awsTipLearnLink: '/learn-link',
|
||||
containsVariableReferenceLink: '/reference',
|
||||
environmentScopeLink: '/help/environments',
|
||||
glFeatures: {
|
||||
ciRemoveCharacterLimitationRawMaskedVar: true,
|
||||
},
|
||||
isProtectedByDefault: false,
|
||||
maskedEnvironmentVariablesLink: '/variables-link',
|
||||
maskableRawRegex,
|
||||
maskableRegex,
|
||||
protectedEnvironmentVariablesLink: '/protected-link',
|
||||
};
|
||||
|
|
@ -424,6 +430,54 @@ describe('Ci variable modal', () => {
|
|||
describe('Validations', () => {
|
||||
const maskError = 'This variable can not be masked.';
|
||||
|
||||
describe('when the variable is raw', () => {
|
||||
const [variable] = mockVariables;
|
||||
const validRawMaskedVariable = {
|
||||
...variable,
|
||||
value: 'd$%^asdsadas',
|
||||
masked: false,
|
||||
raw: true,
|
||||
};
|
||||
|
||||
describe('and FF is enabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mountExtended,
|
||||
props: { selectedVariable: validRawMaskedVariable },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show an error with symbols', async () => {
|
||||
await findMaskedVariableCheckbox().trigger('click');
|
||||
|
||||
expect(findModal().text()).not.toContain(maskError);
|
||||
});
|
||||
|
||||
it('should not show an error when length is less than 8', async () => {
|
||||
await findValueField().vm.$emit('input', 'a');
|
||||
await findMaskedVariableCheckbox().trigger('click');
|
||||
|
||||
expect(findModal().text()).toContain(maskError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and FF is disabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mountExtended,
|
||||
props: { selectedVariable: validRawMaskedVariable },
|
||||
provide: { glFeatures: { ciRemoveCharacterLimitationRawMaskedVar: false } },
|
||||
});
|
||||
});
|
||||
|
||||
it('should show an error with symbols', async () => {
|
||||
await findMaskedVariableCheckbox().trigger('click');
|
||||
|
||||
expect(findModal().text()).toContain(maskError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the mask state is invalid', () => {
|
||||
beforeEach(async () => {
|
||||
const [variable] = mockVariables;
|
||||
|
|
|
|||
|
|
@ -1,761 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MlExperiment with candidates renders correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="gl-alert gl-alert-warning"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s16 gl-alert-icon"
|
||||
data-testid="warning-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#warning"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div
|
||||
aria-live="assertive"
|
||||
class="gl-alert-content"
|
||||
role="alert"
|
||||
>
|
||||
<h2
|
||||
class="gl-alert-title"
|
||||
>
|
||||
Machine Learning Experiment Tracking is in Incubating Phase
|
||||
</h2>
|
||||
|
||||
<div
|
||||
class="gl-alert-body"
|
||||
>
|
||||
|
||||
GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited
|
||||
|
||||
<a
|
||||
class="gl-link"
|
||||
href="https://about.gitlab.com/handbook/engineering/incubation/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gl-alert-actions"
|
||||
>
|
||||
<a
|
||||
class="btn gl-alert-action btn-confirm btn-md gl-button"
|
||||
href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
Feedback
|
||||
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="Dismiss"
|
||||
class="btn gl-dismiss-btn btn-default btn-sm gl-button btn-default-tertiary btn-icon"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon gl-icon s16"
|
||||
data-testid="close-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#close"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!---->
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
|
||||
Experiment candidates
|
||||
|
||||
</h3>
|
||||
|
||||
<table
|
||||
aria-busy="false"
|
||||
aria-colcount="9"
|
||||
class="table b-table gl-table gl-mt-0! ml-candidate-table table-sm"
|
||||
role="table"
|
||||
>
|
||||
<!---->
|
||||
<!---->
|
||||
<thead
|
||||
class=""
|
||||
role="rowgroup"
|
||||
>
|
||||
<!---->
|
||||
<tr
|
||||
class=""
|
||||
role="row"
|
||||
>
|
||||
<th
|
||||
aria-colindex="1"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div>
|
||||
Name
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="2"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div>
|
||||
Created at
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="3"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div>
|
||||
User
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="4"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div>
|
||||
L1 Ratio
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="5"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div>
|
||||
Rmse
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="6"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div>
|
||||
Auc
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="7"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div>
|
||||
Mae
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="8"
|
||||
aria-label="Details"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div />
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="9"
|
||||
aria-label="Artifact"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
role="rowgroup"
|
||||
>
|
||||
<!---->
|
||||
<tr
|
||||
class=""
|
||||
role="row"
|
||||
>
|
||||
<td
|
||||
aria-colindex="1"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="aCandidate"
|
||||
>
|
||||
aCandidate
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="2"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<time
|
||||
class=""
|
||||
datetime="2023-01-05T14:07:02.975Z"
|
||||
title="2023-01-05T14:07:02.975Z"
|
||||
>
|
||||
in 2 years
|
||||
</time>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="3"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="/root"
|
||||
title="root"
|
||||
>
|
||||
@root
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="4"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="0.4"
|
||||
>
|
||||
0.4
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="5"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="1"
|
||||
>
|
||||
1
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="6"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="7"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="8"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="link_to_candidate1"
|
||||
title="Details"
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="9"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="link_to_artifact"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="Artifacts"
|
||||
>
|
||||
Artifacts
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class=""
|
||||
role="row"
|
||||
>
|
||||
<td
|
||||
aria-colindex="1"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="2"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<time
|
||||
class=""
|
||||
datetime="2023-01-05T14:07:02.975Z"
|
||||
title="2023-01-05T14:07:02.975Z"
|
||||
>
|
||||
in 2 years
|
||||
</time>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="3"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div>
|
||||
-
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="4"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="0.5"
|
||||
>
|
||||
0.5
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="5"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="6"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="0.3"
|
||||
>
|
||||
0.3
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="7"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="8"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="link_to_candidate2"
|
||||
title="Details"
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="9"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="Artifacts"
|
||||
>
|
||||
|
||||
-
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class=""
|
||||
role="row"
|
||||
>
|
||||
<td
|
||||
aria-colindex="1"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="2"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<time
|
||||
class=""
|
||||
datetime="2023-01-05T14:07:02.975Z"
|
||||
title="2023-01-05T14:07:02.975Z"
|
||||
>
|
||||
in 2 years
|
||||
</time>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="3"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div>
|
||||
-
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="4"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="0.5"
|
||||
>
|
||||
0.5
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="5"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="6"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="0.3"
|
||||
>
|
||||
0.3
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="7"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="8"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="link_to_candidate3"
|
||||
title="Details"
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="9"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="Artifacts"
|
||||
>
|
||||
|
||||
-
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class=""
|
||||
role="row"
|
||||
>
|
||||
<td
|
||||
aria-colindex="1"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="2"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<time
|
||||
class=""
|
||||
datetime="2023-01-05T14:07:02.975Z"
|
||||
title="2023-01-05T14:07:02.975Z"
|
||||
>
|
||||
in 2 years
|
||||
</time>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="3"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div>
|
||||
-
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="4"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="0.5"
|
||||
>
|
||||
0.5
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="5"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="6"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="0.3"
|
||||
>
|
||||
0.3
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="7"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="8"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="link_to_candidate4"
|
||||
title="Details"
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="9"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="Artifacts"
|
||||
>
|
||||
|
||||
-
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class=""
|
||||
role="row"
|
||||
>
|
||||
<td
|
||||
aria-colindex="1"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="2"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<time
|
||||
class=""
|
||||
datetime="2023-01-05T14:07:02.975Z"
|
||||
title="2023-01-05T14:07:02.975Z"
|
||||
>
|
||||
in 2 years
|
||||
</time>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="3"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div>
|
||||
-
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="4"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="0.5"
|
||||
>
|
||||
0.5
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="5"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="6"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="0.3"
|
||||
>
|
||||
0.3
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="7"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title=""
|
||||
>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="8"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="link_to_candidate5"
|
||||
title="Details"
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="9"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<div
|
||||
title="Artifacts"
|
||||
>
|
||||
|
||||
-
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!---->
|
||||
<!---->
|
||||
</tbody>
|
||||
<!---->
|
||||
</table>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
import { GlAlert, GlPagination } from '@gitlab/ui';
|
||||
import { GlAlert, GlPagination, GlTable, GlLink } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
|
||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import * as urlHelpers from '~/lib/utils/url_utility';
|
||||
|
||||
describe('MlExperiment', () => {
|
||||
let wrapper;
|
||||
|
|
@ -11,130 +15,340 @@ describe('MlExperiment', () => {
|
|||
paramNames = [],
|
||||
pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 },
|
||||
) => {
|
||||
return mountExtended(MlExperiment, {
|
||||
wrapper = mountExtended(MlExperiment, {
|
||||
provide: { candidates, metricNames, paramNames, pagination },
|
||||
});
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const defaultPagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 5 };
|
||||
|
||||
const findEmptyState = () => wrapper.findByText('This experiment has no logged candidates');
|
||||
const candidates = [
|
||||
{
|
||||
rmse: 1,
|
||||
l1_ratio: 0.4,
|
||||
details: 'link_to_candidate1',
|
||||
artifact: 'link_to_artifact',
|
||||
name: 'aCandidate',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
user: { username: 'root', path: '/root' },
|
||||
},
|
||||
{
|
||||
auc: 0.3,
|
||||
l1_ratio: 0.5,
|
||||
details: 'link_to_candidate2',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
name: null,
|
||||
user: null,
|
||||
},
|
||||
{
|
||||
auc: 0.3,
|
||||
l1_ratio: 0.5,
|
||||
details: 'link_to_candidate3',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
name: null,
|
||||
user: null,
|
||||
},
|
||||
{
|
||||
auc: 0.3,
|
||||
l1_ratio: 0.5,
|
||||
details: 'link_to_candidate4',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
name: null,
|
||||
user: null,
|
||||
},
|
||||
{
|
||||
auc: 0.3,
|
||||
l1_ratio: 0.5,
|
||||
details: 'link_to_candidate5',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
name: null,
|
||||
user: null,
|
||||
},
|
||||
];
|
||||
|
||||
const createWrapperWithCandidates = (pagination = defaultPagination) => {
|
||||
createWrapper(candidates, ['rmse', 'auc', 'mae'], ['l1_ratio'], pagination);
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findPagination = () => wrapper.findComponent(GlPagination);
|
||||
const findEmptyState = () => wrapper.findByText('No candidates to display');
|
||||
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
|
||||
const findTable = () => wrapper.findComponent(GlTable);
|
||||
const findTableHeaders = () => findTable().findAll('th');
|
||||
const findTableRows = () => findTable().findAll('tbody > tr');
|
||||
const findNthTableRow = (idx) => findTableRows().at(idx);
|
||||
const findColumnInRow = (row, col) => findNthTableRow(row).findAll('td').at(col);
|
||||
const hrefInRowAndColumn = (row, col) =>
|
||||
findColumnInRow(row, col).findComponent(GlLink).attributes().href;
|
||||
|
||||
it('shows incubation warning', () => {
|
||||
wrapper = createWrapper();
|
||||
createWrapper();
|
||||
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('no candidates', () => {
|
||||
it('shows empty state', () => {
|
||||
wrapper = createWrapper();
|
||||
describe('default inputs', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper();
|
||||
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('shows empty state', () => {
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show pagination', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(findPagination().exists()).toBe(false);
|
||||
});
|
||||
|
||||
expect(wrapper.findComponent(GlPagination).exists()).toBe(false);
|
||||
it('there are no columns', () => {
|
||||
expect(findTable().findAll('th')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('initializes sorting correctly', () => {
|
||||
expect(findRegistrySearch().props('sorting')).toMatchObject({
|
||||
orderBy: 'created_at',
|
||||
sort: 'desc',
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes filters correctly', () => {
|
||||
expect(findRegistrySearch().props('filters')).toMatchObject([{ value: { data: '' } }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateLink', () => {
|
||||
it('generates the correct url', () => {
|
||||
setWindowLocation(
|
||||
'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc&page=1',
|
||||
);
|
||||
|
||||
createWrapperWithCandidates();
|
||||
|
||||
expect(findPagination().props('linkGen')(2)).toBe(
|
||||
'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc&page=2',
|
||||
);
|
||||
});
|
||||
|
||||
it('generates the correct url when no name', () => {
|
||||
setWindowLocation('https://blah.com/?orderBy=auc&orderByType=metric&sort=asc');
|
||||
|
||||
createWrapperWithCandidates();
|
||||
|
||||
expect(findPagination().props('linkGen')(2)).toBe(
|
||||
'https://blah.com/?orderBy=auc&orderByType=metric&sort=asc&page=2',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search', () => {
|
||||
it('shows search box', () => {
|
||||
createWrapper();
|
||||
|
||||
expect(findRegistrySearch().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('metrics are added as options for sorting', () => {
|
||||
createWrapper([], ['bar']);
|
||||
|
||||
const labels = findRegistrySearch()
|
||||
.props('sortableFields')
|
||||
.map((e) => e.orderBy);
|
||||
expect(labels).toContain('metric.bar');
|
||||
});
|
||||
|
||||
it('sets the component filters based on the querystring', () => {
|
||||
setWindowLocation('https://blah?name=A&orderBy=B&sort=C');
|
||||
|
||||
createWrapper();
|
||||
|
||||
expect(findRegistrySearch().props('filters')).toMatchObject([{ value: { data: 'A' } }]);
|
||||
});
|
||||
|
||||
it('sets the component sort based on the querystring', () => {
|
||||
setWindowLocation('https://blah?name=A&orderBy=B&sort=C');
|
||||
|
||||
createWrapper();
|
||||
|
||||
expect(findRegistrySearch().props('sorting')).toMatchObject({ orderBy: 'B', sort: 'c' });
|
||||
});
|
||||
|
||||
it('sets the component sort based on the querystring, when order by is a metric', () => {
|
||||
setWindowLocation('https://blah?name=A&orderBy=B&sort=C&orderByType=metric');
|
||||
|
||||
createWrapper();
|
||||
|
||||
expect(findRegistrySearch().props('sorting')).toMatchObject({
|
||||
orderBy: 'metric.B',
|
||||
sort: 'c',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search submit', () => {
|
||||
beforeEach(() => {
|
||||
setWindowLocation('https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc');
|
||||
jest.spyOn(urlHelpers, 'visitUrl').mockImplementation(() => {});
|
||||
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('On submit, reloads to correct page', () => {
|
||||
findRegistrySearch().vm.$emit('filter:submit');
|
||||
|
||||
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
|
||||
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
|
||||
'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc&page=1',
|
||||
);
|
||||
});
|
||||
|
||||
it('On sorting changed, reloads to correct page', () => {
|
||||
findRegistrySearch().vm.$emit('sorting:changed', { orderBy: 'created_at' });
|
||||
|
||||
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
|
||||
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
|
||||
'https://blah.com/?name=query&orderBy=created_at&orderByType=column&sort=asc&page=1',
|
||||
);
|
||||
});
|
||||
|
||||
it('On sorting changed and is metric, reloads to correct page', () => {
|
||||
findRegistrySearch().vm.$emit('sorting:changed', { orderBy: 'metric.auc' });
|
||||
|
||||
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
|
||||
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
|
||||
'https://blah.com/?name=query&orderBy=auc&orderByType=metric&sort=asc&page=1',
|
||||
);
|
||||
});
|
||||
|
||||
it('On direction changed, reloads to correct page', () => {
|
||||
findRegistrySearch().vm.$emit('sorting:changed', { sort: 'desc' });
|
||||
|
||||
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
|
||||
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
|
||||
'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=desc&page=1',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with candidates', () => {
|
||||
const defaultPagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 5 };
|
||||
|
||||
const createWrapperWithCandidates = (pagination = defaultPagination) => {
|
||||
return createWrapper(
|
||||
[
|
||||
{
|
||||
rmse: 1,
|
||||
l1_ratio: 0.4,
|
||||
details: 'link_to_candidate1',
|
||||
artifact: 'link_to_artifact',
|
||||
name: 'aCandidate',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
user: { username: 'root', path: '/root' },
|
||||
},
|
||||
{
|
||||
auc: 0.3,
|
||||
l1_ratio: 0.5,
|
||||
details: 'link_to_candidate2',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
name: null,
|
||||
user: null,
|
||||
},
|
||||
{
|
||||
auc: 0.3,
|
||||
l1_ratio: 0.5,
|
||||
details: 'link_to_candidate3',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
name: null,
|
||||
user: null,
|
||||
},
|
||||
{
|
||||
auc: 0.3,
|
||||
l1_ratio: 0.5,
|
||||
details: 'link_to_candidate4',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
name: null,
|
||||
user: null,
|
||||
},
|
||||
{
|
||||
auc: 0.3,
|
||||
l1_ratio: 0.5,
|
||||
details: 'link_to_candidate5',
|
||||
created_at: '2023-01-05T14:07:02.975Z',
|
||||
name: null,
|
||||
user: null,
|
||||
},
|
||||
],
|
||||
['rmse', 'auc', 'mae'],
|
||||
['l1_ratio'],
|
||||
pagination,
|
||||
);
|
||||
};
|
||||
|
||||
it('renders correctly', () => {
|
||||
wrapper = createWrapperWithCandidates();
|
||||
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Pagination behaviour', () => {
|
||||
it('should show', () => {
|
||||
wrapper = createWrapperWithCandidates();
|
||||
beforeEach(() => {
|
||||
createWrapperWithCandidates();
|
||||
});
|
||||
|
||||
expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
|
||||
it('should show', () => {
|
||||
expect(findPagination().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should get the page number from the URL', () => {
|
||||
wrapper = createWrapperWithCandidates({ ...defaultPagination, page: 2 });
|
||||
createWrapperWithCandidates({ ...defaultPagination, page: 2 });
|
||||
|
||||
expect(wrapper.findComponent(GlPagination).props().value).toBe(2);
|
||||
expect(findPagination().props().value).toBe(2);
|
||||
});
|
||||
|
||||
it('should not have a prevPage if the page is 1', () => {
|
||||
wrapper = createWrapperWithCandidates();
|
||||
|
||||
expect(wrapper.findComponent(GlPagination).props().prevPage).toBe(null);
|
||||
expect(findPagination().props().prevPage).toBe(null);
|
||||
});
|
||||
|
||||
it('should set the prevPage to 1 if the page is 2', () => {
|
||||
wrapper = createWrapperWithCandidates({ ...defaultPagination, page: 2 });
|
||||
createWrapperWithCandidates({ ...defaultPagination, page: 2 });
|
||||
|
||||
expect(wrapper.findComponent(GlPagination).props().prevPage).toBe(1);
|
||||
expect(findPagination().props().prevPage).toBe(1);
|
||||
});
|
||||
|
||||
it('should not have a nextPage if isLastPage is true', async () => {
|
||||
wrapper = createWrapperWithCandidates({ ...defaultPagination, isLastPage: true });
|
||||
createWrapperWithCandidates({ ...defaultPagination, isLastPage: true });
|
||||
|
||||
expect(wrapper.findComponent(GlPagination).props().nextPage).toBe(null);
|
||||
expect(findPagination().props().nextPage).toBe(null);
|
||||
});
|
||||
|
||||
it('should set the nextPage to 2 if the page is 1', () => {
|
||||
wrapper = createWrapperWithCandidates();
|
||||
expect(findPagination().props().nextPage).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
expect(wrapper.findComponent(GlPagination).props().nextPage).toBe(2);
|
||||
describe('Candidate table', () => {
|
||||
const firstCandidateIndex = 0;
|
||||
const secondCandidateIndex = 1;
|
||||
const firstCandidate = candidates[firstCandidateIndex];
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapperWithCandidates();
|
||||
});
|
||||
|
||||
it('renders all rows', () => {
|
||||
expect(findTableRows()).toHaveLength(candidates.length);
|
||||
});
|
||||
|
||||
it('sets the correct columns in the table', () => {
|
||||
const expectedColumnNames = [
|
||||
'Name',
|
||||
'Created at',
|
||||
'User',
|
||||
'L1 Ratio',
|
||||
'Rmse',
|
||||
'Auc',
|
||||
'Mae',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
|
||||
expect(findTableHeaders().wrappers.map((h) => h.text())).toEqual(expectedColumnNames);
|
||||
});
|
||||
|
||||
describe('Artifact column', () => {
|
||||
const artifactColumnIndex = -1;
|
||||
|
||||
it('shows the a link to the artifact', () => {
|
||||
expect(hrefInRowAndColumn(firstCandidateIndex, artifactColumnIndex)).toBe(
|
||||
firstCandidate.artifact,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows empty state when no artifact', () => {
|
||||
expect(findColumnInRow(secondCandidateIndex, artifactColumnIndex).text()).toBe('-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('User column', () => {
|
||||
const userColumn = 2;
|
||||
|
||||
it('creates a link to the user', () => {
|
||||
const column = findColumnInRow(firstCandidateIndex, userColumn).findComponent(GlLink);
|
||||
|
||||
expect(column.attributes().href).toBe(firstCandidate.user.path);
|
||||
expect(column.text()).toBe(`@${firstCandidate.user.username}`);
|
||||
});
|
||||
|
||||
it('when there is no user shows empty state', () => {
|
||||
createWrapperWithCandidates();
|
||||
|
||||
expect(findColumnInRow(secondCandidateIndex, userColumn).text()).toBe('-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Candidate name column', () => {
|
||||
const nameColumnIndex = 0;
|
||||
|
||||
it('Sets the name', () => {
|
||||
expect(findColumnInRow(firstCandidateIndex, nameColumnIndex).text()).toBe(
|
||||
firstCandidate.name,
|
||||
);
|
||||
});
|
||||
|
||||
it('when there is no user shows nothing', () => {
|
||||
expect(findColumnInRow(secondCandidateIndex, nameColumnIndex).text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Detail column', () => {
|
||||
const detailColumn = -2;
|
||||
|
||||
it('is a link to details', () => {
|
||||
expect(hrefInRowAndColumn(firstCandidateIndex, detailColumn)).toBe(firstCandidate.details);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::VariablesHelper, feature_category: :pipeline_authoring do
|
||||
describe '#ci_variable_maskable_raw_regex' do
|
||||
it 'converts to a javascript regex' do
|
||||
expect(helper.ci_variable_maskable_raw_regex).to eq("^\\S{8,}$")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -109,4 +109,68 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
|
|||
expect(subject['info']).to include(expected_info)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#paginate_candidates' do
|
||||
let(:page) { 1 }
|
||||
let(:max_per_page) { 1 }
|
||||
|
||||
subject { helper.paginate_candidates(experiment.candidates.order(:id), page, max_per_page) }
|
||||
|
||||
it 'paginates' do
|
||||
expect(subject[1]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'only returns max_per_page elements' do
|
||||
expect(subject[0].size).to eq(1)
|
||||
end
|
||||
|
||||
it 'fetches the items on the first page' do
|
||||
expect(subject[0]).to eq([candidate0])
|
||||
end
|
||||
|
||||
it 'creates the pagination info' do
|
||||
expect(subject[1]).to eq({
|
||||
page: 1,
|
||||
is_last_page: false,
|
||||
per_page: 1,
|
||||
total_items: 2,
|
||||
total_pages: 2,
|
||||
out_of_range: false
|
||||
})
|
||||
end
|
||||
|
||||
context 'when not the first page' do
|
||||
let(:page) { 2 }
|
||||
|
||||
it 'fetches the right page' do
|
||||
expect(subject[0]).to eq([candidate1])
|
||||
end
|
||||
|
||||
it 'creates the pagination info' do
|
||||
expect(subject[1]).to eq({
|
||||
page: 2,
|
||||
is_last_page: true,
|
||||
per_page: 1,
|
||||
total_items: 2,
|
||||
total_pages: 2,
|
||||
out_of_range: false
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when out of bounds' do
|
||||
let(:page) { 3 }
|
||||
|
||||
it 'creates the pagination info' do
|
||||
expect(subject[1]).to eq({
|
||||
page: page,
|
||||
is_last_page: false,
|
||||
per_page: 1,
|
||||
total_items: 2,
|
||||
total_pages: 2,
|
||||
out_of_range: true
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::Ml::Mlflow::RunInfo do
|
||||
RSpec.describe API::Entities::Ml::Mlflow::RunInfo, feature_category: :mlops do
|
||||
let_it_be(:candidate) { create(:ml_candidates) }
|
||||
|
||||
subject { described_class.new(candidate, packages_url: 'http://example.com').as_json }
|
||||
|
|
|
|||
|
|
@ -30,6 +30,60 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
|
|||
.to receive(:check_execution_time!)
|
||||
end
|
||||
|
||||
describe '.initialize' do
|
||||
context 'when a local is specified' do
|
||||
let(:params) { { local: 'file' } }
|
||||
|
||||
it 'sets the location' do
|
||||
expect(local_file.location).to eq('file')
|
||||
end
|
||||
|
||||
context 'when the local is prefixed with a slash' do
|
||||
let(:params) { { local: '/file' } }
|
||||
|
||||
it 'removes the slash' do
|
||||
expect(local_file.location).to eq('file')
|
||||
end
|
||||
|
||||
context 'when the FF ci_batch_request_for_local_and_project_includes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_batch_request_for_local_and_project_includes: false)
|
||||
end
|
||||
|
||||
it 'does not remove the slash' do
|
||||
expect(local_file.location).to eq('/file')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the local is prefixed with multiple slashes' do
|
||||
let(:params) { { local: '//file' } }
|
||||
|
||||
it 'removes slashes' do
|
||||
expect(local_file.location).to eq('file')
|
||||
end
|
||||
|
||||
context 'when the FF ci_batch_request_for_local_and_project_includes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_batch_request_for_local_and_project_includes: false)
|
||||
end
|
||||
|
||||
it 'does not remove slashes' do
|
||||
expect(local_file.location).to eq('//file')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a missing local' do
|
||||
let(:params) { { local: nil } }
|
||||
|
||||
it 'sets the location to an empty string' do
|
||||
expect(local_file.location).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#matching?' do
|
||||
context 'when a local is specified' do
|
||||
let(:params) { { local: 'file' } }
|
||||
|
|
@ -91,7 +145,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
|
|||
it 'returns false and adds an error message about an empty file' do
|
||||
allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return("")
|
||||
local_file.validate!
|
||||
expect(local_file.errors).to include("Local file `/lib/gitlab/ci/templates/xxxxxx/existent-file.yml` is empty!")
|
||||
expect(local_file.errors).to include("Local file `lib/gitlab/ci/templates/xxxxxx/existent-file.yml` is empty!")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -101,7 +155,18 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
|
|||
|
||||
it 'returns false and adds an error message stating that included file does not exist' do
|
||||
expect(valid?).to be_falsy
|
||||
expect(local_file.errors).to include("Sha #{sha} is not valid!")
|
||||
expect(local_file.errors).to include("Local file `lib/gitlab/ci/templates/existent-file.yml` does not exist!")
|
||||
end
|
||||
|
||||
context 'when the FF ci_batch_request_for_local_and_project_includes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_batch_request_for_local_and_project_includes: false)
|
||||
end
|
||||
|
||||
it 'returns false and adds an error message stating that sha does not exist' do
|
||||
expect(valid?).to be_falsy
|
||||
expect(local_file.errors).to include("Sha #{sha} is not valid!")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -147,7 +212,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
|
|||
end
|
||||
|
||||
it 'returns an error message' do
|
||||
expect(local_file.error_message).to eq("Local file `/lib/gitlab/ci/templates/xxxxxxxxxxx.yml` does not exist!")
|
||||
expect(local_file.error_message).to eq("Local file `lib/gitlab/ci/templates/xxxxxxxxxxx.yml` does not exist!")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -203,7 +268,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
|
|||
context_project: project.full_path,
|
||||
context_sha: sha,
|
||||
type: :local,
|
||||
location: '/lib/gitlab/ci/templates/existent-file.yml',
|
||||
location: 'lib/gitlab/ci/templates/existent-file.yml',
|
||||
blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/lib/gitlab/ci/templates/existent-file.yml",
|
||||
raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/lib/gitlab/ci/templates/existent-file.yml",
|
||||
extra: {}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
|
|||
my_test:
|
||||
script: echo Hello World
|
||||
YAML
|
||||
'myfolder/file3.yml' => <<~YAML,
|
||||
my_deploy:
|
||||
script: echo Hello World
|
||||
YAML
|
||||
'nested_configs.yml' => <<~YAML
|
||||
include:
|
||||
- local: myfolder/file1.yml
|
||||
|
|
@ -58,16 +62,35 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
|
|||
let(:files) do
|
||||
[
|
||||
Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context),
|
||||
Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context)
|
||||
Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context),
|
||||
Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file3.yml' }, context)
|
||||
]
|
||||
end
|
||||
|
||||
it 'returns an array of file objects' do
|
||||
expect(process.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml')
|
||||
expect(process.map(&:location)).to contain_exactly(
|
||||
'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml'
|
||||
)
|
||||
end
|
||||
|
||||
it 'adds files to the expandset' do
|
||||
expect { process }.to change { context.expandset.count }.by(2)
|
||||
expect { process }.to change { context.expandset.count }.by(3)
|
||||
end
|
||||
|
||||
it 'calls Gitaly only once for all files', :request_store do
|
||||
# 1 for project.commit.id, 1 for the files
|
||||
expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(2)
|
||||
end
|
||||
|
||||
context 'when the FF ci_batch_request_for_local_and_project_includes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_batch_request_for_local_and_project_includes: false)
|
||||
end
|
||||
|
||||
it 'calls Gitaly for each file', :request_store do
|
||||
# 1 for project.commit.id, 3 for the files
|
||||
expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(4)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
# This will be use with the FF ci_refactoring_external_mapper_verifier in the next MR.
|
||||
# It can be removed when the FF is removed.
|
||||
# This will be moved from a `shared_context` to a `describe` once every feature flag is removed.
|
||||
RSpec.shared_context 'gitlab_ci_config_external_mapper' do
|
||||
include StubRequests
|
||||
include RepoHelpers
|
||||
|
|
@ -467,4 +466,12 @@ end
|
|||
|
||||
RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_authoring do
|
||||
it_behaves_like 'gitlab_ci_config_external_mapper'
|
||||
|
||||
context 'when the FF ci_batch_request_for_local_and_project_includes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_batch_request_for_local_and_project_includes: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'gitlab_ci_config_external_mapper'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
|
|||
it 'raises an error' do
|
||||
expect { processor.perform }.to raise_error(
|
||||
described_class::IncludeError,
|
||||
"Local file `/lib/gitlab/ci/templates/non-existent-file.yml` does not exist!"
|
||||
"Local file `lib/gitlab/ci/templates/non-existent-file.yml` does not exist!"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -221,7 +221,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
|
|||
it 'raises an error' do
|
||||
expect { processor.perform }.to raise_error(
|
||||
described_class::IncludeError,
|
||||
"Included file `/lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!"
|
||||
"Included file `lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -313,7 +313,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
|
|||
|
||||
expect(context.includes).to contain_exactly(
|
||||
{ type: :local,
|
||||
location: '/local/file.yml',
|
||||
location: 'local/file.yml',
|
||||
blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/local/file.yml",
|
||||
raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/local/file.yml",
|
||||
extra: {},
|
||||
|
|
@ -341,7 +341,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
|
|||
context_project: project.full_path,
|
||||
context_sha: sha },
|
||||
{ type: :local,
|
||||
location: '/templates/my-build.yml',
|
||||
location: 'templates/my-build.yml',
|
||||
blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-build.yml",
|
||||
raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-build.yml",
|
||||
extra: {},
|
||||
|
|
|
|||
|
|
@ -1503,7 +1503,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
context "when the included internal file is not present" do
|
||||
it_behaves_like 'returns errors', "Local file `/local.gitlab-ci.yml` does not exist!"
|
||||
it_behaves_like 'returns errors', "Local file `local.gitlab-ci.yml` does not exist!"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ RSpec.describe Gitlab::Utils do
|
|||
|
||||
delegate :to_boolean, :boolean_to_yes_no, :slugify, :which,
|
||||
:ensure_array_from_string, :to_exclusive_sentence, :bytes_to_megabytes,
|
||||
:append_path, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!, :decode_path, :ms_to_round_sec, :check_allowed_absolute_path_and_path_traversal!, to: :described_class
|
||||
:append_path, :remove_leading_slashes, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!,
|
||||
:decode_path, :ms_to_round_sec, :check_allowed_absolute_path_and_path_traversal!, to: :described_class
|
||||
|
||||
describe '.check_path_traversal!' do
|
||||
it 'detects path traversal in string without any separators' do
|
||||
|
|
@ -378,6 +379,23 @@ RSpec.describe Gitlab::Utils do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.remove_leading_slashes' do
|
||||
where(:str, :result) do
|
||||
'/foo/bar' | 'foo/bar'
|
||||
'//foo/bar' | 'foo/bar'
|
||||
'/foo/bar/' | 'foo/bar/'
|
||||
'foo/bar' | 'foo/bar'
|
||||
'' | ''
|
||||
nil | ''
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'removes leading slashes' do
|
||||
expect(remove_leading_slashes(str)).to eq(result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.ensure_utf8_size' do
|
||||
context 'string is has less bytes than expected' do
|
||||
it 'backfills string with null characters' do
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::Maskable do
|
||||
RSpec.describe Ci::Maskable, feature_category: :pipeline_authoring do
|
||||
let(:variable) { build(:ci_variable) }
|
||||
|
||||
describe 'masked value validations' do
|
||||
subject { variable }
|
||||
|
||||
context 'when variable is masked' do
|
||||
context 'when variable is masked and expanded' do
|
||||
before do
|
||||
subject.masked = true
|
||||
subject.update!(masked: true, raw: false)
|
||||
end
|
||||
|
||||
it { is_expected.not_to allow_value('hello').for(:value) }
|
||||
|
|
@ -20,6 +20,53 @@ RSpec.describe Ci::Maskable do
|
|||
it { is_expected.to allow_value('helloworld').for(:value) }
|
||||
end
|
||||
|
||||
context 'when method :raw is not defined' do
|
||||
let(:test_var_class) do
|
||||
Struct.new(:masked?) do
|
||||
include ActiveModel::Validations
|
||||
include Ci::Maskable
|
||||
end
|
||||
end
|
||||
|
||||
let(:variable) { test_var_class.new }
|
||||
|
||||
it 'evaluates masked variables as expanded' do
|
||||
expect(subject).not_to be_masked_and_raw
|
||||
expect(subject).to be_masked_and_expanded
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the ci_remove_character_limitation_raw_masked_var FF is disabled' do
|
||||
context 'when variable is masked and raw' do
|
||||
before do
|
||||
subject.update!(masked: true, raw: true)
|
||||
stub_feature_flags(ci_remove_character_limitation_raw_masked_var: false)
|
||||
end
|
||||
|
||||
it { is_expected.not_to allow_value('hello').for(:value) }
|
||||
it { is_expected.not_to allow_value('hello world').for(:value) }
|
||||
it { is_expected.not_to allow_value('hello$VARIABLEworld').for(:value) }
|
||||
it { is_expected.not_to allow_value('hello\rworld').for(:value) }
|
||||
it { is_expected.not_to allow_value('hello&&&world').for(:value) }
|
||||
it { is_expected.not_to allow_value('helloworld!!!!').for(:value) }
|
||||
it { is_expected.to allow_value('helloworld').for(:value) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when variable is masked and raw' do
|
||||
before do
|
||||
subject.update!(masked: true, raw: true)
|
||||
end
|
||||
|
||||
it { is_expected.not_to allow_value('hello').for(:value) }
|
||||
it { is_expected.not_to allow_value('hello world').for(:value) }
|
||||
it { is_expected.to allow_value('hello\rworld').for(:value) }
|
||||
it { is_expected.to allow_value('hello$VARIABLEworld').for(:value) }
|
||||
it { is_expected.to allow_value('helloworld!!!').for(:value) }
|
||||
it { is_expected.to allow_value('hell******world').for(:value) }
|
||||
it { is_expected.to allow_value('helloworld123').for(:value) }
|
||||
end
|
||||
|
||||
context 'when variable is not masked' do
|
||||
before do
|
||||
subject.masked = false
|
||||
|
|
@ -33,40 +80,70 @@ RSpec.describe Ci::Maskable do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'REGEX' do
|
||||
subject { Ci::Maskable::REGEX }
|
||||
describe 'Regexes' do
|
||||
context 'with MASK_AND_RAW_REGEX' do
|
||||
subject { Ci::Maskable::MASK_AND_RAW_REGEX }
|
||||
|
||||
it 'does not match strings shorter than 8 letters' do
|
||||
expect(subject.match?('hello')).to eq(false)
|
||||
it 'does not match strings shorter than 8 letters' do
|
||||
expect(subject.match?('hello')).to eq(false)
|
||||
end
|
||||
|
||||
it 'does not match strings with spaces' do
|
||||
expect(subject.match?('hello world')).to eq(false)
|
||||
end
|
||||
|
||||
it 'does not match strings that span more than one line' do
|
||||
string = <<~EOS
|
||||
hello
|
||||
world
|
||||
EOS
|
||||
|
||||
expect(subject.match?(string)).to eq(false)
|
||||
end
|
||||
|
||||
it 'matches valid strings' do
|
||||
expect(subject.match?('hello$VARIABLEworld')).to eq(true)
|
||||
expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
|
||||
expect(subject.match?('hello\rworld')).to eq(true)
|
||||
expect(subject.match?('HelloWorld%#^')).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not match strings with spaces' do
|
||||
expect(subject.match?('hello world')).to eq(false)
|
||||
end
|
||||
context 'with REGEX' do
|
||||
subject { Ci::Maskable::REGEX }
|
||||
|
||||
it 'does not match strings with shell variables' do
|
||||
expect(subject.match?('hello$VARIABLEworld')).to eq(false)
|
||||
end
|
||||
it 'does not match strings shorter than 8 letters' do
|
||||
expect(subject.match?('hello')).to eq(false)
|
||||
end
|
||||
|
||||
it 'does not match strings with escape characters' do
|
||||
expect(subject.match?('hello\rworld')).to eq(false)
|
||||
end
|
||||
it 'does not match strings with spaces' do
|
||||
expect(subject.match?('hello world')).to eq(false)
|
||||
end
|
||||
|
||||
it 'does not match strings that span more than one line' do
|
||||
string = <<~EOS
|
||||
hello
|
||||
world
|
||||
EOS
|
||||
it 'does not match strings with shell variables' do
|
||||
expect(subject.match?('hello$VARIABLEworld')).to eq(false)
|
||||
end
|
||||
|
||||
expect(subject.match?(string)).to eq(false)
|
||||
end
|
||||
it 'does not match strings with escape characters' do
|
||||
expect(subject.match?('hello\rworld')).to eq(false)
|
||||
end
|
||||
|
||||
it 'does not match strings using unsupported characters' do
|
||||
expect(subject.match?('HelloWorld%#^')).to eq(false)
|
||||
end
|
||||
it 'does not match strings that span more than one line' do
|
||||
string = <<~EOS
|
||||
hello
|
||||
world
|
||||
EOS
|
||||
|
||||
it 'matches valid strings' do
|
||||
expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
|
||||
expect(subject.match?(string)).to eq(false)
|
||||
end
|
||||
|
||||
it 'does not match strings using unsupported characters' do
|
||||
expect(subject.match?('HelloWorld%#^')).to eq(false)
|
||||
end
|
||||
|
||||
it 'matches valid strings' do
|
||||
expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,16 @@ RSpec.describe IncidentManagement::TimelineEventTag do
|
|||
end
|
||||
|
||||
describe 'constants' do
|
||||
it { expect(described_class::START_TIME_TAG_NAME).to eq('Start time') }
|
||||
it { expect(described_class::END_TIME_TAG_NAME).to eq('End time') }
|
||||
it 'contains predefined tags' do
|
||||
expect(described_class::PREDEFINED_TAGS).to contain_exactly(
|
||||
'Start time',
|
||||
'End time',
|
||||
'Impact detected',
|
||||
'Response initiated',
|
||||
'Impact mitigated',
|
||||
'Cause identified'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#by_names scope' do
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ml::Candidate, factory_default: :keep do
|
||||
let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params) }
|
||||
let_it_be(:candidate2) { create(:ml_candidates, experiment: candidate.experiment) }
|
||||
RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops do
|
||||
let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params, name: 'candidate0') }
|
||||
let_it_be(:candidate2) do
|
||||
create(:ml_candidates, experiment: candidate.experiment, user: create(:user), name: 'candidate2')
|
||||
end
|
||||
|
||||
let_it_be(:candidate_artifact) do
|
||||
FactoryBot.create(:generic_package,
|
||||
|
|
@ -109,12 +111,12 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
|
|||
end
|
||||
|
||||
describe "#latest_metrics" do
|
||||
let_it_be(:candidate2) { create(:ml_candidates, experiment: candidate.experiment) }
|
||||
let!(:metric1) { create(:ml_candidate_metrics, candidate: candidate2) }
|
||||
let!(:metric2) { create(:ml_candidate_metrics, candidate: candidate2 ) }
|
||||
let!(:metric3) { create(:ml_candidate_metrics, name: metric1.name, candidate: candidate2) }
|
||||
let_it_be(:candidate3) { create(:ml_candidates, experiment: candidate.experiment) }
|
||||
let_it_be(:metric1) { create(:ml_candidate_metrics, candidate: candidate3) }
|
||||
let_it_be(:metric2) { create(:ml_candidate_metrics, candidate: candidate3 ) }
|
||||
let_it_be(:metric3) { create(:ml_candidate_metrics, name: metric1.name, candidate: candidate3) }
|
||||
|
||||
subject { candidate2.latest_metrics }
|
||||
subject { candidate3.latest_metrics }
|
||||
|
||||
it 'fetches only the last metric for the name' do
|
||||
expect(subject).to match_array([metric2, metric3] )
|
||||
|
|
@ -130,4 +132,55 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
|
|||
expect(subject.association_cached?(:user)).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#by_name' do
|
||||
let(:name) { candidate.name }
|
||||
|
||||
subject { described_class.by_name(name) }
|
||||
|
||||
context 'when name matches' do
|
||||
it 'gets the correct candidates' do
|
||||
expect(subject).to match_array([candidate])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when name matches partially' do
|
||||
let(:name) { 'andidate' }
|
||||
|
||||
it 'gets the correct candidates' do
|
||||
expect(subject).to match_array([candidate, candidate2])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when name does not match' do
|
||||
let(:name) { non_existing_record_id.to_s }
|
||||
|
||||
it 'does not fetch any candidate' do
|
||||
expect(subject).to match_array([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#order_by_metric' do
|
||||
let_it_be(:auc_metrics) do
|
||||
create(:ml_candidate_metrics, name: 'auc', value: 0.4, candidate: candidate)
|
||||
create(:ml_candidate_metrics, name: 'auc', value: 0.8, candidate: candidate2)
|
||||
end
|
||||
|
||||
let(:direction) { 'desc' }
|
||||
|
||||
subject { described_class.order_by_metric('auc', direction) }
|
||||
|
||||
it 'orders correctly' do
|
||||
expect(subject).to eq([candidate2, candidate])
|
||||
end
|
||||
|
||||
context 'when direction is asc' do
|
||||
let(:direction) { 'asc' }
|
||||
|
||||
it 'orders correctly' do
|
||||
expect(subject).to eq([candidate, candidate2])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
|
|||
let(:params) { basic_params.merge(id: experiment.iid, page: 's') }
|
||||
|
||||
it 'uses first page' do
|
||||
expect(assigns(:pagination)).to include(
|
||||
expect(assigns(:pagination_info)).to include(
|
||||
page: 1,
|
||||
is_last_page: false,
|
||||
per_page: 2,
|
||||
|
|
@ -108,6 +108,32 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'search' do
|
||||
let(:params) do
|
||||
basic_params.merge(
|
||||
id: experiment.iid,
|
||||
name: 'some_name',
|
||||
orderBy: 'name',
|
||||
orderByType: 'metric',
|
||||
sort: 'asc',
|
||||
invalid: 'invalid'
|
||||
)
|
||||
end
|
||||
|
||||
it 'formats and filters the parameters' do
|
||||
expect(Projects::Ml::CandidateFinder).to receive(:new).and_call_original do |exp, params|
|
||||
expect(params.to_h).to include({
|
||||
name: 'some_name',
|
||||
order_by: 'name',
|
||||
order_by_type: 'metric',
|
||||
sort: 'asc'
|
||||
})
|
||||
end
|
||||
|
||||
show_experiment
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not perform N+1 sql queries' do
|
||||
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { show_experiment }
|
||||
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
|
|||
occurred_at: Time.current,
|
||||
action: 'new comment',
|
||||
promoted_from_note: comment,
|
||||
timeline_event_tag_names: ['start time', 'end time']
|
||||
timeline_event_tag_names: ['start time', 'end time', 'Impact mitigated']
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -180,11 +180,11 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
|
|||
it 'matches the two tags on the event and creates on project' do
|
||||
result = execute.payload[:timeline_event]
|
||||
|
||||
expect(result.timeline_event_tags.count).to eq(2)
|
||||
expect(result.timeline_event_tags.by_names(['Start time', 'End time']).pluck_names)
|
||||
.to match_array(['Start time', 'End time'])
|
||||
expect(result.timeline_event_tags.count).to eq(3)
|
||||
expect(result.timeline_event_tags.by_names(['Start time', 'End time', 'Impact mitigated']).pluck_names)
|
||||
.to match_array(['Start time', 'End time', 'Impact mitigated'])
|
||||
expect(project.incident_management_timeline_event_tags.pluck_names)
|
||||
.to include('Start time', 'End time')
|
||||
.to include('Start time', 'End time', 'Impact mitigated')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -201,20 +201,22 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService, feature_catego
|
|||
{
|
||||
note: 'Updated note',
|
||||
occurred_at: occurred_at,
|
||||
timeline_event_tag_names: ['start time', 'end time']
|
||||
timeline_event_tag_names: ['start time', 'end time', 'response initiated']
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns the new tag in response' do
|
||||
timeline_event = execute.payload[:timeline_event]
|
||||
|
||||
expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly('Start time', 'End time')
|
||||
expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly(
|
||||
'Start time', 'End time', 'Response initiated')
|
||||
end
|
||||
|
||||
it 'creates the predefined tags on the project' do
|
||||
execute
|
||||
|
||||
expect(project.incident_management_timeline_event_tags.pluck_names).to include('Start time', 'End time')
|
||||
expect(project.incident_management_timeline_event_tags.pluck_names).to include(
|
||||
'Start time', 'End time', 'Response initiated')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -93,13 +93,10 @@ RSpec.describe Projects::PostCreationWorker do
|
|||
|
||||
context 'when project is created', :aggregate_failures do
|
||||
it 'creates tags for the project' do
|
||||
expect { subject }.to change { IncidentManagement::TimelineEventTag.count }.by(2)
|
||||
expect { subject }.to change { IncidentManagement::TimelineEventTag.count }.by(6)
|
||||
|
||||
expect(project.incident_management_timeline_event_tags.pluck_names).to match_array(
|
||||
[
|
||||
::IncidentManagement::TimelineEventTag::START_TIME_TAG_NAME,
|
||||
::IncidentManagement::TimelineEventTag::END_TIME_TAG_NAME
|
||||
]
|
||||
::IncidentManagement::TimelineEventTag::PREDEFINED_TAGS
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue