Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-13 21:11:25 +00:00
parent a5605d87fb
commit 5f36333180
78 changed files with 818 additions and 138 deletions

View File

@ -644,3 +644,16 @@ Cop/UserAdmin:
Performance/OpenStruct:
Exclude:
- 'ee/spec/**/*.rb'
# See https://gitlab.com/gitlab-org/gitlab/-/issues/327495
Style/RegexpLiteral:
Enabled: false
Style/RegexpLiteralMixedPreserve:
Enabled: true
SupportedStyles:
- slashes
- percent_r
- mixed
- mixed_preserve
EnforcedStyle: mixed_preserve

View File

@ -176,8 +176,6 @@ Rails/SaveBang:
- 'spec/lib/gitlab/database/custom_structure_spec.rb'
- 'spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb'
- 'spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb'
- 'spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
- 'spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb'
- 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
- 'spec/lib/gitlab/git_access_spec.rb'
- 'spec/lib/gitlab/import_export/avatar_saver_spec.rb'
@ -3332,3 +3330,60 @@ Gitlab/FeatureAvailableUsage:
- 'ee/spec/models/project_spec.rb'
- 'lib/api/helpers/related_resources_helpers.rb'
- 'spec/models/concerns/featurable_spec.rb'
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/327490
Style/RegexpLiteralMixedPreserve:
Exclude:
- 'app/controllers/projects/repositories_controller.rb'
- 'app/helpers/ci/variables_helper.rb'
- 'app/models/alert_management/alert.rb'
- 'app/models/application_setting.rb'
- 'app/models/blob_viewer/go_mod.rb'
- 'app/models/concerns/ci/maskable.rb'
- 'app/models/operations/feature_flag.rb'
- 'app/models/packages/go/module.rb'
- 'app/models/project_services/chat_message/base_message.rb'
- 'app/services/packages/conan/search_service.rb'
- 'app/services/projects/update_remote_mirror_service.rb'
- 'config/initializers/rspec_profiling.rb'
- 'ee/app/models/status_page/project_setting.rb'
- 'ee/app/presenters/vulnerability_presenter.rb'
- 'ee/lib/api/geo_nodes.rb'
- 'ee/lib/gitlab/vulnerabilities/standard_vulnerability.rb'
- 'ee/spec/controllers/concerns/ee/routable_actions/sso_enforcement_redirect_spec.rb'
- 'ee/spec/controllers/concerns/routable_actions_spec.rb'
- 'ee/spec/controllers/groups/groups_controller_spec.rb'
- 'ee/spec/features/groups/saml_enforcement_spec.rb'
- 'ee/spec/features/markdown/metrics_spec.rb'
- 'ee/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
- 'ee/spec/models/project_services/jira_service_spec.rb'
- 'ee/spec/services/jira/requests/issues/list_service_spec.rb'
- 'lib/api/invitations.rb'
- 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb'
- 'lib/gitlab/metrics/requests_rack_middleware.rb'
- 'lib/gitlab/metrics/subscribers/active_record.rb'
- 'lib/gitlab/regex.rb'
- 'lib/gitlab/utils.rb'
- 'lib/product_analytics/tracker.rb'
- 'qa/qa/page/project/settings/advanced.rb'
- 'qa/spec/service/docker_run/gitlab_runner_spec.rb'
- 'rubocop/cop/gitlab/duplicate_spec_location.rb'
- 'spec/features/clusters/cluster_health_dashboard_spec.rb'
- 'spec/features/markdown/metrics_spec.rb'
- 'spec/features/search/user_searches_for_code_spec.rb'
- 'spec/features/snippets/embedded_snippet_spec.rb'
- 'spec/helpers/diff_helper_spec.rb'
- 'spec/helpers/releases_helper_spec.rb'
- 'spec/lib/gitlab/ci/reports/test_case_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'
- 'spec/lib/gitlab/import_export/shared_spec.rb'
- 'spec/lib/gitlab/utils/usage_data_spec.rb'
- 'spec/presenters/ci/build_runner_presenter_spec.rb'
- 'spec/requests/api/projects_spec.rb'
- 'spec/services/jira/requests/projects/list_service_spec.rb'
- 'spec/support/capybara.rb'
- 'spec/support/helpers/grafana_api_helpers.rb'
- 'spec/support/helpers/query_recorder.rb'
- 'spec/support/helpers/require_migration.rb'
- 'spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb'
- 'spec/views/layouts/_head.html.haml_spec.rb'

View File

@ -922,13 +922,6 @@ Style/RedundantRegexpEscape:
Style/RedundantSelf:
Enabled: false
# Offense count: 213
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Enabled: false
# Offense count: 53
# Cop supports --auto-correct.
Style/RescueModifier:

View File

@ -87,7 +87,7 @@ export default {
@input="searchMergeRequests"
@removeToken="setSearchType(null)"
/>
<gl-icon :size="18" name="search" class="ml-3 input-icon" use-deprecated-sizes />
<gl-icon :size="16" name="search" class="ml-3 input-icon" />
</label>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
@ -105,7 +105,7 @@ export default {
@click.stop="setSearchType(searchType)"
>
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
<gl-icon :size="18" name="search" use-deprecated-sizes />
<gl-icon :size="16" name="search" />
</span>
<span>{{ searchType.label }}</span>
</button>

View File

@ -12,6 +12,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['namespace'],
props: {
memberId: {
type: Number,
@ -19,7 +20,11 @@ export default {
},
},
computed: {
...mapState(['memberPath']),
...mapState({
memberPath(state) {
return state[this.namespace].memberPath;
},
}),
approvePath() {
return this.memberPath.replace(/:id$/, `${this.memberId}/approve_access_request`);
},

View File

@ -12,6 +12,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['namespace'],
props: {
groupLink: {
type: Object,
@ -19,7 +20,11 @@ export default {
},
},
methods: {
...mapActions(['showRemoveGroupLinkModal']),
...mapActions({
showRemoveGroupLinkModal(dispatch, payload) {
return dispatch(`${this.namespace}/showRemoveGroupLinkModal`, payload);
},
}),
},
};
</script>

View File

@ -8,6 +8,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['namespace'],
props: {
memberId: {
type: Number,
@ -43,7 +44,11 @@ export default {
},
},
computed: {
...mapState(['memberPath']),
...mapState({
memberPath(state) {
return state[this.namespace].memberPath;
},
}),
computedMemberPath() {
return this.memberPath.replace(':id', this.memberId);
},

View File

@ -12,6 +12,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['namespace'],
props: {
memberId: {
type: Number,
@ -19,7 +20,11 @@ export default {
},
},
computed: {
...mapState(['memberPath']),
...mapState({
memberPath(state) {
return state[this.namespace].memberPath;
},
}),
resendPath() {
return this.memberPath.replace(/:id$/, `${this.memberId}/resend_invite`);
},

View File

@ -9,8 +9,16 @@ import MembersTable from './table/members_table.vue';
export default {
name: 'MembersApp',
components: { MembersTable, FilterSortContainer, GlAlert },
inject: ['namespace'],
computed: {
...mapState(['showError', 'errorMessage']),
...mapState({
showError(state) {
return state[this.namespace].showError;
},
errorMessage(state) {
return state[this.namespace].errorMessage;
},
}),
},
watch: {
showError(value) {
@ -23,7 +31,9 @@ export default {
},
methods: {
...mapMutations({
hideError: HIDE_ERROR,
hideError(commit) {
return commit(`${this.namespace}/${HIDE_ERROR}`);
},
}),
},
};

View File

@ -6,8 +6,16 @@ import SortDropdown from './sort_dropdown.vue';
export default {
name: 'FilterSortContainer',
components: { MembersFilteredSearchBar, SortDropdown },
inject: ['namespace'],
computed: {
...mapState(['filteredSearchBar', 'tableSortableFields']),
...mapState({
filteredSearchBar(state) {
return state[this.namespace].filteredSearchBar;
},
tableSortableFields(state) {
return state[this.namespace].tableSortableFields;
},
}),
showContainer() {
return this.filteredSearchBar.show || this.showSortDropdown;
},

View File

@ -37,14 +37,18 @@ export default {
],
},
],
inject: ['sourceId', 'canManageMembers'],
inject: ['namespace', 'sourceId', 'canManageMembers'],
data() {
return {
initialFilterValue: [],
};
},
computed: {
...mapState(['filteredSearchBar']),
...mapState({
filteredSearchBar(state) {
return state[this.namespace].filteredSearchBar;
},
}),
tokens() {
return this.$options.availableTokens.filter((token) => {
if (

View File

@ -8,8 +8,16 @@ import { parseSortParam, buildSortHref } from '~/members/utils';
export default {
name: 'SortDropdown',
components: { GlSorting, GlSortingItem },
inject: ['namespace'],
computed: {
...mapState(['tableSortableFields', 'filteredSearchBar']),
...mapState({
tableSortableFields(state) {
return state[this.namespace].tableSortableFields;
},
filteredSearchBar(state) {
return state[this.namespace].filteredSearchBar;
},
}),
sort() {
return parseSortParam(this.tableSortableFields);
},

View File

@ -23,6 +23,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['namespace'],
props: {
member: {
type: Object,
@ -30,7 +31,11 @@ export default {
},
},
computed: {
...mapState(['memberPath']),
...mapState({
memberPath(state) {
return state[this.namespace].memberPath;
},
}),
leavePath() {
return this.memberPath.replace(/:id$/, 'leave');
},

View File

@ -22,8 +22,19 @@ export default {
},
modalId: REMOVE_GROUP_LINK_MODAL_ID,
components: { GlModal, GlSprintf, GlForm },
inject: ['namespace'],
computed: {
...mapState(['memberPath', 'groupLinkToRemove', 'removeGroupLinkModalVisible']),
...mapState({
memberPath(state) {
return state[this.namespace].memberPath;
},
groupLinkToRemove(state) {
return state[this.namespace].groupLinkToRemove;
},
removeGroupLinkModalVisible(state) {
return state[this.namespace].removeGroupLinkModalVisible;
},
}),
groupLinkPath() {
return this.memberPath.replace(/:id$/, this.groupLinkToRemove?.id);
},
@ -35,7 +46,11 @@ export default {
},
},
methods: {
...mapActions(['hideRemoveGroupLinkModal']),
...mapActions({
hideRemoveGroupLinkModal(dispatch) {
return dispatch(`${this.namespace}/hideRemoveGroupLinkModal`);
},
}),
handlePrimary() {
this.$refs.form.$el.submit();
},

View File

@ -7,6 +7,7 @@ import { s__ } from '~/locale';
export default {
name: 'ExpirationDatepicker',
components: { GlDatepicker },
inject: ['namespace'],
props: {
member: {
type: Object,
@ -46,7 +47,11 @@ export default {
}
},
methods: {
...mapActions(['updateMemberExpiration']),
...mapActions({
updateMemberExpiration(dispatch, payload) {
return dispatch(`${this.namespace}/updateMemberExpiration`, payload);
},
}),
handleInput(date) {
this.busy = true;
this.updateMemberExpiration({

View File

@ -31,9 +31,19 @@ export default {
LdapOverrideConfirmationModal: () =>
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
inject: ['currentUserId'],
inject: ['namespace', 'currentUserId'],
computed: {
...mapState(['members', 'tableFields', 'tableAttrs']),
...mapState({
members(state) {
return state[this.namespace].members;
},
tableFields(state) {
return state[this.namespace].tableFields;
},
tableAttrs(state) {
return state[this.namespace].tableAttrs;
},
}),
filteredFields() {
return FIELDS.filter(
(field) => this.tableFields.includes(field.key) && this.showField(field),

View File

@ -11,6 +11,7 @@ export default {
GlDropdownItem,
LdapDropdownItem: () => import('ee_component/members/components/ldap/ldap_dropdown_item.vue'),
},
inject: ['namespace'],
props: {
member: {
type: Object,
@ -44,7 +45,11 @@ export default {
}
},
methods: {
...mapActions(['updateMemberRole']),
...mapActions({
updateMemberRole(dispatch, payload) {
return dispatch(`${this.namespace}/updateMemberRole`, payload);
},
}),
handleSelect(value, name) {
if (value === this.member.accessLevel.integerValue) {
return;

View File

@ -8,6 +8,7 @@ import membersStore from './store';
export const initMembersApp = (
el,
{
namespace,
tableFields = [],
tableAttrs = {},
tableSortableFields = [],
@ -24,22 +25,25 @@ export const initMembersApp = (
const { sourceId, canManageMembers, ...vuexStoreAttributes } = parseDataAttributes(el);
const store = new Vuex.Store(
membersStore({
...vuexStoreAttributes,
tableFields,
tableAttrs,
tableSortableFields,
requestFormatter,
filteredSearchBar,
}),
);
const store = new Vuex.Store({
modules: {
[namespace]: membersStore({
...vuexStoreAttributes,
tableFields,
tableAttrs,
tableSortableFields,
requestFormatter,
filteredSearchBar,
}),
},
});
return new Vue({
el,
components: { App },
store,
provide: {
namespace,
currentUserId: gon.current_user_id || null,
sourceId,
canManageMembers,

View File

@ -3,6 +3,7 @@ import mutations from 'ee_else_ce/members/store/mutations';
import createState from 'ee_else_ce/members/store/state';
export default (initialState) => ({
namespaced: true,
state: createState(initialState),
actions,
mutations,

View File

@ -8,6 +8,7 @@ import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigg
import { s__ } from '~/locale';
import memberExpirationDate from '~/member_expiration_date';
import { initMembersApp } from '~/members';
import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import UsersSelect from '~/users_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
@ -29,6 +30,7 @@ function mountRemoveMemberModal() {
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-group-members-list'), {
namespace: MEMBER_TYPES.user,
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
@ -43,6 +45,7 @@ initMembersApp(document.querySelector('.js-group-members-list'), {
});
initMembersApp(document.querySelector('.js-group-group-links-list'), {
namespace: MEMBER_TYPES.group,
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
@ -51,6 +54,7 @@ initMembersApp(document.querySelector('.js-group-group-links-list'), {
requestFormatter: groupLinkRequestFormatter,
});
initMembersApp(document.querySelector('.js-group-invited-members-list'), {
namespace: MEMBER_TYPES.invite,
tableFields: SHARED_FIELDS.concat('invited'),
requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
@ -62,6 +66,7 @@ initMembersApp(document.querySelector('.js-group-invited-members-list'), {
},
});
initMembersApp(document.querySelector('.js-group-access-requests-list'), {
namespace: MEMBER_TYPES.accessRequest,
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: groupMemberRequestFormatter,
});

View File

@ -7,6 +7,7 @@ import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigg
import { s__ } from '~/locale';
import memberExpirationDate from '~/member_expiration_date';
import { initMembersApp } from '~/members';
import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import { projectMemberRequestFormatter } from '~/projects/members/utils';
import UsersSelect from '~/users_select';
@ -42,6 +43,7 @@ new UsersSelect(); // eslint-disable-line no-new
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-project-members-list'), {
namespace: MEMBER_TYPES.user,
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
@ -56,6 +58,7 @@ initMembersApp(document.querySelector('.js-project-members-list'), {
});
initMembersApp(document.querySelector('.js-project-group-links-list'), {
namespace: MEMBER_TYPES.group,
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
@ -72,11 +75,13 @@ initMembersApp(document.querySelector('.js-project-group-links-list'), {
});
initMembersApp(document.querySelector('.js-project-invited-members-list'), {
namespace: MEMBER_TYPES.invite,
tableFields: SHARED_FIELDS.concat('invited'),
requestFormatter: projectMemberRequestFormatter,
});
initMembersApp(document.querySelector('.js-project-access-requests-list'), {
namespace: MEMBER_TYPES.accessRequest,
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: projectMemberRequestFormatter,
});

View File

@ -47,10 +47,15 @@ export default class PerformanceBarStore {
}
canTrackRequest(requestUrl) {
return (
requestUrl.endsWith('/api/graphql') ||
this.requests.filter((request) => request.url === requestUrl).length < 2
);
// We want to store at most 2 unique requests per URL, as additional
// requests to the same URL probably aren't very interesting.
//
// GraphQL requests are the exception: because all GraphQL requests
// go to the same URL, we set a higher limit of 10 to allow
// capturing different queries a page may make.
const requestsLimit = requestUrl.endsWith('/api/graphql') ? 10 : 2;
return this.requests.filter((request) => request.url === requestUrl).length < requestsLimit;
}
static truncateUrl(requestUrl) {

View File

@ -15,4 +15,7 @@ export const STAGE_VIEW = 'stage';
export const LAYER_VIEW = 'layer';
export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
export const SINGLE_JOB = 'single_job';
export const JOB_DROPDOWN = 'job_dropdown';
export const IID_FAILURE = 'missing_iid';

View File

@ -1,5 +1,6 @@
<script>
import { reportToSentry } from '../../utils';
import { JOB_DROPDOWN, SINGLE_JOB } from './constants';
import JobItem from './job_item.vue';
/**
@ -28,6 +29,10 @@ export default {
default: '',
},
},
jobItemTypes: {
jobDropdown: JOB_DROPDOWN,
singleJob: SINGLE_JOB,
},
computed: {
computedJobId() {
return this.pipelineId > -1 ? `${this.group.name}-${this.pipelineId}` : '';
@ -57,11 +62,10 @@ export default {
>
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
<job-item
:dropdown-length="group.size"
:type="$options.jobItemTypes.jobDropdown"
:group-tooltip="tooltipText"
:job="group"
:stage-name="stageName"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
<div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div>
@ -75,6 +79,7 @@ export default {
<job-item
:dropdown-length="group.size"
:job="job"
:type="$options.jobItemTypes.singleJob"
css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>

View File

@ -8,7 +8,7 @@ import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue';
import { accessValue } from './accessors';
import { REST } from './constants';
import { REST, SINGLE_JOB } from './constants';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@ -97,6 +97,11 @@ export default {
required: false,
default: '',
},
type: {
type: String,
required: false,
default: SINGLE_JOB,
},
},
computed: {
boundary() {
@ -111,6 +116,9 @@ export default {
hasDetails() {
return accessValue(this.dataMethod, 'hasDetails', this.status);
},
isSingleItem() {
return this.type === SINGLE_JOB;
},
nameComponent() {
return this.hasDetails ? 'gl-link' : 'div';
},
@ -177,6 +185,17 @@ export default {
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
jobItemClick(evt) {
if (this.isSingleItem) {
/*
This is so the jobDropdown still toggles. Issue to refactor:
https://gitlab.com/gitlab-org/gitlab/-/issues/267117
*/
evt.stopPropagation();
}
this.hideTooltips();
},
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
},
@ -201,7 +220,7 @@ export default {
:href="detailsPath"
class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
:data-testid="testId"
@click.stop="hideTooltips"
@click="jobItemClick"
@mouseout="hideTooltips"
>
<div class="ci-job-name-component gl-display-flex gl-align-items-center">

View File

@ -1,6 +1,6 @@
%p.text-center
%span.light
Already have login and password?
= _('Already have login and password?')
- path_params = { redirect_to_referer: 'yes' }
- path_params[:invite_email] = @invite_email if @invite_email.present?
= link_to "Sign in", new_session_path(:user, path_params)
= link_to _('Sign in'), new_session_path(:user, path_params)

View File

@ -1,4 +1,4 @@
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
- current_route_path = request.fullpath.match(%r{-/tree/[^/]+/(.+$)}).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
- @content_class = "limit-container-width" unless fluid_layout
- @skip_current_level_breadcrumb = true

View File

@ -1,4 +1,4 @@
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
- current_route_path = request.fullpath.match(%r{-/tree/[^/]+/(.+$)}).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
- add_page_startup_graphql_call('repository/permissions', { projectPath: @project.full_path })
- add_page_startup_graphql_call('repository/files', { nextPageCursor: "", pageSize: 100, projectPath: @project.full_path, ref: current_ref, path: current_route_path || "/"})

View File

@ -1,7 +1,7 @@
- classes = local_assigns.fetch(:classes, '')
%span.js-filepicker
%button.gl-button.btn.js-filepicker-button{ type: 'button', class: classes }= _("Choose file…")
%button.gl-button.btn.btn-default.js-filepicker-button{ type: 'button', class: classes }= _("Choose file…")
%span.file_name.js-filepicker-filename= _("No file chosen.")
= f.file_field field, class: "js-filepicker-input hidden"
- if help_text.present?

View File

@ -0,0 +1,5 @@
---
title: Add a migration to insert trail plans within SAAS for Ultimate and Premium plans
merge_request: 57814
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Externalize strings in shared/_sign_in_link.html.haml
merge_request: 58283
author: nuwe1
type: other

View File

@ -0,0 +1,5 @@
---
title: Add btn-default class for file picker button
merge_request: 58238
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang Rubocop offenses for email handlers
merge_request: 58095
author: Huzaifa Iftikhar @huzaifaiftikhar
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Limit number of GraphQL requests tracked in performance bar to 10
merge_request: 59158
author:
type: performance

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
class AddNewTrailPlans < ActiveRecord::Migration[6.0]
class Plan < ActiveRecord::Base
self.inheritance_column = :_type_disabled
has_one :limits, class_name: 'PlanLimits'
def actual_limits
self.limits || self.build_limits
end
end
class PlanLimits < ActiveRecord::Base
self.inheritance_column = :_type_disabled
belongs_to :plan
end
def create_plan_limits(plan_limit_name, plan)
plan_limit = Plan.find_or_initialize_by(name: plan_limit_name).actual_limits.dup
plan_limit.plan = plan
plan_limit.save!
end
def up
return unless Gitlab.dev_env_or_com?
ultimate_trial = Plan.create!(name: 'ultimate_trial', title: 'Ultimate Trial')
premium_trial = Plan.create!(name: 'premium_trial', title: 'Premium Trial')
create_plan_limits('gold', ultimate_trial)
create_plan_limits('silver', premium_trial)
end
def down
return unless Gitlab.dev_env_or_com?
Plan.where(name: %w(ultimate_trial premium_trial)).delete_all
end
end

View File

@ -0,0 +1 @@
b40c702ea6b2120da6fe11b213064a7a124dbc86bfb2d6785bfd2274c44f1e22

View File

@ -99,7 +99,7 @@ GitLab CI/CD uses a number of concepts to describe and run your build and deploy
| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. |
| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own runners to execute your scripts. |
| [Pipeline efficiency](pipelines/pipeline_efficiency.md) | Configure your pipelines to run quickly and efficiently. |
| [Test cases](test_cases/index.md) | Configure your pipelines to run quickly and efficiently. |
| [Test cases](test_cases/index.md) | Configure your pipelines to run quickly and efficiently. <!--- this seems to be a duplicate description ---> |
## Configuration

View File

@ -45,7 +45,7 @@ There are 3 ways to resolve an abuse report, with a button for each method:
The following is an example of the **Abuse Reports** page:
![abuse-reports-page-image](img/abuse_reports_page.png)
![abuse-reports-page-image](img/abuse_reports_page_v13_11.png)
### Blocking users

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -24,11 +24,11 @@ Initially, no data appears. Data is populated as users comment on open merge req
Code Review Analytics is available to users with Reporter access and above, and displays a table of open merge requests that have at least one non-author comment. The review time is measured from the time the first non-author comment was submitted.
To access Code Review Analytics, from your project's menu, go to **Project Analytics > Code Review**.
To access Code Review Analytics, from your project's menu, go to **Analytics > Code Review**.
You can filter the list of merge requests by milestone and label.
![Code Review Analytics](img/code_review_analytics_v12_8.png "List of code reviews; oldest review first.")
![Code Review Analytics](img/code_review_analytics_v13_11.png "List of code reviews; oldest review first.")
The table is sorted by:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -13,7 +13,7 @@ Issue Analytics is a bar graph which illustrates the number of issues created ea
The default time span is 13 months, which includes the current month, and the 12 months
prior.
To access the chart, navigate to your project sidebar and select **{chart}** **Analytics > Issue Analytics**.
To access the chart, navigate to your project sidebar and select **Analytics > Issue**.
Hover over each bar to see the total number of issues.
@ -31,7 +31,7 @@ You can change the total number of months displayed by setting a URL parameter.
For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
shows a total of 15 months for the chart in the GitLab.org group.
![Issues created per month](img/issues_created_per_month_v12_8.png)
![Issues created per month](img/issues_created_per_month_v13_11.png)
## Drill into the information

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@ -17,7 +17,7 @@ for merging into production.
To access the Compliance Dashboard for a group, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu.
![Compliance Dashboard](img/compliance_dashboard_v13_6.png)
![Compliance Dashboard](img/compliance_dashboard_v13_11.png)
NOTE:
The Compliance Dashboard shows only the latest MR on each project.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -13,14 +13,12 @@ Configure the Insights that matter for your groups to explore data such as
triage hygiene, issues created/closed per a given period, average time for merge
requests to be merged and much more.
![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png)
![Insights example stacked bar chart](img/insights_example_stacked_bar_chart_v13_11.png)
## View your group's Insights
You can access your group's Insights by clicking the **Analytics > Insights**
link in the left sidebar:
![Insights sidebar link](img/insights_sidebar_link_v12_8.png)
link in the left sidebar.
## Configure your Insights

View File

@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# GitLab Container Registry
# GitLab Container Registry **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4040) in GitLab 8.8.
> - Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker

View File

@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Packages & Registries
# Packages and Registries **(FREE)**
The GitLab [Package Registry](package_registry/index.md) acts as a private or public registry
for a variety of common package managers. You can publish and share

View File

@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Ruby gems in the Package Registry
# Ruby gems in the Package Registry **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/803) in [GitLab Free](https://about.gitlab.com/pricing/) 13.10.

View File

@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Store all of your packages in one GitLab project
# Store all of your packages in one GitLab project **(FREE)**
You can store all of your packages in one project's Package Registry. Rather than using
a GitLab repository to store code, you can use the repository to store all your packages.

View File

@ -333,7 +333,7 @@ project and should only have access to that project.
External users:
- Cannot create projects (including forks), groups, or snippets.
- Can only create projects (including forks), subgroups, and snippets within the top-level group to which they belong.
- Can only access public projects and projects to which they are explicitly granted access,
thus hiding all other internal or private ones from them (like being
logged out).

View File

@ -3266,6 +3266,9 @@ msgstr ""
msgid "Already blocked"
msgstr ""
msgid "Already have login and password?"
msgstr ""
msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
msgstr ""

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# This cop is based on `Style/RegexpLiteral` but adds a new
# `EnforcedStyle` option `mixed_preserve`.
#
# This cop will be removed once the upstream PR is merged and RuboCop upgraded.
#
# See https://github.com/rubocop/rubocop/pull/9688
class RegexpLiteralMixedPreserve < RuboCop::Cop::Style::RegexpLiteral
module Patch
private
def allowed_slash_literal?(node)
super || allowed_mixed_preserve?(node)
end
def allowed_percent_r_literal?(node)
super || allowed_mixed_preserve?(node)
end
def allowed_mixed_preserve?(node)
style == :mixed_preserve && !contains_disallowed_slash?(node)
end
end
prepend Patch
end
end
end
end

View File

@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ApproveAccessRequestButton from '~/members/components/action_buttons/approve_access_request_button.vue';
import { MEMBER_TYPES } from '~/members/constants';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@ -14,9 +15,14 @@ describe('ApproveAccessRequestButton', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
memberPath: '/groups/foo-bar/-/group_members/:id',
...state,
modules: {
[MEMBER_TYPES.accessRequest]: {
namespaced: true,
state: {
memberPath: '/groups/foo-bar/-/group_members/:id',
...state,
},
},
},
});
};
@ -25,6 +31,9 @@ describe('ApproveAccessRequestButton', () => {
wrapper = shallowMount(ApproveAccessRequestButton, {
localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.accessRequest,
},
propsData: {
memberId: 1,
...propsData,

View File

@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveGroupLinkButton from '~/members/components/action_buttons/remove_group_link_button.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { group } from '../../mock_data';
const localVue = createLocalVue();
@ -17,7 +18,12 @@ describe('RemoveGroupLinkButton', () => {
const createStore = () => {
return new Vuex.Store({
actions,
modules: {
[MEMBER_TYPES.group]: {
namespaced: true,
actions,
},
},
});
};
@ -25,6 +31,9 @@ describe('RemoveGroupLinkButton', () => {
wrapper = mount(RemoveGroupLinkButton, {
localVue,
store: createStore(),
provide: {
namespace: MEMBER_TYPES.group,
},
propsData: {
groupLink: group,
},

View File

@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
import { MEMBER_TYPES } from '~/members/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
@ -11,9 +12,14 @@ describe('RemoveMemberButton', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
memberPath: '/groups/foo-bar/-/group_members/:id',
...state,
modules: {
[MEMBER_TYPES.user]: {
namespaced: true,
state: {
memberPath: '/groups/foo-bar/-/group_members/:id',
...state,
},
},
},
});
};
@ -22,6 +28,9 @@ describe('RemoveMemberButton', () => {
wrapper = shallowMount(RemoveMemberButton, {
localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.user,
},
propsData: {
memberId: 1,
memberType: 'GroupMember',

View File

@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ResendInviteButton from '~/members/components/action_buttons/resend_invite_button.vue';
import { MEMBER_TYPES } from '~/members/constants';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@ -14,9 +15,14 @@ describe('ResendInviteButton', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
memberPath: '/groups/foo-bar/-/group_members/:id',
...state,
modules: {
[MEMBER_TYPES.invite]: {
namespaced: true,
state: {
memberPath: '/groups/foo-bar/-/group_members/:id',
...state,
},
},
},
});
};
@ -25,6 +31,9 @@ describe('ResendInviteButton', () => {
wrapper = shallowMount(ResendInviteButton, {
localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.invite,
},
propsData: {
memberId: 1,
...propsData,

View File

@ -5,6 +5,7 @@ import Vuex from 'vuex';
import * as commonUtils from '~/lib/utils/common_utils';
import MembersApp from '~/members/components/app.vue';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations';
@ -17,16 +18,24 @@ describe('MembersApp', () => {
const createComponent = (state = {}, options = {}) => {
store = new Vuex.Store({
state: {
showError: true,
errorMessage: 'Something went wrong, please try again.',
...state,
modules: {
[MEMBER_TYPES.user]: {
namespaced: true,
state: {
showError: true,
errorMessage: 'Something went wrong, please try again.',
...state,
},
mutations,
},
},
mutations,
});
wrapper = shallowMount(MembersApp, {
localVue,
provide: {
namespace: MEMBER_TYPES.user,
},
store,
...options,
});
@ -48,7 +57,9 @@ describe('MembersApp', () => {
it('renders and scrolls to error alert', async () => {
createComponent({ showError: false, errorMessage: '' });
store.commit(RECEIVE_MEMBER_ROLE_ERROR, { error: new Error('Network Error') });
store.commit(`${MEMBER_TYPES.user}/${RECEIVE_MEMBER_ROLE_ERROR}`, {
error: new Error('Network Error'),
});
await nextTick();
@ -66,7 +77,7 @@ describe('MembersApp', () => {
it('does not render and scroll to error alert', async () => {
createComponent();
store.commit(HIDE_ERROR);
store.commit(`${MEMBER_TYPES.user}/${HIDE_ERROR}`);
await nextTick();

View File

@ -3,6 +3,7 @@ import Vuex from 'vuex';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
@ -12,22 +13,30 @@ describe('FilterSortContainer', () => {
const createComponent = (state) => {
const store = new Vuex.Store({
state: {
filteredSearchBar: {
show: true,
tokens: ['two_factor'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
modules: {
[MEMBER_TYPES.user]: {
namespaced: true,
state: {
filteredSearchBar: {
show: true,
tokens: ['two_factor'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
},
tableSortableFields: ['account'],
...state,
},
},
tableSortableFields: ['account'],
...state,
},
});
wrapper = shallowMount(FilterSortContainer, {
localVue,
store,
provide: {
namespace: MEMBER_TYPES.user,
},
});
};

View File

@ -2,6 +2,7 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import { MEMBER_TYPES } from '~/members/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
const localVue = createLocalVue();
@ -12,15 +13,20 @@ describe('MembersFilteredSearchBar', () => {
const createComponent = ({ state = {}, provide = {} } = {}) => {
const store = new Vuex.Store({
state: {
filteredSearchBar: {
show: true,
tokens: ['two_factor'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
modules: {
[MEMBER_TYPES.user]: {
namespaced: true,
state: {
filteredSearchBar: {
show: true,
tokens: ['two_factor'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
},
...state,
},
},
...state,
},
});
@ -29,6 +35,7 @@ describe('MembersFilteredSearchBar', () => {
provide: {
sourceId: 1,
canManageMembers: true,
namespace: MEMBER_TYPES.user,
...provide,
},
store,

View File

@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import * as urlUtilities from '~/lib/utils/url_utility';
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
@ -14,16 +15,21 @@ describe('SortDropdown', () => {
const createComponent = (state) => {
const store = new Vuex.Store({
state: {
tableSortableFields: ['account', 'granted', 'expires', 'maxRole', 'lastSignIn'],
filteredSearchBar: {
show: true,
tokens: ['two_factor'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
modules: {
[MEMBER_TYPES.user]: {
namespaced: true,
state: {
tableSortableFields: ['account', 'granted', 'expires', 'maxRole', 'lastSignIn'],
filteredSearchBar: {
show: true,
tokens: ['two_factor'],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
},
...state,
},
},
...state,
},
});
@ -31,6 +37,7 @@ describe('SortDropdown', () => {
localVue,
provide: {
sourceId: 1,
namespace: MEMBER_TYPES.user,
},
store,
});

View File

@ -4,7 +4,7 @@ import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import LeaveModal from '~/members/components/modals/leave_modal.vue';
import { LEAVE_MODAL_ID } from '~/members/constants';
import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
import { member } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@ -17,9 +17,14 @@ describe('LeaveModal', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
memberPath: '/groups/foo-bar/-/group_members/:id',
...state,
modules: {
[MEMBER_TYPES.user]: {
namespaced: true,
state: {
memberPath: '/groups/foo-bar/-/group_members/:id',
...state,
},
},
},
});
};
@ -28,6 +33,9 @@ describe('LeaveModal', () => {
wrapper = mount(LeaveModal, {
localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.user,
},
propsData: {
member,
...propsData,

View File

@ -4,7 +4,7 @@ import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import RemoveGroupLinkModal from '~/members/components/modals/remove_group_link_modal.vue';
import { REMOVE_GROUP_LINK_MODAL_ID } from '~/members/constants';
import { REMOVE_GROUP_LINK_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
import { group } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@ -21,13 +21,18 @@ describe('RemoveGroupLinkModal', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
memberPath: '/groups/foo-bar/-/group_links/:id',
groupLinkToRemove: group,
removeGroupLinkModalVisible: true,
...state,
modules: {
[MEMBER_TYPES.group]: {
namespaced: true,
state: {
memberPath: '/groups/foo-bar/-/group_links/:id',
groupLinkToRemove: group,
removeGroupLinkModalVisible: true,
...state,
},
actions,
},
},
actions,
});
};
@ -35,6 +40,9 @@ describe('RemoveGroupLinkModal', () => {
wrapper = mount(RemoveGroupLinkModal, {
localVue,
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.group,
},
attrs: {
static: true,
},

View File

@ -5,6 +5,7 @@ import Vuex from 'vuex';
import { useFakeDate } from 'helpers/fake_date';
import waitForPromises from 'helpers/wait_for_promises';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { member } from '../../mock_data';
const localVue = createLocalVue();
@ -31,7 +32,11 @@ describe('ExpirationDatepicker', () => {
),
};
return new Vuex.Store({ actions });
return new Vuex.Store({
modules: {
[MEMBER_TYPES.user]: { namespaced: true, actions },
},
});
};
const createComponent = (propsData = {}) => {
@ -41,6 +46,9 @@ describe('ExpirationDatepicker', () => {
permissions: { canUpdate: true },
...propsData,
},
provide: {
namespace: MEMBER_TYPES.user,
},
localVue,
store: createStore(),
mocks: {

View File

@ -14,6 +14,7 @@ import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
import * as initUserPopovers from '~/user_popovers';
import { member as memberMock, directMember, invite, accessRequest } from '../../mock_data';
@ -25,14 +26,19 @@ describe('MembersTable', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
members: [],
tableFields: [],
tableAttrs: {
table: { 'data-qa-selector': 'members_list' },
tr: { 'data-qa-selector': 'member_row' },
modules: {
[MEMBER_TYPES.user]: {
namespaced: true,
state: {
members: [],
tableFields: [],
tableAttrs: {
table: { 'data-qa-selector': 'members_list' },
tr: { 'data-qa-selector': 'member_row' },
},
...state,
},
},
...state,
},
});
};
@ -44,6 +50,7 @@ describe('MembersTable', () => {
provide: {
sourceId: 1,
currentUserId: 1,
namespace: MEMBER_TYPES.user,
...provide,
},
stubs: [

View File

@ -7,6 +7,7 @@ import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { BV_DROPDOWN_SHOW } from '~/lib/utils/constants';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { member } from '../../mock_data';
const localVue = createLocalVue();
@ -24,11 +25,18 @@ describe('RoleDropdown', () => {
updateMemberRole: jest.fn(() => Promise.resolve()),
};
return new Vuex.Store({ actions });
return new Vuex.Store({
modules: {
[MEMBER_TYPES.user]: { namespaced: true, actions },
},
});
};
const createComponent = (propsData = {}) => {
wrapper = mount(RoleDropdown, {
provide: {
namespace: MEMBER_TYPES.user,
},
propsData: {
member,
permissions: {},

View File

@ -1,5 +1,6 @@
import { createWrapper } from '@vue/test-utils';
import MembersApp from '~/members/components/app.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { initMembersApp } from '~/members/index';
import { membersJsonString, members } from './mock_data';
@ -10,6 +11,7 @@ describe('initMembersApp', () => {
const setup = () => {
vm = initMembersApp(el, {
namespace: MEMBER_TYPES.user,
tableFields: ['account'],
tableAttrs: { table: { 'data-qa-selector': 'members_list' } },
tableSortableFields: ['account'],
@ -45,42 +47,46 @@ describe('initMembersApp', () => {
it('parses and sets `members` in Vuex store', () => {
setup();
expect(vm.$store.state.members).toEqual(members);
expect(vm.$store.state[MEMBER_TYPES.user].members).toEqual(members);
});
it('sets `tableFields` in Vuex store', () => {
setup();
expect(vm.$store.state.tableFields).toEqual(['account']);
expect(vm.$store.state[MEMBER_TYPES.user].tableFields).toEqual(['account']);
});
it('sets `tableAttrs` in Vuex store', () => {
setup();
expect(vm.$store.state.tableAttrs).toEqual({ table: { 'data-qa-selector': 'members_list' } });
expect(vm.$store.state[MEMBER_TYPES.user].tableAttrs).toEqual({
table: { 'data-qa-selector': 'members_list' },
});
});
it('sets `tableSortableFields` in Vuex store', () => {
setup();
expect(vm.$store.state.tableSortableFields).toEqual(['account']);
expect(vm.$store.state[MEMBER_TYPES.user].tableSortableFields).toEqual(['account']);
});
it('sets `requestFormatter` in Vuex store', () => {
setup();
expect(vm.$store.state.requestFormatter()).toEqual({});
expect(vm.$store.state[MEMBER_TYPES.user].requestFormatter()).toEqual({});
});
it('sets `filteredSearchBar` in Vuex store', () => {
setup();
expect(vm.$store.state.filteredSearchBar).toEqual({ show: false });
expect(vm.$store.state[MEMBER_TYPES.user].filteredSearchBar).toEqual({ show: false });
});
it('sets `memberPath` in Vuex store', () => {
setup();
expect(vm.$store.state.memberPath).toBe('/groups/foo-bar/-/group_members/:id');
expect(vm.$store.state[MEMBER_TYPES.user].memberPath).toBe(
'/groups/foo-bar/-/group_members/:id',
);
});
});

View File

@ -59,4 +59,44 @@ describe('PerformanceBarStore', () => {
expect(store.findRequest('id').details.test.calls).toEqual(123);
});
});
describe('canTrackRequest', () => {
let store;
beforeEach(() => {
store = new PerformanceBarStore();
});
it('limits to 10 requests for GraphQL', () => {
expect(store.canTrackRequest('https://gitlab.com/api/graphql')).toBe(true);
store.addRequest('0', 'https://gitlab.com/api/graphql');
store.addRequest('1', 'https://gitlab.com/api/graphql');
store.addRequest('2', 'https://gitlab.com/api/graphql');
store.addRequest('3', 'https://gitlab.com/api/graphql');
store.addRequest('4', 'https://gitlab.com/api/graphql');
store.addRequest('5', 'https://gitlab.com/api/graphql');
store.addRequest('6', 'https://gitlab.com/api/graphql');
store.addRequest('7', 'https://gitlab.com/api/graphql');
store.addRequest('8', 'https://gitlab.com/api/graphql');
expect(store.canTrackRequest('https://gitlab.com/api/graphql')).toBe(true);
store.addRequest('9', 'https://gitlab.com/api/graphql');
expect(store.canTrackRequest('https://gitlab.com/api/graphql')).toBe(false);
});
it('limits to 2 requests for all other URLs', () => {
expect(store.canTrackRequest('https://gitlab.com/api/v4/users/1')).toBe(true);
store.addRequest('a', 'https://gitlab.com/api/v4/users/1');
expect(store.canTrackRequest('https://gitlab.com/api/v4/users/1')).toBe(true);
store.addRequest('b', 'https://gitlab.com/api/v4/users/1');
expect(store.canTrackRequest('https://gitlab.com/api/v4/users/1')).toBe(false);
});
});
});

View File

@ -74,7 +74,7 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
context 'when the noteable could not be found' do
before do
noteable.destroy
noteable.destroy!
end
it 'raises a NoteableNotFoundError' do

View File

@ -0,0 +1,95 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe AddNewTrailPlans, :migration do
describe '#up' do
before do
allow(Gitlab).to receive(:dev_env_or_com?).and_return true
end
it 'creates 2 entries within the plans table' do
expect { migrate! }.to change { AddNewTrailPlans::Plan.count }.by 2
expect(AddNewTrailPlans::Plan.last(2).pluck(:name)).to match_array(%w(ultimate_trial premium_trial))
end
it 'creates 2 entries for plan limits' do
expect { migrate! }.to change { AddNewTrailPlans::PlanLimits.count }.by 2
end
context 'when the plan limits for gold and silver exists' do
before do
table(:plans).create!(id: 1, name: 'gold', title: 'Gold')
table(:plan_limits).create!(id: 1, plan_id: 1, storage_size_limit: 2000)
table(:plans).create!(id: 2, name: 'silver', title: 'Silver')
table(:plan_limits).create!(id: 2, plan_id: 2, storage_size_limit: 1000)
end
it 'duplicates the gold and silvers plan limits entries' do
migrate!
ultimate_plan_limits = AddNewTrailPlans::Plan.find_by(name: 'ultimate_trial').limits
expect(ultimate_plan_limits.storage_size_limit).to be 2000
premium_plan_limits = AddNewTrailPlans::Plan.find_by(name: 'premium_trial').limits
expect(premium_plan_limits.storage_size_limit).to be 1000
end
end
context 'when the instance is not SaaS' do
before do
allow(Gitlab).to receive(:dev_env_or_com?).and_return false
end
it 'does not create plans and plan limits and returns' do
expect { migrate! }.not_to change { AddNewTrailPlans::Plan.count }
expect { migrate! }.not_to change { AddNewTrailPlans::Plan.count }
end
end
end
describe '#down' do
before do
table(:plans).create!(id: 3, name: 'other')
table(:plan_limits).create!(plan_id: 3)
end
context 'when the instance is SaaS' do
before do
allow(Gitlab).to receive(:dev_env_or_com?).and_return true
end
it 'removes the newly added ultimate and premium trial entries' do
migrate!
expect { described_class.new.down }.to change { AddNewTrailPlans::Plan.count }.by(-2)
expect(AddNewTrailPlans::Plan.find_by(name: 'premium_trial')).to be_nil
expect(AddNewTrailPlans::Plan.find_by(name: 'ultimate_trial')).to be_nil
other_plan = AddNewTrailPlans::Plan.find_by(name: 'other')
expect(other_plan).to be_persisted
expect(AddNewTrailPlans::PlanLimits.count).to eq(1)
expect(AddNewTrailPlans::PlanLimits.first.plan_id).to eq(other_plan.id)
end
end
context 'when the instance is not SaaS' do
before do
allow(Gitlab).to receive(:dev_env_or_com?).and_return false
table(:plans).create!(id: 1, name: 'ultimate_trial', title: 'Ultimate Trial')
table(:plans).create!(id: 2, name: 'premium_trial', title: 'Premium Trial')
table(:plan_limits).create!(id: 1, plan_id: 1)
table(:plan_limits).create!(id: 2, plan_id: 2)
end
it 'does not delete plans and plan limits and returns' do
migrate!
expect { described_class.new.down }.not_to change { AddNewTrailPlans::Plan.count }
expect(AddNewTrailPlans::PlanLimits.count).to eq(3)
end
end
end
end

View File

@ -0,0 +1,131 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../../../rubocop/cop/style/regexp_literal_mixed_preserve'
# This spec contains only relevant examples.
#
# See also https://github.com/rubocop/rubocop/pull/9688
RSpec.describe RuboCop::Cop::Style::RegexpLiteralMixedPreserve, :config do
let(:config) do
supported_styles = { 'SupportedStyles' => %w[slashes percent_r mixed mixed_preserve] }
RuboCop::Config.new('Style/PercentLiteralDelimiters' =>
percent_literal_delimiters_config,
'Style/RegexpLiteralMixedPreserve' =>
cop_config.merge(supported_styles))
end
let(:percent_literal_delimiters_config) { { 'PreferredDelimiters' => { '%r' => '{}' } } }
context 'when EnforcedStyle is set to mixed_preserve' do
let(:cop_config) { { 'EnforcedStyle' => 'mixed_preserve' } }
describe 'a single-line `//` regex without slashes' do
it 'is accepted' do
expect_no_offenses('foo = /a/')
end
end
describe 'a single-line `//` regex with slashes' do
it 'registers an offense and corrects' do
expect_offense(<<~'RUBY')
foo = /home\//
^^^^^^^^ Use `%r` around regular expression.
RUBY
expect_correction(<<~'RUBY')
foo = %r{home/}
RUBY
end
describe 'when configured to allow inner slashes' do
before do
cop_config['AllowInnerSlashes'] = true
end
it 'is accepted' do
expect_no_offenses('foo = /home\\//')
end
end
end
describe 'a multi-line `//` regex without slashes' do
it 'is accepted' do
expect_no_offenses(<<~'RUBY')
foo = /
foo
bar
/x
RUBY
end
end
describe 'a multi-line `//` regex with slashes' do
it 'registers an offense and corrects' do
expect_offense(<<~'RUBY')
foo = /
^ Use `%r` around regular expression.
https?:\/\/
example\.com
/x
RUBY
expect_correction(<<~'RUBY')
foo = %r{
https?://
example\.com
}x
RUBY
end
end
describe 'a single-line %r regex without slashes' do
it 'is accepted' do
expect_no_offenses(<<~RUBY)
foo = %r{a}
RUBY
end
end
describe 'a single-line %r regex with slashes' do
it 'is accepted' do
expect_no_offenses('foo = %r{home/}')
end
describe 'when configured to allow inner slashes' do
before do
cop_config['AllowInnerSlashes'] = true
end
it 'is accepted' do
expect_no_offenses(<<~RUBY)
foo = %r{home/}
RUBY
end
end
end
describe 'a multi-line %r regex without slashes' do
it 'is accepted' do
expect_no_offenses(<<~RUBY)
foo = %r{
foo
bar
}x
RUBY
end
end
describe 'a multi-line %r regex with slashes' do
it 'is accepted' do
expect_no_offenses(<<~RUBY)
foo = %r{
https?://
example\.com
}x
RUBY
end
end
end
end