Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue64
-rw-r--r--app/assets/javascripts/boards/index.js3
-rw-r--r--app/assets/javascripts/boards/stores/actions.js3
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js4
-rw-r--r--app/assets/javascripts/boards/stores/state.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue12
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue2
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/item.vue35
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue81
-rw-r--r--app/assets/javascripts/members/constants.js16
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue15
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue49
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue31
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql19
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue2
-rw-r--r--app/assets/javascripts/search_settings/components/search_settings.vue58
-rw-r--r--app/assets/javascripts/search_settings/constants.js3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue26
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue21
-rw-r--r--app/assets/stylesheets/pages/issuable.scss12
-rw-r--r--app/controllers/groups/boards_controller.rb1
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/import/bulk_imports_controller.rb22
-rw-r--r--app/controllers/projects/boards_controller.rb1
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb1
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/repositories/git_http_controller.rb7
-rw-r--r--app/graphql/types/packages/nuget/metadatum_type.rb6
-rw-r--r--app/models/bulk_import.rb11
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/member.rb5
-rw-r--r--app/models/packages/helm/file_metadatum.rb2
-rw-r--r--app/serializers/member_entity.rb6
-rw-r--r--app/services/bulk_imports/create_service.rb13
-rw-r--r--app/services/bulk_imports/get_importable_data_service.rb45
-rw-r--r--app/views/admin/users/_access_levels.html.haml24
-rw-r--r--app/views/admin/users/_form.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/projects/ci/pipeline_editor/show.html.haml2
-rw-r--r--config/feature_flags/development/pipeline_editor_mini_graph.yml8
-rw-r--r--danger/specs/Dangerfile4
-rw-r--r--data/deprecations/templates/_deprecation_template.md.erb8
-rw-r--r--db/migrate/20211001001222_add_source_version_to_bulk_imports.rb7
-rw-r--r--db/migrate/20211006103122_change_helm_channel_length.rb14
-rw-r--r--db/migrate/20211012091822_add_text_limit_to_bulk_imports_source_version.rb13
-rw-r--r--db/schema_migrations/202110010012221
-rw-r--r--db/schema_migrations/202110061031221
-rw-r--r--db/schema_migrations/202110120918221
-rw-r--r--db/structure.sql6
-rw-r--r--doc/api/graphql/reference/index.md6
-rw-r--r--doc/development/ee_features.md2
-rw-r--r--doc/development/fe_guide/haml.md15
-rw-r--r--doc/development/pipelines.md40
-rw-r--r--doc/development/testing_guide/best_practices.md2
-rw-r--r--doc/development/testing_guide/ci.md46
-rw-r--r--doc/development/testing_guide/index.md2
-rw-r--r--doc/integration/saml.md17
-rw-r--r--doc/topics/autodevops/customize.md2
-rw-r--r--doc/topics/autodevops/index.md2
-rw-r--r--doc/topics/autodevops/quick_start_guide.md12
-rw-r--r--doc/topics/autodevops/stages.md16
-rw-r--r--doc/topics/autodevops/upgrading_auto_deploy_dependencies.md2
-rw-r--r--doc/update/deprecations.md8
-rw-r--r--doc/user/group/saml_sso/index.md8
-rw-r--r--lib/bulk_imports/clients/graphql.rb2
-rw-r--r--lib/bulk_imports/clients/http.rb26
-rw-r--r--lib/bulk_imports/error.rb2
-rw-r--r--lib/bulk_imports/groups/stage.rb6
-rw-r--r--lib/bulk_imports/stage.rb6
-rw-r--r--lib/gitlab/form_builders/gitlab_ui_form_builder.rb60
-rw-r--r--lib/gitlab/regex.rb2
-rw-r--r--locale/gitlab.pot72
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb96
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb25
-rw-r--r--spec/factories/bulk_import.rb1
-rw-r--r--spec/features/boards/sidebar_labels_spec.rb50
-rw-r--r--spec/features/groups/import_export/connect_instance_spec.rb34
-rw-r--r--spec/fixtures/api/schemas/entities/member.json6
-rw-r--r--spec/frontend/boards/stores/actions_spec.js10
-rw-r--r--spec/frontend/issuable_suggestions/components/item_spec.js63
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js85
-rw-r--r--spec/frontend/members/mock_data.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js29
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js51
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js42
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js53
-rw-r--r--spec/frontend/search_settings/components/search_settings_spec.js35
-rw-r--r--spec/frontend/sidebar/sidebar_labels_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js2
-rw-r--r--spec/graphql/types/packages/nuget/metadatum_type_spec.rb6
-rw-r--r--spec/lib/bulk_imports/clients/graphql_spec.rb2
-rw-r--r--spec/lib/bulk_imports/clients/http_spec.rb26
-rw-r--r--spec/lib/bulk_imports/groups/stage_spec.rb14
-rw-r--r--spec/lib/bulk_imports/projects/stage_spec.rb10
-rw-r--r--spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb63
-rw-r--r--spec/lib/gitlab/regex_spec.rb1
-rw-r--r--spec/models/bulk_import_spec.rb14
-rw-r--r--spec/models/bulk_imports/entity_spec.rb4
-rw-r--r--spec/models/bulk_imports/tracker_spec.rb3
-rw-r--r--spec/models/member_spec.rb21
-rw-r--r--spec/models/packages/helm/file_metadatum_spec.rb4
-rw-r--r--spec/requests/api/bulk_imports_spec.rb9
-rw-r--r--spec/serializers/member_entity_spec.rb26
-rw-r--r--spec/services/bulk_imports/create_service_spec.rb17
-rw-r--r--spec/services/bulk_imports/get_importable_data_service_spec.rb46
-rw-r--r--spec/support/shared_contexts/bulk_imports_requests_shared_context.rb52
-rw-r--r--spec/workers/bulk_import_worker_spec.rb2
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb14
-rw-r--r--workhorse/internal/git/info-refs.go20
-rw-r--r--workhorse/internal/git/info-refs_test.go42
-rw-r--r--workhorse/internal/git/upload-pack_test.go13
121 files changed, 1552 insertions, 513 deletions
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index e0105d63d99..9bbb8a1a1b2 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -3,15 +3,18 @@ import { GlDrawer } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
+import { __, sprintf } from '~/locale';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
+import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@@ -23,6 +26,7 @@ export default {
SidebarConfidentialityWidget,
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
+ SidebarLabelsWidget,
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
SidebarTodoWidget,
@@ -46,16 +50,20 @@ export default {
weightFeatureAvailable: {
default: false,
},
+ allowLabelEdit: {
+ default: false,
+ },
},
inheritAttrs: false,
computed: {
...mapGetters([
+ 'isGroupBoard',
'isSidebarOpen',
'activeBoardItem',
'groupPathForActiveIssue',
'projectPathForActiveIssue',
]),
- ...mapState(['sidebarType', 'issuableType']),
+ ...mapState(['sidebarType', 'issuableType', 'isSettingLabels']),
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
@@ -65,17 +73,48 @@ export default {
fullPath() {
return this.activeBoardItem?.referencePath?.split('#')[0] || '';
},
+ createLabelTitle() {
+ return sprintf(__('Create %{workspace} label'), {
+ workspace: this.isGroupBoard ? 'group' : 'project',
+ });
+ },
+ manageLabelTitle() {
+ return sprintf(__('Manage %{workspace} labels'), {
+ workspace: this.isGroupBoard ? 'group' : 'project',
+ });
+ },
+ attrWorkspacePath() {
+ return this.isGroupBoard ? this.groupPathForActiveIssue : undefined;
+ },
},
methods: {
...mapActions([
'toggleBoardItem',
'setAssignees',
'setActiveItemConfidential',
+ 'setActiveBoardItemLabels',
'setActiveItemWeight',
]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
+ handleUpdateSelectedLabels(input) {
+ this.setActiveBoardItemLabels({
+ iid: this.activeBoardItem.iid,
+ projectPath: this.projectPathForActiveIssue,
+ addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)),
+ removeLabelIds: this.activeBoardItem.labels
+ .filter((label) => !input.find((selected) => selected.id === label.id))
+ .map((label) => label.id),
+ });
+ },
+ handleLabelRemove(input) {
+ this.setActiveBoardItemLabels({
+ iid: this.activeBoardItem.iid,
+ projectPath: this.projectPathForActiveIssue,
+ removeLabelIds: [input],
+ });
+ },
},
};
</script>
@@ -160,7 +199,28 @@ export default {
:issuable-type="issuableType"
data-testid="sidebar-due-date"
/>
- <board-sidebar-labels-select class="block labels" />
+ <sidebar-labels-widget
+ v-if="glFeatures.labelsWidget"
+ class="block labels"
+ data-testid="sidebar-labels"
+ :iid="activeBoardItem.iid"
+ :full-path="projectPathForActiveIssue"
+ :allow-label-remove="allowLabelEdit"
+ :allow-multiselect="true"
+ :selected-labels="activeBoardItem.labels"
+ :labels-select-in-progress="isSettingLabels"
+ :footer-create-label-title="createLabelTitle"
+ :footer-manage-label-title="manageLabelTitle"
+ :labels-create-title="createLabelTitle"
+ :labels-filter-base-path="projectPathForActiveIssue"
+ :attr-workspace-path="attrWorkspacePath"
+ :issuable-type="issuableType"
+ @onLabelRemove="handleLabelRemove"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ >
+ {{ __('None') }}
+ </sidebar-labels-widget>
+ <board-sidebar-labels-select v-else class="block labels" />
<sidebar-weight-widget
v-if="weightFeatureAvailable"
:iid="activeBoardItem.iid"
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 1c8b1f9b009..b6b1094fb3a 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -87,6 +87,9 @@ function mountBoardApp(el) {
iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable),
issuableType: issuableTypes.issue,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
+ allowLabelCreate: parseBoolean(el.dataset.canUpdate),
+ allowLabelEdit: parseBoolean(el.dataset.canUpdate),
+ allowScopedLabels: parseBoolean(el.dataset.scopedLabels),
},
render: (createComponent) => createComponent(BoardApp),
});
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 270f2ff085b..ca993e75cf9 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -656,6 +656,7 @@ export default {
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
+ commit(types.SET_LABELS_LOADING, true);
const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetLabelsMutation,
@@ -669,6 +670,8 @@ export default {
},
});
+ commit(types.SET_LABELS_LOADING, false);
+
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
}
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 928cece19f7..26b785932bb 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -28,6 +28,7 @@ export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
+export const SET_LABELS_LOADING = 'SET_LABELS_LOADING';
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
export const RESET_ISSUES = 'RESET_ISSUES';
export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index ef5b84b4575..d381c076c19 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -195,6 +195,10 @@ export default {
Vue.set(state.boardItems[itemId], prop, value);
},
+ [mutationTypes.SET_LABELS_LOADING](state, isLoading) {
+ state.isSettingLabels = isLoading;
+ },
+
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
state.isSettingAssignees = isLoading;
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 80c51c966d2..2a6605e687b 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -12,6 +12,7 @@ export default () => ({
listsFlags: {},
boardItemsByListId: {},
backupItemsList: [],
+ isSettingLabels: false,
isSettingAssignees: false,
pageInfoByListId: {},
boardItems: {},
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index 4aee02e45c8..9d4eddc510a 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -293,7 +293,7 @@ export default {
:items="roles"
:loading="isLoadingRoles"
:loading-text="s__('ClusterIntegration|Loading IAM Roles')"
- :placeholder="s__('ClusterIntergation|Select service role')"
+ :placeholder="s__('ClusterIntegration|Select service role')"
:search-field-placeholder="s__('ClusterIntegration|Search IAM Roles')"
:empty-text="s__('ClusterIntegration|No IAM Roles found')"
:has-errors="Boolean(loadingRolesError)"
@@ -330,7 +330,7 @@ export default {
:disabled-text="s__('ClusterIntegration|Select a region to choose a Key Pair')"
:loading="isLoadingKeyPairs"
:loading-text="s__('ClusterIntegration|Loading Key Pairs')"
- :placeholder="s__('ClusterIntergation|Select key pair')"
+ :placeholder="s__('ClusterIntegration|Select key pair')"
:search-field-placeholder="s__('ClusterIntegration|Search Key Pairs')"
:empty-text="s__('ClusterIntegration|No Key Pairs found')"
:has-errors="Boolean(loadingKeyPairsError)"
@@ -359,7 +359,7 @@ export default {
:disabled="vpcDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a region to choose a VPC')"
:loading-text="s__('ClusterIntegration|Loading VPCs')"
- :placeholder="s__('ClusterIntergation|Select a VPC')"
+ :placeholder="s__('ClusterIntegration|Select a VPC')"
:search-field-placeholder="s__('ClusterIntegration|Search VPCs')"
:empty-text="s__('ClusterIntegration|No VPCs found')"
:has-errors="Boolean(loadingVpcsError)"
@@ -389,7 +389,7 @@ export default {
:disabled="subnetDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a VPC to choose a subnet')"
:loading-text="s__('ClusterIntegration|Loading subnets')"
- :placeholder="s__('ClusterIntergation|Select a subnet')"
+ :placeholder="s__('ClusterIntegration|Select a subnet')"
:search-field-placeholder="s__('ClusterIntegration|Search subnets')"
:empty-text="s__('ClusterIntegration|No subnet found')"
:has-errors="displaySubnetError"
@@ -420,7 +420,7 @@ export default {
:disabled="securityGroupDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a VPC to choose a security group')"
:loading-text="s__('ClusterIntegration|Loading security groups')"
- :placeholder="s__('ClusterIntergation|Select a security group')"
+ :placeholder="s__('ClusterIntegration|Select a security group')"
:search-field-placeholder="s__('ClusterIntegration|Search security groups')"
:empty-text="s__('ClusterIntegration|No security group found')"
:has-errors="Boolean(loadingSecurityGroupsError)"
@@ -451,7 +451,7 @@ export default {
:items="instanceTypes"
:loading="isLoadingInstanceTypes"
:loading-text="s__('ClusterIntegration|Loading instance types')"
- :placeholder="s__('ClusterIntergation|Select an instance type')"
+ :placeholder="s__('ClusterIntegration|Select an instance type')"
:search-field-placeholder="s__('ClusterIntegration|Search instance types')"
:empty-text="s__('ClusterIntegration|No instance type found')"
:has-errors="Boolean(loadingInstanceTypesError)"
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
index 12b6070a79a..8f18ac29c0f 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
@@ -43,7 +43,7 @@ export default {
:loading="isLoadingItems"
:has-errors="Boolean(loadingItemsError)"
:loading-text="s__('ClusterIntegration|Loading networks')"
- :placeholder="s__('ClusterIntergation|Select a network')"
+ :placeholder="s__('ClusterIntegration|Select a network')"
:search-field-placeholder="s__('ClusterIntegration|Search networks')"
:empty-text="s__('ClusterIntegration|No networks found')"
:error-message="s__('ClusterIntegration|Could not load networks')"
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
index ec7889e2907..dab4adc3789 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
@@ -34,7 +34,7 @@ export default {
:loading="isLoadingItems"
:has-errors="Boolean(loadingItemsError)"
:loading-text="s__('ClusterIntegration|Loading subnetworks')"
- :placeholder="s__('ClusterIntergation|Select a subnetwork')"
+ :placeholder="s__('ClusterIntegration|Select a subnetwork')"
:search-field-placeholder="s__('ClusterIntegration|Search subnetworks')"
:empty-text="s__('ClusterIntegration|No subnetworks found')"
:error-message="s__('ClusterIntegration|Could not load subnetworks')"
diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
index 858c30649bb..1a470d74b59 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
@@ -17,7 +17,7 @@ export default {
},
},
i18n: {
- percentageDescription: __('Enter an integer number number between 0 and 100'),
+ percentageDescription: __('Enter an integer number between 0 and 100'),
percentageInvalid: __('Percent rollout must be an integer number between 0 and 100'),
percentageLabel: __('Percentage'),
stickinessDescription: __('Consistency guarantee method'),
diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue
index dea7608685a..a01f4f747b9 100644
--- a/app/assets/javascripts/issuable_suggestions/components/item.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/item.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlLink, GlTooltip, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { __ } from '~/locale';
@@ -26,12 +25,6 @@ export default {
},
},
computed: {
- isOpen() {
- return this.suggestion.state === 'opened';
- },
- isClosed() {
- return this.suggestion.state === 'closed';
- },
counts() {
return [
{
@@ -48,7 +41,13 @@ export default {
},
].filter(({ count }) => count);
},
- stateIcon() {
+ isClosed() {
+ return this.suggestion.state === 'closed';
+ },
+ stateIconClass() {
+ return this.isClosed ? 'gl-text-blue-500' : 'gl-text-green-500';
+ },
+ stateIconName() {
return this.isClosed ? 'issue-close' : 'issue-open-m';
},
stateTitle() {
@@ -72,7 +71,7 @@ export default {
v-gl-tooltip.bottom
:title="__('Confidential')"
name="eye-slash"
- class="suggestion-help-hover mr-1 suggestion-confidential"
+ class="gl-cursor-help gl-mr-2 gl-text-orange-500"
/>
<gl-link
:href="suggestion.webUrl"
@@ -83,15 +82,7 @@ export default {
</gl-link>
</div>
<div class="text-secondary suggestion-footer">
- <gl-icon
- ref="state"
- :name="stateIcon"
- :class="{
- 'suggestion-state-open': isOpen,
- 'suggestion-state-closed': isClosed,
- }"
- class="suggestion-help-hover"
- />
+ <gl-icon ref="state" :name="stateIconName" :class="stateIconClass" class="gl-cursor-help" />
<gl-tooltip :target="() => $refs.state" placement="bottom">
<span class="d-block">
<span class="bold"> {{ stateTitle }} </span> {{ timeFormatted(closedOrCreatedDate) }}
@@ -102,9 +93,9 @@ export default {
<timeago-tooltip
:time="suggestion.createdAt"
tooltip-placement="bottom"
- class="suggestion-help-hover"
+ class="gl-cursor-help"
/>
- by
+ {{ __('by') }}
<gl-link :href="suggestion.author.webUrl">
<user-avatar-image
:img-src="suggestion.author.avatarUrl"
@@ -122,7 +113,7 @@ export default {
<timeago-tooltip
:time="suggestion.updatedAt"
tooltip-placement="bottom"
- class="suggestion-help-hover"
+ class="gl-cursor-help"
/>
</template>
<span class="suggestion-counts">
@@ -131,7 +122,7 @@ export default {
:key="id"
v-gl-tooltip.bottom
:title="tooltipTitle"
- class="suggestion-help-hover gl-ml-3 text-tertiary"
+ class="gl-cursor-help gl-ml-3 text-tertiary"
>
<gl-icon :name="icon" /> {{ count }}
</span>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 3caecdd1d4b..202f3aa89e1 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -5,7 +5,13 @@ import MembersTableCell from 'ee_else_ce/members/components/table/members_table_
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
-import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants';
+import {
+ FIELDS,
+ ACTIVE_TAB_QUERY_PARAM_NAME,
+ MEMBER_STATE_AWAITING,
+ USER_STATE_BLOCKED_PENDING_APPROVAL,
+ BADGE_LABELS_PENDING_OWNER_APPROVAL,
+} from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import RemoveMemberModal from '../modals/remove_member_modal.vue';
import CreatedAt from './created_at.vue';
@@ -129,6 +135,74 @@ export default {
window.location.href,
);
},
+ /**
+ * Returns whether it's a new or existing user
+ *
+ * If memberInviteMetadata doesn't exist, it means we're adding an existing user
+ * to the Group/Project, so `isNewUser` should be false.
+ * If memberInviteMetadata exists but `userState` has content,
+ * the user has registered but is awaiting root approval
+ *
+ * @param {object} memberInviteMetadata - MemberEntity.invite
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @returns {boolean}
+ */
+ isNewUser(memberInviteMetadata) {
+ return memberInviteMetadata && !memberInviteMetadata.userState;
+ },
+ /**
+ * Returns whether the user is awaiting root approval
+ *
+ * This checks User.state exposed via MemberEntity
+ *
+ * @param {object} memberInviteMetadata - MemberEntity.invite
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @returns {boolean}
+ */
+ isUserPendingRootApproval(memberInviteMetadata) {
+ return memberInviteMetadata?.userState === USER_STATE_BLOCKED_PENDING_APPROVAL;
+ },
+ /**
+ * Returns whether the member is awaiting owner approval
+ *
+ * This checks Member.state exposed via MemberEntity
+ *
+ * @param {Number} memberState - Member.state exposed via MemberEntity.state
+ * @see {@link ~/ee/app/models/ee/member.rb}
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @returns {boolean}
+ */
+ isMemberPendingOwnerApproval(memberState) {
+ return memberState === MEMBER_STATE_AWAITING;
+ },
+ isUserAwaiting(memberInviteMetadata, memberState) {
+ return (
+ this.isUserPendingRootApproval(memberInviteMetadata) ||
+ this.isMemberPendingOwnerApproval(memberState)
+ );
+ },
+ shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState) {
+ return (
+ this.isUserAwaiting(memberInviteMetadata, memberState) &&
+ !this.isNewUser(memberInviteMetadata)
+ );
+ },
+ /**
+ * Returns the string to be used in the invite badge
+ *
+ * @param {object} memberInviteMetadata - MemberEntity.invite
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @param {Number} memberState - Member.state exposed via MemberEntity.state
+ * @see {@link ~/ee/app/models/ee/member.rb}
+ * @returns {string}
+ */
+ inviteBadge(memberInviteMetadata, memberState) {
+ if (this.shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState)) {
+ return BADGE_LABELS_PENDING_OWNER_APPROVAL;
+ }
+
+ return '';
+ },
},
};
</script>
@@ -172,8 +246,11 @@ export default {
<created-at :date="createdAt" :created-by="createdBy" />
</template>
- <template #cell(invited)="{ item: { createdAt, createdBy } }">
+ <template #cell(invited)="{ item: { createdAt, createdBy, invite, state } }">
<created-at :date="createdAt" :created-by="createdBy" />
+ <gl-badge v-if="inviteBadge(invite, state)" data-testid="invited-badge">{{
+ inviteBadge(invite, state)
+ }}</gl-badge>
</template>
<template #cell(requested)="{ item: { createdAt } }">
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 54d4442d5ce..f5ca881ab0d 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -89,6 +89,22 @@ export const TAB_QUERY_PARAM_VALUES = {
accessRequest: 'access_requests',
};
+/**
+ * This user state value comes from the User model
+ * see the state machine in app/models/user.rb
+ */
+export const USER_STATE_BLOCKED_PENDING_APPROVAL = 'blocked_pending_approval';
+
+/**
+ * This and following member state constants' values
+ * come from ee/app/models/ee/member.rb
+ */
+export const MEMBER_STATE_CREATED = 0;
+export const MEMBER_STATE_AWAITING = 1;
+export const MEMBER_STATE_ACTIVE = 2;
+
+export const BADGE_LABELS_PENDING_OWNER_APPROVAL = __('Pending owner approval');
+
export const DAYS_TO_EXPIRE_SOON = 7;
export const LEAVE_MODAL_ID = 'member-leave-modal';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
index f0da7db6c91..1360b03856f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
@@ -24,7 +24,13 @@ export default {
<template>
<div>
- <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
+ <details-row
+ v-if="packageEntity.metadata.projectUrl"
+ icon="project"
+ padding="gl-p-4"
+ dashed
+ data-testid="nuget-source"
+ >
<gl-sprintf :message="$options.i18n.sourceText">
<template #link>
<gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{
@@ -33,7 +39,12 @@ export default {
</template>
</gl-sprintf>
</details-row>
- <details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
+ <details-row
+ v-if="packageEntity.metadata.licenseUrl"
+ icon="license"
+ padding="gl-p-4"
+ data-testid="nuget-license"
+ >
<gl-sprintf :message="$options.i18n.licenseText">
<template #link>
<gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
new file mode 100644
index 00000000000..75b1398a3c2
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -0,0 +1,49 @@
+<script>
+import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
+
+export default {
+ components: {
+ PipelineMiniGraph,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ pipelinePath() {
+ return this.pipeline.detailedStatus?.detailsPath || '';
+ },
+ pipelineStages() {
+ const stages = this.pipeline.stages?.edges;
+ if (!stages) {
+ return [];
+ }
+
+ return stages.map(({ node }) => {
+ const { name, detailedStatus } = node;
+ return {
+ // TODO: fetch dropdown_path from graphql when available
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/342585
+ dropdown_path: `${this.pipelinePath}/stage.json?stage=${name}`,
+ name,
+ path: `${this.pipelinePath}#${name}`,
+ status: {
+ details_path: `${this.pipelinePath}#${name}`,
+ has_details: detailedStatus.hasDetails,
+ ...detailedStatus,
+ },
+ title: `${name}: ${detailedStatus.text}`,
+ };
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5">
+ <pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index ec240854be5..a1fa2147994 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -10,6 +10,8 @@ import {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000;
export const i18n = {
@@ -30,7 +32,9 @@ export default {
GlLink,
GlLoadingIcon,
GlSprintf,
+ PipelineEditorMiniGraph,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['projectFullPath'],
props: {
commitSha: {
@@ -55,12 +59,15 @@ export default {
};
},
update(data) {
- const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {};
+ const { id, commitPath = '', detailedStatus = {}, stages, status } =
+ data.project?.pipeline || {};
return {
id,
commitPath,
detailedStatus,
+ stages,
+ status,
};
},
result(res) {
@@ -111,9 +118,7 @@ export default {
</script>
<template>
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-white-space-nowrap gl-max-w-full"
- >
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap">
<template v-if="showLoadingState">
<div>
<gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" />
@@ -129,19 +134,12 @@ export default {
<template v-else>
<div>
<a :href="status.detailsPath" class="gl-mr-auto">
- <ci-icon :status="status" :size="16" />
+ <ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" />
</a>
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
- <gl-link
- :href="status.detailsPath"
- class="pipeline-id gl-font-weight-normal pipeline-number"
- target="_blank"
- data-testid="pipeline-id"
- >
- {{ content }}{{ pipelineId }}</gl-link
- >
+ <span data-testid="pipeline-id"> {{ content }}{{ pipelineId }} </span>
</template>
<template #status>{{ status.text }}</template>
<template #commit>
@@ -157,8 +155,13 @@ export default {
</gl-sprintf>
</span>
</div>
- <div>
+ <div class="gl-display-flex gl-flex-wrap">
+ <pipeline-editor-mini-graph
+ v-if="glFeatures.pipelineEditorMiniGraph"
+ :pipeline="pipeline"
+ />
<gl-button
+ class="gl-mt-2 gl-md-mt-0"
target="_blank"
category="secondary"
variant="confirm"
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
index d3a7387ad2d..0c3653a2880 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
@@ -11,6 +11,25 @@ query getPipeline($fullPath: ID!, $sha: String!) {
group
text
}
+ stages {
+ edges {
+ node {
+ id
+ name
+ status
+ detailedStatus {
+ detailsPath
+ group
+ hasDetails
+ icon
+ id
+ label
+ text
+ tooltip
+ }
+ }
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
index 545263a9e37..f936c03c5d3 100644
--- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -201,7 +201,7 @@ export default {
<gl-button
ref="addButton"
category="primary"
- variant="success"
+ variant="confirm"
:disabled="isSubmitButtonDisabled"
:loading="isSubmitting"
type="submit"
diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue
index 116967a62c8..3e23b8a3435 100644
--- a/app/assets/javascripts/search_settings/components/search_settings.vue
+++ b/app/assets/javascripts/search_settings/components/search_settings.vue
@@ -1,7 +1,13 @@
<script>
import { GlSearchBoxByType } from '@gitlab/ui';
-import { uniq } from 'lodash';
-import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants';
+import { uniq, escapeRegExp } from 'lodash';
+import {
+ EXCLUDED_NODES,
+ HIDE_CLASS,
+ HIGHLIGHT_CLASS,
+ NONE_PADDING_CLASS,
+ TYPING_DELAY,
+} from '../constants';
const origExpansions = new Map();
@@ -37,9 +43,13 @@ const resetSections = ({ sectionSelector }) => {
};
const clearHighlights = () => {
- document
- .querySelectorAll(`.${HIGHLIGHT_CLASS}`)
- .forEach((element) => element.classList.remove(HIGHLIGHT_CLASS));
+ document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((element) => {
+ const { parentNode } = element;
+ const textNode = document.createTextNode(element.textContent);
+ parentNode.replaceChild(textNode, element);
+
+ parentNode.normalize();
+ });
};
const hideSectionsExcept = (sectionSelector, visibleSections) => {
@@ -50,17 +60,41 @@ const hideSectionsExcept = (sectionSelector, visibleSections) => {
});
};
-const highlightElements = (elements = []) => {
- elements.forEach((element) => element.classList.add(HIGHLIGHT_CLASS));
+const transformMatchElement = (element, searchTerm) => {
+ const textStr = element.textContent;
+ const escapedSearchTerm = new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi');
+
+ const textList = textStr.split(escapedSearchTerm);
+ const replaceFragment = document.createDocumentFragment();
+ textList.forEach((text) => {
+ let addElement = document.createTextNode(text);
+ if (escapedSearchTerm.test(text)) {
+ addElement = document.createElement('mark');
+ addElement.className = `${HIGHLIGHT_CLASS} ${NONE_PADDING_CLASS}`;
+ addElement.textContent = text;
+ escapedSearchTerm.lastIndex = 0;
+ }
+ replaceFragment.appendChild(addElement);
+ });
+
+ return replaceFragment;
+};
+
+const highlightElements = (elements = [], searchTerm) => {
+ elements.forEach((element) => {
+ const replaceFragment = transformMatchElement(element, searchTerm);
+ element.innerHTML = '';
+ element.appendChild(replaceFragment);
+ });
};
-const displayResults = ({ sectionSelector, expandSection }, matches) => {
+const displayResults = ({ sectionSelector, expandSection, searchTerm }, matches) => {
const elements = matches.map((match) => match.parentElement);
const sections = uniq(elements.map((element) => findSettingsSection(sectionSelector, element)));
hideSectionsExcept(sectionSelector, sections);
sections.forEach(expandSection);
- highlightElements(elements);
+ highlightElements(elements, searchTerm);
};
const clearResults = (params) => {
@@ -116,21 +150,21 @@ export default {
},
methods: {
search(value) {
+ this.searchTerm = value;
const displayOptions = {
sectionSelector: this.sectionSelector,
expandSection: this.expandSection,
collapseSection: this.collapseSection,
isExpanded: this.isExpandedFn,
+ searchTerm: this.searchTerm,
};
- this.searchTerm = value;
-
clearResults(displayOptions);
if (value.length) {
saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
- displayResults(displayOptions, search(this.searchRoot, value));
+ displayResults(displayOptions, search(this.searchRoot, this.searchTerm));
} else {
restoreExpansionState(displayOptions);
}
diff --git a/app/assets/javascripts/search_settings/constants.js b/app/assets/javascripts/search_settings/constants.js
index 9452d149122..a49351dc7b0 100644
--- a/app/assets/javascripts/search_settings/constants.js
+++ b/app/assets/javascripts/search_settings/constants.js
@@ -7,5 +7,8 @@ export const HIDE_CLASS = 'gl-display-none';
// used to highlight the text that matches the * search term
export const HIGHLIGHT_CLASS = 'gl-bg-orange-100';
+// used to remove padding for text that matches the * search term
+export const NONE_PADDING_CLASS = 'gl-p-0';
+
// How many seconds to wait until the user * stops typing
export const TYPING_DELAY = 400;
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index af426584f4f..d5647619ea3 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -36,6 +36,7 @@ export default {
'allowLabelEdit',
'allowScopedLabels',
'iid',
+ 'fullPath',
'initiallySelectedLabels',
'issuableType',
'labelsFetchPath',
@@ -145,6 +146,8 @@ export default {
<labels-select-widget
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block"
+ :iid="iid"
+ :full-path="fullPath"
:allow-label-remove="allowLabelEdit"
:allow-multiselect="true"
:footer-create-label-title="__('Create project label')"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 13701cbbd64..3ee0baf8812 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -57,6 +57,15 @@ export default {
required: false,
default: false,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ attrWorkspacePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
@@ -182,6 +191,8 @@ export default {
:selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect"
:issuable-type="issuableType"
+ :full-path="fullPath"
+ :attr-workspace-path="attrWorkspacePath"
@hideCreateView="toggleDropdownContentsCreateView"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index 0f660c92e7c..a2ed08e6b28 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -19,16 +19,20 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: {
- fullPath: {
- default: '',
- },
- },
props: {
issuableType: {
type: String,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ attrWorkspacePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
@@ -46,11 +50,19 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
mutationVariables() {
- return this.issuableType === IssuableType.Epic
+ if (this.issuableType === IssuableType.Epic) {
+ return {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ groupPath: this.fullPath,
+ };
+ }
+
+ return this.attrWorkspacePath !== undefined
? {
title: this.labelTitle,
color: this.selectedColor,
- groupPath: this.fullPath,
+ groupPath: this.attrWorkspacePath,
}
: {
title: this.labelTitle,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index 2215f2cba91..e6a25362ff0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -24,7 +24,6 @@ export default {
GlIntersectionObserver,
LabelItem,
},
- inject: ['fullPath'],
model: {
prop: 'localSelectedLabels',
},
@@ -45,6 +44,10 @@ export default {
type: Array,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -84,7 +87,7 @@ export default {
return this.$apollo.queries.labels.loading;
},
localSelectedLabelsIds() {
- return this.localSelectedLabels.map((label) => label.id);
+ return this.localSelectedLabels.map((label) => getIdFromGraphQLId(label.id));
},
visibleLabels() {
if (this.searchKey) {
@@ -130,7 +133,9 @@ export default {
updateSelectedLabels(label) {
let labels;
if (this.isLabelSelected(label)) {
- labels = this.localSelectedLabels.filter(({ id }) => id !== getIdFromGraphQLId(label.id));
+ labels = this.localSelectedLabels.filter(
+ ({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id,
+ );
} else {
labels = [
...this.localSelectedLabels,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index e4ec7909bdd..6bd43da2203 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -21,15 +21,20 @@ export default {
SidebarEditableItem,
},
inject: {
- iid: {
- default: '',
- },
allowLabelEdit: {
default: false,
},
- fullPath: {},
},
props: {
+ iid: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
allowLabelRemove: {
type: Boolean,
required: false,
@@ -99,6 +104,11 @@ export default {
type: String,
required: true,
},
+ attrWorkspacePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
@@ -206,6 +216,8 @@ export default {
:variant="variant"
:issuable-type="issuableType"
:is-visible="edit"
+ :full-path="fullPath"
+ :attr-workspace-path="attrWorkspacePath"
@setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/>
@@ -224,6 +236,7 @@ export default {
:selected-labels="selectedLabels"
:variant="variant"
:issuable-type="issuableType"
+ :full-path="fullPath"
@setLabels="handleDropdownClose"
/>
</div>
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6296b023e90..a2d822bb5de 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -877,18 +877,6 @@
vertical-align: sub;
}
-.suggestion-confidential {
- color: $orange-500;
-}
-
-.suggestion-state-open {
- color: $green-500;
-}
-
-.suggestion-state-closed {
- color: $blue-500;
-}
-
.suggestion-help-hover {
cursor: help;
}
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 60708c13b85..e8e6a7e5c1a 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -11,6 +11,7 @@ class Groups::BoardsController < Groups::ApplicationController
push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml)
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml)
+ push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml)
end
feature_category :boards
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 558a4bbfd45..6e59f159636 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -54,7 +54,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def invited_members
- group_members.invite
+ group_members.invite.with_invited_user_state
end
def non_invited_members
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index 2dca9385da6..bec26cb547d 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -22,13 +22,16 @@ class Import::BulkImportsController < ApplicationController
def status
respond_to do |format|
format.json do
- data = importable_data
+ data = ::BulkImports::GetImportableDataService.new(params, query_params, credentials).execute
pagination_headers.each do |header|
- response.set_header(header, data.headers[header])
+ response.set_header(header, data[:response].headers[header])
end
- render json: { importable_data: serialized_data(data.parsed_response) }
+ json_response = { importable_data: serialized_data(data[:response].parsed_response) }
+ json_response[:version_validation] = data[:version_validation]
+
+ render json: json_response
end
format.html do
@source_url = session[url_key]
@@ -66,10 +69,6 @@ class Import::BulkImportsController < ApplicationController
@serializer ||= BaseSerializer.new(current_user: current_user)
end
- def importable_data
- client.get('groups', query_params)
- end
-
# Default query string params used to fetch groups from GitLab source instance
#
# top_level_only: fetch only top level groups (subgroups are fetched during import itself)
@@ -85,15 +84,6 @@ class Import::BulkImportsController < ApplicationController
query_params
end
- def client
- @client ||= BulkImports::Clients::HTTP.new(
- url: session[url_key],
- token: session[access_token_key],
- per_page: params[:per_page],
- page: params[:page]
- )
- end
-
def configure_params
params.permit(access_token_key, url_key)
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 316582f3994..834e4baa7dd 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -11,6 +11,7 @@ class Projects::BoardsController < Projects::ApplicationController
push_frontend_feature_flag(:issue_boards_filtered_search, project, default_enabled: :yaml)
push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
+ push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
end
feature_category :boards
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 953b9d83a66..e925bbea5e9 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -4,6 +4,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action do
push_frontend_feature_flag(:pipeline_editor_drawer, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:pipeline_editor_mini_graph, @project, default_enabled: :yaml)
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index cce2497ae25..e8074f7d793 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -58,7 +58,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def invited_members
- members.invite
+ members.invite.with_invited_user_state
end
def non_invited_members
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index e51bfe6a37e..c3c6a51239d 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -11,6 +11,9 @@ module Repositories
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception
rescue_from Gitlab::GitAccessProject::CreationError, with: :render_422_with_exception
rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception
+ rescue_from GRPC::Unavailable do |e|
+ render_503_with_exception(e, message: 'The git server, Gitaly, is not available at this time. Please contact your administrator.')
+ end
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
@@ -71,8 +74,8 @@ module Repositories
render plain: exception.message, status: :unprocessable_entity
end
- def render_503_with_exception(exception)
- render plain: exception.message, status: :service_unavailable
+ def render_503_with_exception(exception, message: nil)
+ render plain: message || exception.message, status: :service_unavailable
end
def update_fetch_statistics
diff --git a/app/graphql/types/packages/nuget/metadatum_type.rb b/app/graphql/types/packages/nuget/metadatum_type.rb
index ed9d97724af..b58fd954a74 100644
--- a/app/graphql/types/packages/nuget/metadatum_type.rb
+++ b/app/graphql/types/packages/nuget/metadatum_type.rb
@@ -10,9 +10,9 @@ module Types
authorize :read_package
field :id, ::Types::GlobalIDType[::Packages::Nuget::Metadatum], null: false, description: 'ID of the metadatum.'
- field :license_url, GraphQL::Types::String, null: false, description: 'License URL of the Nuget package.'
- field :project_url, GraphQL::Types::String, null: false, description: 'Project URL of the Nuget package.'
- field :icon_url, GraphQL::Types::String, null: false, description: 'Icon URL of the Nuget package.'
+ field :license_url, GraphQL::Types::String, null: true, description: 'License URL of the Nuget package.'
+ field :project_url, GraphQL::Types::String, null: true, description: 'Project URL of the Nuget package.'
+ field :icon_url, GraphQL::Types::String, null: true, description: 'Icon URL of the Nuget package.'
end
end
end
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
index dee55675304..818ae04ba29 100644
--- a/app/models/bulk_import.rb
+++ b/app/models/bulk_import.rb
@@ -4,7 +4,8 @@
# projects to a GitLab instance. It associates the import with the responsible
# user.
class BulkImport < ApplicationRecord
- MINIMUM_GITLAB_MAJOR_VERSION = 14
+ MIN_MAJOR_VERSION = 14
+ MIN_MINOR_VERSION_FOR_PROJECT = 4
belongs_to :user, optional: false
@@ -34,6 +35,14 @@ class BulkImport < ApplicationRecord
end
end
+ def source_version_info
+ Gitlab::VersionInfo.parse(source_version)
+ end
+
+ def self.min_gl_version_for_project_migration
+ Gitlab::VersionInfo.new(MIN_MAJOR_VERSION, MIN_MINOR_VERSION_FOR_PROJECT)
+ end
+
def self.all_human_statuses
state_machine.states.map(&:human_name)
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index d2b9e1f567a..ecac4ab95f4 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -83,9 +83,9 @@ class BulkImports::Entity < ApplicationRecord
def pipelines
@pipelines ||= case source_type
when 'group_entity'
- BulkImports::Groups::Stage.pipelines
+ BulkImports::Groups::Stage.new(bulk_import).pipelines
when 'project_entity'
- BulkImports::Projects::Stage.pipelines
+ BulkImports::Projects::Stage.new(bulk_import).pipelines
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 45af34a3e6b..7e0b4705217 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -50,6 +50,11 @@ class Member < ApplicationRecord
},
if: :project_bot?
+ scope :with_invited_user_state, -> do
+ joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
+ .select('members.*', 'invited_user.state as invited_user_state')
+ end
+
scope :in_hierarchy, ->(source) do
groups = source.root_ancestor.self_and_descendants
group_members = Member.default_scoped.where(source: groups)
diff --git a/app/models/packages/helm/file_metadatum.rb b/app/models/packages/helm/file_metadatum.rb
index 1771003d1f9..dfa4ab6df82 100644
--- a/app/models/packages/helm/file_metadatum.rb
+++ b/app/models/packages/helm/file_metadatum.rb
@@ -12,7 +12,7 @@ module Packages
validates :channel,
presence: true,
- length: { maximum: 63 },
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.helm_channel_regex }
validates :metadata,
diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb
index 5100a41638e..d7221109ecb 100644
--- a/app/serializers/member_entity.rb
+++ b/app/serializers/member_entity.rb
@@ -44,6 +44,8 @@ class MemberEntity < Grape::Entity
MemberUserEntity.represent(member.user, source: options[:source])
end
+ expose :state
+
expose :invite, if: -> (member) { member.invite? } do
expose :email do |member|
member.invite_email
@@ -56,6 +58,10 @@ class MemberEntity < Grape::Entity
expose :can_resend do |member|
member.can_resend_invite?
end
+
+ expose :user_state do |member|
+ member.respond_to?(:invited_user_state) ? member.invited_user_state : ""
+ end
end
end
diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb
index 1cea7632aa1..c1becbb5609 100644
--- a/app/services/bulk_imports/create_service.rb
+++ b/app/services/bulk_imports/create_service.rb
@@ -52,7 +52,11 @@ module BulkImports
def create_bulk_import
BulkImport.transaction do
- bulk_import = BulkImport.create!(user: current_user, source_type: 'gitlab')
+ bulk_import = BulkImport.create!(
+ user: current_user,
+ source_type: 'gitlab',
+ source_version: client.instance_version
+ )
bulk_import.create_configuration!(credentials.slice(:url, :access_token))
params.each do |entity|
@@ -68,5 +72,12 @@ module BulkImports
bulk_import
end
end
+
+ def client
+ @client ||= BulkImports::Clients::HTTP.new(
+ url: @credentials[:url],
+ token: @credentials[:access_token]
+ )
+ end
end
end
diff --git a/app/services/bulk_imports/get_importable_data_service.rb b/app/services/bulk_imports/get_importable_data_service.rb
new file mode 100644
index 00000000000..07e0b3976a1
--- /dev/null
+++ b/app/services/bulk_imports/get_importable_data_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class GetImportableDataService
+ def initialize(params, query_params, credentials)
+ @params = params
+ @query_params = query_params
+ @credentials = credentials
+ end
+
+ def execute
+ {
+ version_validation: version_validation,
+ response: importables
+ }
+ end
+
+ private
+
+ def importables
+ client.get('groups', @query_params)
+ end
+
+ def version_validation
+ {
+ features: {
+ project_migration: {
+ available: client.compatible_for_project_migration?,
+ min_version: BulkImport.min_gl_version_for_project_migration.to_s
+ },
+ source_instance_version: client.instance_version.to_s
+ }
+ }
+ end
+
+ def client
+ @client ||= BulkImports::Clients::HTTP.new(
+ url: @credentials[:url],
+ token: @credentials[:access_token],
+ per_page: @params[:per_page],
+ page: @params[:page]
+ )
+ end
+ end
+end
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index aeb274fe2cb..6a5f07dd2db 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -19,22 +19,20 @@
.col-sm-10
- editing_current_user = (current_user == @user)
- = f.radio_button :access_level, :regular, disabled: editing_current_user
- = f.label :access_level_regular, class: 'font-weight-bold' do
- = s_('AdminUsers|Regular')
- %p.light
- = s_('AdminUsers|Regular users have access to their groups and projects')
+ = f.gitlab_ui_radio_component :access_level, :regular,
+ s_('AdminUsers|Regular'),
+ radio_options: { disabled: editing_current_user },
+ help_text: s_('AdminUsers|Regular users have access to their groups and projects.')
= render_if_exists 'admin/users/auditor_access_level_radio', f: f, disabled: editing_current_user
- = f.radio_button :access_level, :admin, disabled: editing_current_user
- = f.label :access_level_admin, class: 'font-weight-bold' do
- = s_('AdminUsers|Admin')
- %p.light
- = s_('AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation')
- - if editing_current_user
- %p.light
- = s_('AdminUsers|You cannot remove your own admin rights.')
+ - help_text = s_('AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation.')
+ - help_text += ' ' + s_('AdminUsers|You cannot remove your own admin rights.') if editing_current_user
+ = f.gitlab_ui_radio_component :access_level, :admin,
+ s_('AdminUsers|Admin'),
+ radio_options: { disabled: editing_current_user },
+ help_text: help_text
+
.form-group.row
.col-sm-2.col-form-label.gl-pt-0
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 9d62c19e2fc..96fd0f5aac1 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -1,5 +1,5 @@
.user_new
- = form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
+ = gitlab_ui_form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
= form_errors(@user)
%fieldset
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index ec2904245d3..dff1b5e3d04 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -17,6 +17,7 @@
= render_two_factor_auth_recovery_settings_check
= render_if_exists "layouts/header/ee_subscribable_banner"
= render_if_exists "shared/namespace_storage_limit_alert"
+ = render_if_exists "shared/namespace_user_cap_reached_alert"
= render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :page_level_alert
= yield :customize_homepage_banner
diff --git a/app/views/projects/ci/pipeline_editor/show.html.haml b/app/views/projects/ci/pipeline_editor/show.html.haml
index 674765e9f89..ce6f7553ab4 100644
--- a/app/views/projects/ci/pipeline_editor/show.html.haml
+++ b/app/views/projects/ci/pipeline_editor/show.html.haml
@@ -1,3 +1,5 @@
+- add_page_specific_style 'page_bundles/pipelines'
+
- page_title s_('Pipelines|Pipeline Editor')
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco')
diff --git a/config/feature_flags/development/pipeline_editor_mini_graph.yml b/config/feature_flags/development/pipeline_editor_mini_graph.yml
new file mode 100644
index 00000000000..6f31cb18d82
--- /dev/null
+++ b/config/feature_flags/development/pipeline_editor_mini_graph.yml
@@ -0,0 +1,8 @@
+---
+name: pipeline_editor_mini_graph
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71622
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342217
+milestone: '14.4'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile
index 7cc68407170..117eaf61062 100644
--- a/danger/specs/Dangerfile
+++ b/danger/specs/Dangerfile
@@ -20,7 +20,7 @@ Please make sure the spec files pass in AS-IF-FOSS mode either:
1. Locally with `FOSS_ONLY=1 bin/rspec -- %<spec_files>s`.
1. In the MR pipeline by verifying that the `rspec foss-impact` job has passed.
-1. In the MR pipelines by including `RUN AS-IF-FOSS` in the MR title (you can do it with the ``/title %<mr_title>s [RUN AS-IF-FOSS]`` quick action) and start a new MR pipeline.
+1. In the MR pipelines by setting the ~"pipeline:run-as-if-foss" label on the MR (you can do it with the `/label ~"pipeline:run-as-if-foss"` quick action) and start a new MR pipeline.
MSG
@@ -46,7 +46,7 @@ end
# The only changes outside `ee/` are in `spec/`
if has_ee_app_changes && has_spec_changes && !(has_app_changes || has_ee_spec_changes)
- warn format(EE_CHANGE_WITH_FOSS_SPEC_CHANGE_MESSAGE, spec_files: spec_changes.join(" "), mr_title: gitlab.mr_json['title']), sticky: false
+ warn format(EE_CHANGE_WITH_FOSS_SPEC_CHANGE_MESSAGE, spec_files: spec_changes.join(" ")), sticky: false
end
# Forbidding a new file addition under `/spec/controllers` or `/ee/spec/controllers`
diff --git a/data/deprecations/templates/_deprecation_template.md.erb b/data/deprecations/templates/_deprecation_template.md.erb
index 5721badff36..a037151c6ac 100644
--- a/data/deprecations/templates/_deprecation_template.md.erb
+++ b/data/deprecations/templates/_deprecation_template.md.erb
@@ -6,6 +6,14 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo
# Deprecated feature removal schedule
+DISCLAIMER:
+This page contains information related to upcoming products, features, and functionality.
+It is important to note that the information presented is for informational purposes only.
+Please do not rely on this information for purchasing or planning purposes.
+As with all projects, the items mentioned on this page are subject to change or delay.
+The development, release, and timing of any products, features, or functionality remain at the
+sole discretion of GitLab Inc.
+
<!-- vale off -->
<!--
diff --git a/db/migrate/20211001001222_add_source_version_to_bulk_imports.rb b/db/migrate/20211001001222_add_source_version_to_bulk_imports.rb
new file mode 100644
index 00000000000..d0eb4a32cac
--- /dev/null
+++ b/db/migrate/20211001001222_add_source_version_to_bulk_imports.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddSourceVersionToBulkImports < Gitlab::Database::Migration[1.0]
+ def change
+ add_column :bulk_imports, :source_version, :text # rubocop:disable Migration/AddLimitToTextColumns
+ end
+end
diff --git a/db/migrate/20211006103122_change_helm_channel_length.rb b/db/migrate/20211006103122_change_helm_channel_length.rb
new file mode 100644
index 00000000000..6579ca4053b
--- /dev/null
+++ b/db/migrate/20211006103122_change_helm_channel_length.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class ChangeHelmChannelLength < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :packages_helm_file_metadata, :channel, 255, constraint_name: check_constraint_name(:packages_helm_file_metadata, :channel, 'max_length_v2')
+ remove_text_limit :packages_helm_file_metadata, :channel, constraint_name: check_constraint_name(:packages_helm_file_metadata, :channel, 'max_length')
+ end
+
+ def down
+ # no-op: Danger of failing if there are records with length(channel) > 63
+ end
+end
diff --git a/db/migrate/20211012091822_add_text_limit_to_bulk_imports_source_version.rb b/db/migrate/20211012091822_add_text_limit_to_bulk_imports_source_version.rb
new file mode 100644
index 00000000000..9b4fca9a98c
--- /dev/null
+++ b/db/migrate/20211012091822_add_text_limit_to_bulk_imports_source_version.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddTextLimitToBulkImportsSourceVersion < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :bulk_imports, :source_version, 63
+ end
+
+ def down
+ remove_text_limit :bulk_imports, :source_version
+ end
+end
diff --git a/db/schema_migrations/20211001001222 b/db/schema_migrations/20211001001222
new file mode 100644
index 00000000000..2b0ebb346a9
--- /dev/null
+++ b/db/schema_migrations/20211001001222
@@ -0,0 +1 @@
+23be5444bb11f731e98edc9b6aad814d02fd0f3f6be9abdea9060898cc2b95f1 \ No newline at end of file
diff --git a/db/schema_migrations/20211006103122 b/db/schema_migrations/20211006103122
new file mode 100644
index 00000000000..4d2347702de
--- /dev/null
+++ b/db/schema_migrations/20211006103122
@@ -0,0 +1 @@
+1e29e4712d81aacf1178996c2dd9e82593be5a2311273800d91640d8eccd38ed \ No newline at end of file
diff --git a/db/schema_migrations/20211012091822 b/db/schema_migrations/20211012091822
new file mode 100644
index 00000000000..09c198571af
--- /dev/null
+++ b/db/schema_migrations/20211012091822
@@ -0,0 +1 @@
+3482e5c12f1603cb67d24aee14f003345ef2a5c350c7dccafdea6554db04c4cc \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index ca50725b346..198b5473393 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11216,7 +11216,9 @@ CREATE TABLE bulk_imports (
source_type smallint NOT NULL,
status smallint NOT NULL,
created_at timestamp with time zone NOT NULL,
- updated_at timestamp with time zone NOT NULL
+ updated_at timestamp with time zone NOT NULL,
+ source_version text,
+ CONSTRAINT check_ea4e58775a CHECK ((char_length(source_version) <= 63))
);
CREATE SEQUENCE bulk_imports_id_seq
@@ -17062,7 +17064,7 @@ CREATE TABLE packages_helm_file_metadata (
package_file_id bigint NOT NULL,
channel text NOT NULL,
metadata jsonb,
- CONSTRAINT check_c34067922d CHECK ((char_length(channel) <= 63))
+ CONSTRAINT check_06e8d100af CHECK ((char_length(channel) <= 255))
);
CREATE TABLE packages_maven_metadata (
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 2b5ebf1473e..17d62ea19ad 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -12148,10 +12148,10 @@ Nuget metadata.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="nugetmetadataiconurl"></a>`iconUrl` | [`String!`](#string) | Icon URL of the Nuget package. |
+| <a id="nugetmetadataiconurl"></a>`iconUrl` | [`String`](#string) | Icon URL of the Nuget package. |
| <a id="nugetmetadataid"></a>`id` | [`PackagesNugetMetadatumID!`](#packagesnugetmetadatumid) | ID of the metadatum. |
-| <a id="nugetmetadatalicenseurl"></a>`licenseUrl` | [`String!`](#string) | License URL of the Nuget package. |
-| <a id="nugetmetadataprojecturl"></a>`projectUrl` | [`String!`](#string) | Project URL of the Nuget package. |
+| <a id="nugetmetadatalicenseurl"></a>`licenseUrl` | [`String`](#string) | License URL of the Nuget package. |
+| <a id="nugetmetadataprojecturl"></a>`projectUrl` | [`String`](#string) | Project URL of the Nuget package. |
### `OncallParticipantType`
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 42fb9fd42fc..7f74d9660e9 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -40,7 +40,7 @@ By default, merge request pipelines for development run in an EE-context only. I
developing features that differ between FOSS and EE, you may wish to run pipelines in a
FOSS context as well.
-To run pipelines in both contexts, include `RUN AS-IF-FOSS` in the merge request title.
+To run pipelines in both contexts, add the `~"pipeline:run-as-if-foss"` label to the merge request.
See the [As-if-FOSS jobs](pipelines.md#as-if-foss-jobs) pipelines documentation for more information.
diff --git a/doc/development/fe_guide/haml.md b/doc/development/fe_guide/haml.md
index 8f501007755..f905fdad77e 100644
--- a/doc/development/fe_guide/haml.md
+++ b/doc/development/fe_guide/haml.md
@@ -57,7 +57,7 @@ For example:
When using the GitLab UI form builder, the following components are available for use in HAML.
NOTE:
-Currently only `gitlab_ui_checkbox_component` is available but more components are planned.
+Currently only the listed components are available but more components are planned.
#### gitlab_ui_checkbox_component
@@ -72,3 +72,16 @@ Currently only `gitlab_ui_checkbox_component` is available but more components a
| `checked_value` | Value when checkbox is checked. | `String` | `false` (`'1'`) |
| `unchecked_value` | Value when checkbox is unchecked. | `String` | `false` (`'0'`) |
| `label_options` | Options that are passed to [Rails `label` method](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-label). | `Hash` | `false` (`{}`) |
+
+#### gitlab_ui_radio_component
+
+[GitLab UI Docs](https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-form-form-radio--default)
+
+| Argument | Description | Type | Required (default value) |
+|---|---|---|---|
+| `method` | Attribute on the object passed to `gitlab_ui_form_for`. | `Symbol` | `true` |
+| `value` | The value of the radio tag. | `Symbol` | `true` |
+| `label` | Radio label. | `String` | `true` |
+| `help_text` | Help text displayed below the radio button. | `String` | `false` (`nil`) |
+| `radio_options` | Options that are passed to [Rails `radio_button` method](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-radio_button). | `Hash` | `false` (`{}`) |
+| `label_options` | Options that are passed to [Rails `label` method](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-label). | `Hash` | `false` (`{}`) |
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index f09ac4f4f69..20ba630218c 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -112,14 +112,46 @@ This number can be overridden by setting a CI/CD variable named `RSPEC_FAIL_FAST
## Test jobs
-Consult [GitLab tests in the Continuous Integration (CI) context](testing_guide/ci.md)
-for more information.
-
We have dedicated jobs for each [testing level](testing_guide/testing_levels.md) and each job runs depending on the
changes made in your merge request.
If you want to force all the RSpec jobs to run regardless of your changes, you can add the `pipeline:run-all-rspec` label to the merge request.
-> Forcing all jobs on docs only related MRs would not have the prerequisite jobs and would lead to errors
+WARNING:
+Forcing all jobs on docs only related MRs would not have the prerequisite jobs and would lead to errors
+
+### Test suite parallelization
+
+Our current RSpec tests parallelization setup is as follows:
+
+1. The `retrieve-tests-metadata` job in the `prepare` stage ensures we have a
+ `knapsack/report-master.json` file:
+ - The `knapsack/report-master.json` file is fetched from the latest `main` pipeline which runs `update-tests-metadata`
+ (for now it's the 2-hourly scheduled master pipeline), if it's not here we initialize the file with `{}`.
+1. Each `[rspec|rspec-ee] [unit|integration|system|geo] n m` job are run with
+ `knapsack rspec` and should have an evenly distributed share of tests:
+ - It works because the jobs have access to the `knapsack/report-master.json`
+ since the "artifacts from all previous stages are passed by default".
+ - the jobs set their own report path to
+ `"knapsack/${TEST_TOOL}_${TEST_LEVEL}_${DATABASE}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json"`.
+ - if knapsack is doing its job, test files that are run should be listed under
+ `Report specs`, not under `Leftover specs`.
+1. The `update-tests-metadata` job (which only runs on scheduled pipelines for
+ [the canonical project](https://gitlab.com/gitlab-org/gitlab) takes all the
+ `knapsack/rspec*_pg_*.json` files and merge them all together into a single
+ `knapsack/report-master.json` file that is saved as artifact.
+
+After that, the next pipeline uses the up-to-date `knapsack/report-master.json` file.
+
+### Monitoring
+
+The GitLab test suite is [monitored](performance.md#rspec-profiling) for the `main` branch, and any branch
+that includes `rspec-profile` in their name.
+
+### Logging
+
+- Rails logging to `log/test.log` is disabled by default in CI [for
+ performance reasons](https://jtway.co/speed-up-your-rails-test-suite-by-6-in-1-line-13fedb869ec4). To override this setting, provide the
+ `RAILS_ENABLE_TEST_LOG` environment variable.
## Review app jobs
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 79664490368..52e89a10556 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -68,7 +68,7 @@ SILENCE_DEPRECATIONS=1 bin/rspec spec/models/project_spec.rb
### Test speed
-GitLab has a massive test suite that, without [parallelization](ci.md#test-suite-parallelization-on-the-ci), can take hours
+GitLab has a massive test suite that, without [parallelization](../pipelines.md#test-suite-parallelization), can take hours
to run. It's important that we make an effort to write tests that are accurate
and effective _as well as_ fast.
diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md
index e3fccdcee34..de024084c9c 100644
--- a/doc/development/testing_guide/ci.md
+++ b/doc/development/testing_guide/ci.md
@@ -1,45 +1,9 @@
---
-stage: none
-group: unassigned
-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
+redirect_to: '../pipelines.md'
+remove_date: '2022-01-12'
---
-# GitLab tests in the Continuous Integration (CI) context
+This file was moved to [another location](../pipelines.md).
-## Test suite parallelization on the CI
-
-Our current CI parallelization setup is as follows:
-
-1. The `retrieve-tests-metadata` job in the `prepare` stage ensures we have a
- `knapsack/report-master.json` file:
- - The `knapsack/report-master.json` file is fetched from the latest `main` pipeline which runs `update-tests-metadata`
- (for now it's the 2-hourly scheduled master pipeline), if it's not here we initialize the file with `{}`.
-1. Each `[rspec|rspec-ee] [unit|integration|system|geo] n m` job are run with
- `knapsack rspec` and should have an evenly distributed share of tests:
- - It works because the jobs have access to the `knapsack/report-master.json`
- since the "artifacts from all previous stages are passed by default".
- - the jobs set their own report path to
- `"knapsack/${TEST_TOOL}_${TEST_LEVEL}_${DATABASE}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json"`.
- - if knapsack is doing its job, test files that are run should be listed under
- `Report specs`, not under `Leftover specs`.
-1. The `update-tests-metadata` job (which only runs on scheduled pipelines for
- [the canonical project](https://gitlab.com/gitlab-org/gitlab) takes all the
- `knapsack/rspec*_pg_*.json` files and merge them all together into a single
- `knapsack/report-master.json` file that is saved as artifact.
-
-After that, the next pipeline uses the up-to-date `knapsack/report-master.json` file.
-
-## Monitoring
-
-The GitLab test suite is [monitored](../performance.md#rspec-profiling) for the `main` branch, and any branch
-that includes `rspec-profile` in their name.
-
-## CI setup
-
-- Rails logging to `log/test.log` is disabled by default in CI [for
- performance reasons](https://jtway.co/speed-up-your-rails-test-suite-by-6-in-1-line-13fedb869ec4). To override this setting, provide the
- `RAILS_ENABLE_TEST_LOG` environment variable.
-
----
-
-[Return to Testing documentation](index.md)
+<!-- This redirect file can be deleted after <2022-01-12>. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
index 015d8a92a4d..2e00a00c454 100644
--- a/doc/development/testing_guide/index.md
+++ b/doc/development/testing_guide/index.md
@@ -48,7 +48,7 @@ testing promises, stubbing etc.
What are flaky tests, the different kind of flaky tests we encountered, and what
we do about them.
-## [GitLab tests in the Continuous Integration (CI) context](ci.md)
+## [GitLab pipelines](../pipelines.md)
How GitLab test suite is run in the CI context: setup, caches, artifacts,
parallelization, monitoring.
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 21ab73faa0d..3e92998e9a7 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -197,15 +197,13 @@ For example configurations, see the [notes on specific providers](#providers).
| Field | Supported keys |
|-----------------|----------------|
| Email (required)| `email`, `mail` |
-| Username | `username`, `nickname` |
| Full Name | `name` |
| First Name | `first_name`, `firstname`, `firstName` |
| Last Name | `last_name`, `lastname`, `lastName` |
-If a username is not specified, the email address is used to generate the GitLab username.
-
-See [`attribute_statements`](#attribute_statements) for examples on how the
-assertions are configured.
+See [`attribute_statements`](#attribute_statements) for examples on how custom
+assertions are configured. This section also describes how to configure custom
+username attributes.
Please refer to [the OmniAuth SAML gem](https://github.com/omniauth/omniauth-saml/blob/master/lib/omniauth/strategies/saml.rb)
for a full list of supported assertions.
@@ -444,7 +442,7 @@ SAML users has an administrator role.
You may also bypass the auto sign-in feature by browsing to
`https://gitlab.example.com/users/sign_in?auto_sign_in=false`.
-### `attribute_statements`
+### `attribute_statements` **(FREE SELF)**
NOTE:
This setting should be used only to map attributes that are part of the OmniAuth
@@ -476,11 +474,10 @@ args: {
#### Set a username
-By default, the email in the SAML response is used to automatically generate the
-user's GitLab username.
+By default, the local part of the email address in the SAML response is used to
+generate the user's GitLab username.
-If you'd like to set another attribute as the username, assign it to the `nickname` OmniAuth `info`
-hash attribute, and add the following setting to your configuration file:
+Configure `nickname` in `attribute_statements` to specify one or more attributes that contain a user's desired username:
```yaml
args: {
diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md
index ecd4aaa7f0b..c01ed4a49d0 100644
--- a/doc/topics/autodevops/customize.md
+++ b/doc/topics/autodevops/customize.md
@@ -58,7 +58,7 @@ If your goal is to use only a single custom buildpack, you should provide the pr
## Custom `Dockerfile`
-> Support for `DOCKERFILE_PATH` was [added in GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35662)
+> Support for `DOCKERFILE_PATH` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35662) in GitLab 13.2
If your project has a `Dockerfile` in the root of the project repository, Auto DevOps
builds a Docker image based on the Dockerfile, rather than using buildpacks.
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index e232af05d50..9340f89c502 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -131,7 +131,7 @@ following levels:
| Instance type | [Project](#at-the-project-level) | [Group](#at-the-group-level) | [Instance](#at-the-instance-level) (Admin Area) |
|---------------------|------------------------|------------------------|------------------------|
-| GitLab SaaS | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
+| GitLab SaaS | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
| GitLab self-managed | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
Before enabling Auto DevOps, consider [preparing it for deployment](requirements.md). If you don't, Auto DevOps can build and test your app,
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 4ee412f5c65..c84b5e4d9c7 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -199,13 +199,13 @@ The jobs are separated into stages:
vulnerabilities and is allowed to fail ([Auto Container Scanning](stages.md#auto-container-scanning))
- The `dependency_scanning` job checks if the application has any dependencies
susceptible to vulnerabilities and is allowed to fail
- ([Auto Dependency Scanning](stages.md#auto-dependency-scanning)) **(ULTIMATE)**
+ ([Auto Dependency Scanning](stages.md#auto-dependency-scanning))
- Jobs suffixed with `-sast` run static analysis on the current code to check for potential
- security issues, and are allowed to fail ([Auto SAST](stages.md#auto-sast)) **(ULTIMATE)**
- - The `secret-detection` job checks for leaked secrets and is allowed to fail ([Auto Secret Detection](stages.md#auto-secret-detection)) **(ULTIMATE)**
+ security issues, and are allowed to fail ([Auto SAST](stages.md#auto-sast))
+ - The `secret-detection` job checks for leaked secrets and is allowed to fail ([Auto Secret Detection](stages.md#auto-secret-detection))
- The `license_scanning` job searches the application's dependencies to determine each of their
licenses and is allowed to fail
- ([Auto License Compliance](stages.md#auto-license-compliance)) **(ULTIMATE)**
+ ([Auto License Compliance](stages.md#auto-license-compliance))
- **Review** - Pipelines on the default branch include this stage with a `dast_environment_deploy` job.
To learn more, see [Dynamic Application Security Testing (DAST)](../../user/application_security/dast/index.md).
@@ -214,7 +214,7 @@ The jobs are separated into stages:
Kubernetes ([Auto Deploy](stages.md#auto-deploy)).
- **Performance** - Performance tests are run on the deployed application
- ([Auto Browser Performance Testing](stages.md#auto-browser-performance-testing)). **(PREMIUM)**
+ ([Auto Browser Performance Testing](stages.md#auto-browser-performance-testing)).
- **Cleanup** - Pipelines on the default branch include this stage with a `stop_dast_environment` job.
@@ -323,7 +323,7 @@ and customized to fit your workflow. Here are some helpful resources for further
1. [Auto DevOps](index.md)
1. [Multiple Kubernetes clusters](multiple_clusters_auto_devops.md)
-1. [Incremental rollout to production](customize.md#incremental-rollout-to-production) **(PREMIUM)**
+1. [Incremental rollout to production](customize.md#incremental-rollout-to-production)
1. [Disable jobs you don't need with CI/CD variables](customize.md#cicd-variables)
1. [Use your own buildpacks to build your application](customize.md#custom-buildpacks)
1. [Prometheus monitoring](../../user/project/integrations/prometheus.md)
diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md
index 9e6f3103664..ead2e957684 100644
--- a/doc/topics/autodevops/stages.md
+++ b/doc/topics/autodevops/stages.md
@@ -35,7 +35,7 @@ your own `Dockerfile`, you must either:
### Auto Build using Cloud Native Buildpacks
-> - Introduced in [GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28165).
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28165) in GitLab 12.10.
> - Auto Build using Cloud Native Buildpacks by default was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63351) in GitLab 14.0.
Auto Build builds an application using a project's `Dockerfile` if present. If no
@@ -147,7 +147,7 @@ might want to use a [custom buildpack](customize.md#custom-buildpacks).
## Auto Code Quality
-> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) to GitLab Free in 13.2.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) from GitLab Starter to GitLab Free in 13.2.
Auto Code Quality uses the
[Code Quality image](https://gitlab.com/gitlab-org/ci-cd/codequality) to run
@@ -174,8 +174,8 @@ see the documentation.
## Auto Secret Detection
-> - Introduced in GitLab Ultimate 13.1.
-> - [Select functionality made available in all tiers](../../user/application_security/secret_detection/#making-secret-detection-available-to-all-gitlab-tiers) in 13.3
+> - Introduced in GitLab 13.1.
+> - Select functionality [made available](../../user/application_security/secret_detection/#making-secret-detection-available-to-all-gitlab-tiers) in all tiers in GitLab 13.3
Secret Detection uses the
[Secret Detection Docker image](https://gitlab.com/gitlab-org/security-products/analyzers/secrets) to run Secret Detection on the current code, and checks for leaked secrets. Auto Secret Detection requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 or above.
@@ -202,7 +202,7 @@ see the documentation.
## Auto License Compliance **(ULTIMATE)**
-> Introduced in GitLab Ultimate 11.0.
+> Introduced in GitLab 11.0.
License Compliance uses the
[License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/analyzers/license-finder)
@@ -310,7 +310,7 @@ You can disable DAST:
## Auto Browser Performance Testing **(PREMIUM)**
-> Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.4.
+> Introduced in GitLab 10.4.
Auto [Browser Performance Testing](../../user/project/merge_requests/browser_performance_testing.md)
measures the browser performance of a web page with the
@@ -331,7 +331,7 @@ Any browser performance differences between the source and target branches are a
## Auto Load Performance Testing **(PREMIUM)**
-> Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
+> Introduced in GitLab 13.2.
Auto [Load Performance Testing](../../user/project/merge_requests/load_performance_testing.md)
measures the server performance of an application with the
@@ -348,7 +348,7 @@ Any load performance test result differences between the source and target branc
## Auto Deploy
-[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216008) in GitLab 13.6, you have the choice to deploy to [Amazon Elastic Compute Cloud (Amazon EC2)](https://aws.amazon.com/ec2/) in addition to a Kubernetes cluster.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216008) in GitLab 13.6, you have the choice to deploy to [Amazon Elastic Compute Cloud (Amazon EC2)](https://aws.amazon.com/ec2/) in addition to a Kubernetes cluster.
Auto Deploy is an optional step for Auto DevOps. If the [requirements](requirements.md) are not met, the job is skipped.
diff --git a/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md b/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md
index 7ddcdcbacb5..8c460247734 100644
--- a/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md
+++ b/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference
---
-# Upgrading deployments for newer Auto Deploy dependencies
+# Upgrading deployments for newer Auto Deploy dependencies **(FREE)**
[Auto Deploy](stages.md#auto-deploy) is a feature that deploys your application to a Kubernetes cluster.
It consists of several dependencies:
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index c1bf316a4ae..e2af4f453c0 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -6,6 +6,14 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo
# Deprecated feature removal schedule
+DISCLAIMER:
+This page contains information related to upcoming products, features, and functionality.
+It is important to note that the information presented is for informational purposes only.
+Please do not rely on this information for purchasing or planning purposes.
+As with all projects, the items mentioned on this page are subject to change or delay.
+The development, release, and timing of any products, features, or functionality remain at the
+sole discretion of GitLab Inc.
+
<!-- vale off -->
<!--
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 4d10110efa9..328d1fa506d 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -67,9 +67,8 @@ the user details need to be passed to GitLab as SAML assertions.
At a minimum, the user's email address *must* be specified as an assertion named `email` or `mail`.
See [the assertions list](../../../integration/saml.md#assertions) for other available claims.
-
-NOTE:
-The `username` assertion is not supported for GitLab.com SaaS integrations.
+In addition to the attributes in the linked assertions list, GitLab.com supports `username`
+and `nickname` attributes.
### Metadata configuration
@@ -268,6 +267,9 @@ convert the information to XML. An example SAML response is shown here.
<saml2:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">user.email</saml2:AttributeValue>
</saml2:Attribute>
+ <saml2:Attribute Name="username" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+ <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">user.nickName</saml2:AttributeValue>
+ </saml2:Attribute>
<saml2:Attribute Name="first_name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">user.firstName</saml2:AttributeValue>
</saml2:Attribute>
diff --git a/lib/bulk_imports/clients/graphql.rb b/lib/bulk_imports/clients/graphql.rb
index 43ad9f0aa2d..a9f908a4247 100644
--- a/lib/bulk_imports/clients/graphql.rb
+++ b/lib/bulk_imports/clients/graphql.rb
@@ -57,7 +57,7 @@ module BulkImports
response = client.execute('{ metadata { version } }')
version = Gitlab::VersionInfo.parse(response.data.metadata.version)
- if version.major < BulkImport::MINIMUM_GITLAB_MAJOR_VERSION
+ if version.major < BulkImport::MIN_MAJOR_VERSION
raise ::BulkImports::Error.unsupported_gitlab_version
else
@compatible_instance_version = true
diff --git a/lib/bulk_imports/clients/http.rb b/lib/bulk_imports/clients/http.rb
index f98d4a5eb14..90414a875c6 100644
--- a/lib/bulk_imports/clients/http.rb
+++ b/lib/bulk_imports/clients/http.rb
@@ -3,6 +3,8 @@
module BulkImports
module Clients
class HTTP
+ include Gitlab::Utils::StrongMemoize
+
API_VERSION = 'v4'
DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 30
@@ -52,24 +54,32 @@ module BulkImports
Gitlab::Utils.append_path(api_url, resource)
end
- def validate_instance_version!
- return if @compatible_instance_version
+ def instance_version
+ strong_memoize(:instance_version) do
+ response = with_error_handling do
+ Gitlab::HTTP.get(resource_url(:version), default_options)
+ end
- response = with_error_handling do
- Gitlab::HTTP.get(resource_url(:version), default_options)
+ Gitlab::VersionInfo.parse(response.parsed_response['version'])
end
+ end
+
+ def compatible_for_project_migration?
+ instance_version >= BulkImport.min_gl_version_for_project_migration
+ end
- version = Gitlab::VersionInfo.parse(response.parsed_response['version'])
+ private
+
+ def validate_instance_version!
+ return if @compatible_instance_version
- if version.major < BulkImport::MINIMUM_GITLAB_MAJOR_VERSION
+ if instance_version.major < BulkImport::MIN_MAJOR_VERSION
raise ::BulkImports::Error.unsupported_gitlab_version
else
@compatible_instance_version = true
end
end
- private
-
# rubocop:disable GitlabSecurity/PublicSend
def request(method, resource, options = {}, &block)
validate_instance_version!
diff --git a/lib/bulk_imports/error.rb b/lib/bulk_imports/error.rb
index 0464aea642e..988982d3cdf 100644
--- a/lib/bulk_imports/error.rb
+++ b/lib/bulk_imports/error.rb
@@ -3,7 +3,7 @@
module BulkImports
class Error < StandardError
def self.unsupported_gitlab_version
- self.new("Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MINIMUM_GITLAB_MAJOR_VERSION}.")
+ self.new("Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MIN_MAJOR_VERSION}.")
end
end
end
diff --git a/lib/bulk_imports/groups/stage.rb b/lib/bulk_imports/groups/stage.rb
index 1a094ea56be..398b954dc6f 100644
--- a/lib/bulk_imports/groups/stage.rb
+++ b/lib/bulk_imports/groups/stage.rb
@@ -47,7 +47,7 @@ module BulkImports
end
def project_entities_pipeline
- if ::Feature.enabled?(:bulk_import_projects, default_enabled: :yaml)
+ if project_pipeline_available? && ::Feature.enabled?(:bulk_import_projects, default_enabled: :yaml)
{
project_entities: {
pipeline: BulkImports::Groups::Pipelines::ProjectEntitiesPipeline,
@@ -58,6 +58,10 @@ module BulkImports
{}
end
end
+
+ def project_pipeline_available?
+ @bulk_import.source_version_info >= BulkImport.min_gl_version_for_project_migration
+ end
end
end
end
diff --git a/lib/bulk_imports/stage.rb b/lib/bulk_imports/stage.rb
index 103623cd030..9c19e9ea60b 100644
--- a/lib/bulk_imports/stage.rb
+++ b/lib/bulk_imports/stage.rb
@@ -2,8 +2,10 @@
module BulkImports
class Stage
- def self.pipelines
- new.pipelines
+ def initialize(bulk_import)
+ raise(ArgumentError, 'Expected an argument of type ::BulkImport') unless bulk_import.is_a?(::BulkImport)
+
+ @bulk_import = bulk_import
end
def pipelines
diff --git a/lib/gitlab/form_builders/gitlab_ui_form_builder.rb b/lib/gitlab/form_builders/gitlab_ui_form_builder.rb
index a5290508e42..3f9053d4e0c 100644
--- a/lib/gitlab/form_builders/gitlab_ui_form_builder.rb
+++ b/lib/gitlab/form_builders/gitlab_ui_form_builder.rb
@@ -22,29 +22,53 @@ module Gitlab
format_options(checkbox_options, ['custom-control-input']),
checked_value,
unchecked_value
- ) +
- @template.label(
- @object_name, method, format_options(label_options, ['custom-control-label'])
- ) do
- if help_text
- @template.content_tag(
- :span,
- label
- ) +
- @template.content_tag(
- :p,
- help_text,
- class: 'help-text'
- )
- else
- label
- end
- end
+ ) + generic_label(method, label, label_options, help_text: help_text)
+ end
+ end
+
+ def gitlab_ui_radio_component(
+ method,
+ value,
+ label,
+ help_text: nil,
+ radio_options: {},
+ label_options: {}
+ )
+ @template.content_tag(
+ :div,
+ class: 'gl-form-radio custom-control custom-radio'
+ ) do
+ @template.radio_button(
+ @object_name,
+ method,
+ value,
+ format_options(radio_options, ['custom-control-input'])
+ ) + generic_label(method, label, label_options, help_text: help_text, value: value)
end
end
private
+ def generic_label(method, label, label_options, help_text: nil, value: nil)
+ @template.label(
+ @object_name, method, format_options(label_options.merge({ value: value }), ['custom-control-label'])
+ ) do
+ if help_text
+ @template.content_tag(
+ :span,
+ label
+ ) +
+ @template.content_tag(
+ :p,
+ help_text,
+ class: 'help-text'
+ )
+ else
+ label
+ end
+ end
+ end
+
def format_options(options, classes)
classes << options[:class]
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 6321b71a587..dbb56f3825d 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -131,7 +131,7 @@ module Gitlab
end
def helm_channel_regex
- @helm_channel_regex ||= %r{\A([a-zA-Z0-9](\.|-|_)?){1,63}(?<!\.|-|_)\z}.freeze
+ @helm_channel_regex ||= %r{\A([a-zA-Z0-9](\.|-|_)?){1,255}(?<!\.|-|_)\z}.freeze
end
def helm_package_regex
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 239eb4ff747..31c9ec02916 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2567,7 +2567,7 @@ msgstr ""
msgid "AdminUsers|Admin"
msgstr ""
-msgid "AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation"
+msgid "AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation."
msgstr ""
msgid "AdminUsers|Admins"
@@ -2582,6 +2582,12 @@ msgstr ""
msgid "AdminUsers|Approved users can:"
msgstr ""
+msgid "AdminUsers|Auditor"
+msgstr ""
+
+msgid "AdminUsers|Auditors have read-only access to all groups, projects, and users."
+msgstr ""
+
msgid "AdminUsers|Automatically marked as default internal user"
msgstr ""
@@ -2717,7 +2723,7 @@ msgstr ""
msgid "AdminUsers|Regular"
msgstr ""
-msgid "AdminUsers|Regular users have access to their groups and projects"
+msgid "AdminUsers|Regular users have access to their groups and projects."
msgstr ""
msgid "AdminUsers|Reject"
@@ -7942,6 +7948,9 @@ msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|Select a VPC"
+msgstr ""
+
msgid "ClusterIntegration|Select a VPC to choose a security group"
msgstr ""
@@ -7951,6 +7960,9 @@ msgstr ""
msgid "ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{linkStart}Amazon Web Services %{linkEnd}."
msgstr ""
+msgid "ClusterIntegration|Select a network"
+msgstr ""
+
msgid "ClusterIntegration|Select a network to choose a subnetwork"
msgstr ""
@@ -7960,9 +7972,24 @@ msgstr ""
msgid "ClusterIntegration|Select a region to choose a VPC"
msgstr ""
+msgid "ClusterIntegration|Select a security group"
+msgstr ""
+
+msgid "ClusterIntegration|Select a subnet"
+msgstr ""
+
+msgid "ClusterIntegration|Select a subnetwork"
+msgstr ""
+
msgid "ClusterIntegration|Select a zone to choose a network"
msgstr ""
+msgid "ClusterIntegration|Select an instance type"
+msgstr ""
+
+msgid "ClusterIntegration|Select key pair"
+msgstr ""
+
msgid "ClusterIntegration|Select machine type"
msgstr ""
@@ -7975,6 +8002,9 @@ msgstr ""
msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
+msgid "ClusterIntegration|Select service role"
+msgstr ""
+
msgid "ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{linkStart}Amazon Web Services%{linkEnd}."
msgstr ""
@@ -8125,30 +8155,6 @@ msgstr ""
msgid "ClusterIntegration|sign up"
msgstr ""
-msgid "ClusterIntergation|Select a VPC"
-msgstr ""
-
-msgid "ClusterIntergation|Select a network"
-msgstr ""
-
-msgid "ClusterIntergation|Select a security group"
-msgstr ""
-
-msgid "ClusterIntergation|Select a subnet"
-msgstr ""
-
-msgid "ClusterIntergation|Select a subnetwork"
-msgstr ""
-
-msgid "ClusterIntergation|Select an instance type"
-msgstr ""
-
-msgid "ClusterIntergation|Select key pair"
-msgstr ""
-
-msgid "ClusterIntergation|Select service role"
-msgstr ""
-
msgid "Clusters|An error occurred while loading clusters"
msgstr ""
@@ -9546,6 +9552,9 @@ msgstr ""
msgid "Create %{type}"
msgstr ""
+msgid "Create %{workspace} label"
+msgstr ""
+
msgid "Create New Directory"
msgstr ""
@@ -12831,9 +12840,6 @@ msgstr ""
msgid "Enter an integer number between 0 and 100"
msgstr ""
-msgid "Enter an integer number number between 0 and 100"
-msgstr ""
-
msgid "Enter any color or choose one of the suggested colors below."
msgstr ""
@@ -20769,6 +20775,9 @@ msgstr ""
msgid "Makes this issue confidential."
msgstr ""
+msgid "Manage %{workspace} labels"
+msgstr ""
+
msgid "Manage Web IDE features."
msgstr ""
@@ -23799,7 +23808,7 @@ msgstr ""
msgid "OnDemandScans|On-demand scans"
msgstr ""
-msgid "OnDemandScans|On-demand scans run outside of DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Lean more%{learnMoreLinkEnd}."
+msgid "OnDemandScans|On-demand scans run outside of DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Learn more%{learnMoreLinkEnd}."
msgstr ""
msgid "OnDemandScans|On-demand scans run outside the DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Learn more%{learnMoreLinkEnd}"
@@ -24737,6 +24746,9 @@ msgstr ""
msgid "Pending comments"
msgstr ""
+msgid "Pending owner approval"
+msgstr ""
+
msgid "Pending sync…"
msgstr ""
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index 1f4b119a058..3adba32c74a 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -51,62 +51,87 @@ RSpec.describe Import::BulkImportsController do
end
describe 'GET status' do
+ def get_status(params_override = {})
+ params = { page: 1, per_page: 20, filter: '' }.merge(params_override)
+
+ get :status,
+ params: params,
+ format: :json,
+ session: {
+ bulk_import_gitlab_url: 'https://gitlab.example.com',
+ bulk_import_gitlab_access_token: 'demo-pat'
+ }
+ end
+
+ include_context 'bulk imports requests context', 'https://gitlab.example.com'
+
let(:client) { BulkImports::Clients::HTTP.new(url: 'http://gitlab.example', token: 'token') }
+ let(:version) { "#{BulkImport::MIN_MAJOR_VERSION}.#{BulkImport::MIN_MINOR_VERSION_FOR_PROJECT}.0" }
+ let(:version_response) { double(code: 200, success?: true, parsed_response: { 'version' => version }) }
describe 'serialized group data' do
- let(:client_response) do
+ let(:expected_response) do
double(
parsed_response: [
- { 'id' => 1, 'full_name' => 'group1', 'full_path' => 'full/path/group1', 'web_url' => 'http://demo.host/full/path/group1' },
- { 'id' => 2, 'full_name' => 'group2', 'full_path' => 'full/path/group2', 'web_url' => 'http://demo.host/full/path/group1' }
+ {
+ "full_name" => "Stub",
+ "full_path" => "stub-group",
+ "id" => 2595438,
+ "web_url" => "https://gitlab.com/groups/auto-breakfast"
+ }
],
headers: {
'x-next-page' => '2',
'x-page' => '1',
'x-per-page' => '20',
- 'x-total' => '37',
+ 'x-total' => '42',
'x-total-pages' => '2'
}
)
end
- let(:client_params) do
- {
- top_level_only: true,
- min_access_level: Gitlab::Access::OWNER
- }
- end
-
- before do
- allow(controller).to receive(:client).and_return(client)
- allow(client).to receive(:get).with('groups', client_params).and_return(client_response)
- end
-
it 'returns serialized group data' do
- get :status, format: :json
+ get_status
+
+ version_validation = {
+ "features" => {
+ "project_migration" => {
+ "available" => true,
+ "min_version" => BulkImport.min_gl_version_for_project_migration.to_s
+ },
+ "source_instance_version" => version
+ }
+ }
- expect(json_response).to eq({ importable_data: client_response.parsed_response }.as_json)
+ expect(json_response).to include("importable_data" => expected_response.parsed_response, "version_validation" => hash_including(version_validation))
end
it 'forwards pagination headers' do
- get :status, format: :json
-
- expect(response.headers['x-per-page']).to eq client_response.headers['x-per-page']
- expect(response.headers['x-page']).to eq client_response.headers['x-page']
- expect(response.headers['x-next-page']).to eq client_response.headers['x-next-page']
- expect(response.headers['x-prev-page']).to eq client_response.headers['x-prev-page']
- expect(response.headers['x-total']).to eq client_response.headers['x-total']
- expect(response.headers['x-total-pages']).to eq client_response.headers['x-total-pages']
+ get_status
+
+ expect(response.headers['x-per-page']).to eq expected_response.headers['x-per-page']
+ expect(response.headers['x-page']).to eq expected_response.headers['x-page']
+ expect(response.headers['x-next-page']).to eq expected_response.headers['x-next-page']
+ expect(response.headers['x-prev-page']).to eq expected_response.headers['x-prev-page']
+ expect(response.headers['x-total']).to eq expected_response.headers['x-total']
+ expect(response.headers['x-total-pages']).to eq expected_response.headers['x-total-pages']
end
context 'when filtering' do
- it 'returns filtered result' do
- filter = 'test'
- search_params = client_params.merge(search: filter)
+ let_it_be(:filter) { 'test' }
- expect(client).to receive(:get).with('groups', search_params).and_return(client_response)
+ let(:client_params) do
+ {
+ top_level_only: true,
+ min_access_level: Gitlab::Access::OWNER,
+ search: filter
+ }
+ end
+
+ it 'returns filtered result' do
+ get_status(filter: filter)
- get :status, format: :json, params: { filter: filter }
+ expect(json_response['importable_data'].first['full_name']).to eq('Test')
end
end
end
@@ -148,18 +173,19 @@ RSpec.describe Import::BulkImportsController do
context 'when connection error occurs' do
before do
- allow(controller).to receive(:client).and_return(client)
- allow(client).to receive(:get).and_raise(BulkImports::Error)
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
+ allow(instance).to receive(:get).and_raise(BulkImports::Error)
+ end
end
it 'returns 422' do
- get :status, format: :json
+ get_status
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
it 'clears session' do
- get :status, format: :json
+ get_status
expect(session[:gitlab_url]).to be_nil
expect(session[:gitlab_access_token]).to be_nil
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 04d5008cb34..b5cd14154a3 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -7,12 +7,33 @@ RSpec.describe Repositories::GitHttpController do
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) }
+ shared_examples 'handles unavailable Gitaly' do
+ let(:params) { super().merge(service: 'git-upload-pack') }
+
+ before do
+ request.headers.merge! auth_env(user.username, user.password, nil)
+ end
+
+ context 'when Gitaly is unavailable' do
+ it 'responds with a 503 message' do
+ expect(Gitlab::GitalyClient).to receive(:call).and_raise(GRPC::Unavailable)
+
+ get :info_refs, params: params
+
+ expect(response).to have_gitlab_http_status(:service_unavailable)
+ expect(response.body).to eq('The git server, Gitaly, is not available at this time. Please contact your administrator.')
+ end
+ end
+ end
+
context 'when repository container is a project' do
it_behaves_like Repositories::GitHttpController do
let(:container) { project }
let(:user) { project.owner }
let(:access_checker_class) { Gitlab::GitAccess }
+ it_behaves_like 'handles unavailable Gitaly'
+
describe 'POST #git_upload_pack' do
before do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
@@ -84,6 +105,8 @@ RSpec.describe Repositories::GitHttpController do
let(:container) { personal_snippet }
let(:user) { personal_snippet.author }
let(:access_checker_class) { Gitlab::GitAccessSnippet }
+
+ it_behaves_like 'handles unavailable Gitaly'
end
end
@@ -92,6 +115,8 @@ RSpec.describe Repositories::GitHttpController do
let(:container) { project_snippet }
let(:user) { project_snippet.author }
let(:access_checker_class) { Gitlab::GitAccessSnippet }
+
+ it_behaves_like 'handles unavailable Gitaly'
end
end
end
diff --git a/spec/factories/bulk_import.rb b/spec/factories/bulk_import.rb
index 07907bab3df..748afc0c67c 100644
--- a/spec/factories/bulk_import.rb
+++ b/spec/factories/bulk_import.rb
@@ -4,6 +4,7 @@ FactoryBot.define do
factory :bulk_import, class: 'BulkImport' do
user
source_type { :gitlab }
+ source_version { BulkImport.min_gl_version_for_project_migration.to_s }
trait :created do
status { 0 }
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index fa16f47f69a..511233b50c0 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -29,12 +29,11 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
end
context 'labels' do
- # https://gitlab.com/gitlab-org/gitlab/-/issues/322725
- xit 'shows current labels when editing' do
+ it 'shows current labels when editing' do
click_card(card)
page.within('.labels') do
- click_link 'Edit'
+ click_button 'Edit'
wait_for_requests
@@ -54,9 +53,9 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
wait_for_requests
- click_link bug.title
+ click_on bug.title
- find('[data-testid="close-icon"]').click
+ click_button 'Close'
wait_for_requests
@@ -79,11 +78,11 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
wait_for_requests
- click_link bug.title
+ click_on bug.title
- click_link regression.title
+ click_on regression.title
- find('[data-testid="close-icon"]').click
+ click_button 'Close'
wait_for_requests
@@ -108,9 +107,9 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
wait_for_requests
- click_link stretch.title
+ click_button stretch.title
- find('[data-testid="close-icon"]').click
+ click_button 'Close'
wait_for_requests
@@ -125,43 +124,22 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
expect(card).not_to have_content(stretch.title)
end
- # https://gitlab.com/gitlab-org/gitlab/-/issues/324290
- xit 'creates project label' do
+ it 'creates project label' do
click_card(card)
page.within('.labels') do
- click_link 'Edit'
+ click_button 'Edit'
wait_for_requests
- click_link 'Create project label'
- fill_in 'new_label_name', with: 'test label'
+ click_on 'Create project label'
+ fill_in 'Name new label', with: 'test label'
first('.suggest-colors-dropdown a').click
click_button 'Create'
wait_for_requests
- expect(page).to have_link 'test label'
+ expect(page).to have_button 'test label'
end
expect(page).to have_selector('.board', count: 3)
end
-
- # https://gitlab.com/gitlab-org/gitlab/-/issues/324290
- xit 'creates project label and list' do
- click_card(card)
-
- page.within('.labels') do
- click_link 'Edit'
- wait_for_requests
-
- click_link 'Create project label'
- fill_in 'new_label_name', with: 'test label'
- first('.suggest-colors-dropdown a').click
- first('.js-add-list').click
- click_button 'Create'
- wait_for_requests
-
- expect(page).to have_link 'test label'
- end
- expect(page).to have_selector('.board', count: 4)
- end
end
end
diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb
index cf893e444c4..552b599a3f3 100644
--- a/spec/features/groups/import_export/connect_instance_spec.rb
+++ b/spec/features/groups/import_export/connect_instance_spec.rb
@@ -19,34 +19,12 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
end
context 'when the user provides valid credentials' do
+ source_url = 'https://gitlab.com'
+
+ include_context 'bulk imports requests context', source_url
+
it 'successfully connects to remote instance' do
- source_url = 'https://gitlab.com'
pat = 'demo-pat'
- stub_path = 'stub-group'
- total = 37
-
- stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=50&search=" % { url: source_url }).to_return(
- body: [{
- id: 2595438,
- web_url: 'https://gitlab.com/groups/auto-breakfast',
- name: 'Stub',
- path: stub_path,
- full_name: 'Stub',
- full_path: stub_path
- }].to_json,
- headers: {
- 'Content-Type' => 'application/json',
- 'X-Next-Page' => 2,
- 'X-Page' => 1,
- 'X-Per-Page' => 20,
- 'X-Total' => total,
- 'X-Total-Pages' => 2
- }
- )
-
- allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
- allow(client).to receive(:validate_instance_version!).and_return(true)
- end
expect(page).to have_content 'Import groups from another instance of GitLab'
expect(page).to have_content 'Not all related objects are migrated'
@@ -56,8 +34,8 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
click_on 'Connect instance'
- expect(page).to have_content 'Showing 1-1 of %{total} groups from %{url}' % { url: source_url, total: total }
- expect(page).to have_content stub_path
+ expect(page).to have_content 'Showing 1-1 of 42 groups from %{url}' % { url: source_url }
+ expect(page).to have_content 'stub-group'
visit '/'
diff --git a/spec/fixtures/api/schemas/entities/member.json b/spec/fixtures/api/schemas/entities/member.json
index f06687f9809..dec98123e85 100644
--- a/spec/fixtures/api/schemas/entities/member.json
+++ b/spec/fixtures/api/schemas/entities/member.json
@@ -56,13 +56,15 @@
{ "$ref": "member_user.json" }
]
},
+ "state": { "type": "integer" },
"invite": {
"type": "object",
- "required": ["email", "avatar_url", "can_resend"],
+ "required": ["email", "avatar_url", "can_resend", "user_state"],
"properties": {
"email": { "type": "string" },
"avatar_url": { "type": "string" },
- "can_resend": { "type": "boolean" }
+ "can_resend": { "type": "boolean" },
+ "user_state": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index d29d5c18854..0b90912a584 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1577,7 +1577,7 @@ describe('setActiveIssueLabels', () => {
projectPath: 'h/b',
};
- it('should assign labels on success', (done) => {
+ it('should assign labels on success, and sets loading state for labels', (done) => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
@@ -1594,6 +1594,14 @@ describe('setActiveIssueLabels', () => {
{ ...state, ...getters },
[
{
+ type: types.SET_LABELS_LOADING,
+ payload: true,
+ },
+ {
+ type: types.SET_LABELS_LOADING,
+ payload: false,
+ },
+ {
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
diff --git a/spec/frontend/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js
index 39083b3d8fb..45f96103e3e 100644
--- a/spec/frontend/issuable_suggestions/components/item_spec.js
+++ b/spec/frontend/issuable_suggestions/components/item_spec.js
@@ -6,10 +6,10 @@ import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_ima
import mockData from '../mock_data';
describe('Issuable suggestions suggestion component', () => {
- let vm;
+ let wrapper;
function createComponent(suggestion = {}) {
- vm = shallowMount(Suggestion, {
+ wrapper = shallowMount(Suggestion, {
propsData: {
suggestion: {
...mockData(),
@@ -19,37 +19,40 @@ describe('Issuable suggestions suggestion component', () => {
});
}
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findAuthorLink = () => wrapper.findAll(GlLink).at(1);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
+ const findUserAvatar = () => wrapper.findComponent(UserAvatarImage);
+
afterEach(() => {
- vm.destroy();
+ wrapper.destroy();
});
it('renders title', () => {
createComponent();
- expect(vm.text()).toContain('Test issue');
+ expect(wrapper.text()).toContain('Test issue');
});
it('renders issue link', () => {
createComponent();
- const link = vm.find(GlLink);
-
- expect(link.attributes('href')).toBe(`${TEST_HOST}/test/issue/1`);
+ expect(findLink().attributes('href')).toBe(`${TEST_HOST}/test/issue/1`);
});
it('renders IID', () => {
createComponent();
- expect(vm.text()).toContain('#1');
+ expect(wrapper.text()).toContain('#1');
});
describe('opened state', () => {
it('renders icon', () => {
createComponent();
- const icon = vm.find(GlIcon);
-
- expect(icon.props('name')).toBe('issue-open-m');
+ expect(findIcon().props('name')).toBe('issue-open-m');
+ expect(findIcon().attributes('class')).toMatch('gl-text-green-500');
});
it('renders created timeago', () => {
@@ -57,10 +60,8 @@ describe('Issuable suggestions suggestion component', () => {
closedAt: '',
});
- const tooltip = vm.find(GlTooltip);
-
- expect(tooltip.find('.d-block').text()).toContain('Opened');
- expect(tooltip.text()).toContain('3 days ago');
+ expect(findTooltip().text()).toContain('Opened');
+ expect(findTooltip().text()).toContain('3 days ago');
});
});
@@ -70,18 +71,15 @@ describe('Issuable suggestions suggestion component', () => {
state: 'closed',
});
- const icon = vm.find(GlIcon);
-
- expect(icon.props('name')).toBe('issue-close');
+ expect(findIcon().props('name')).toBe('issue-close');
+ expect(findIcon().attributes('class')).toMatch('gl-text-blue-500');
});
it('renders closed timeago', () => {
createComponent();
- const tooltip = vm.find(GlTooltip);
-
- expect(tooltip.find('.d-block').text()).toContain('Opened');
- expect(tooltip.text()).toContain('1 day ago');
+ expect(findTooltip().text()).toContain('Opened');
+ expect(findTooltip().text()).toContain('1 day ago');
});
});
@@ -89,18 +87,14 @@ describe('Issuable suggestions suggestion component', () => {
it('renders author info', () => {
createComponent();
- const link = vm.findAll(GlLink).at(1);
-
- expect(link.text()).toContain('Author Name');
- expect(link.text()).toContain('@author.username');
+ expect(findAuthorLink().text()).toContain('Author Name');
+ expect(findAuthorLink().text()).toContain('@author.username');
});
it('renders author image', () => {
createComponent();
- const image = vm.find(UserAvatarImage);
-
- expect(image.props('imgSrc')).toBe(`${TEST_HOST}/avatar`);
+ expect(findUserAvatar().props('imgSrc')).toBe(`${TEST_HOST}/avatar`);
});
});
@@ -108,7 +102,7 @@ describe('Issuable suggestions suggestion component', () => {
it('renders upvotes count', () => {
createComponent();
- const count = vm.findAll('.suggestion-counts span').at(0);
+ const count = wrapper.findAll('.suggestion-counts span').at(0);
expect(count.text()).toContain('1');
expect(count.find(GlIcon).props('name')).toBe('thumb-up');
@@ -117,7 +111,7 @@ describe('Issuable suggestions suggestion component', () => {
it('renders notes count', () => {
createComponent();
- const count = vm.findAll('.suggestion-counts span').at(1);
+ const count = wrapper.findAll('.suggestion-counts span').at(1);
expect(count.text()).toContain('2');
expect(count.find(GlIcon).props('name')).toBe('comment');
@@ -130,10 +124,9 @@ describe('Issuable suggestions suggestion component', () => {
confidential: true,
});
- const icon = vm.find(GlIcon);
-
- expect(icon.props('name')).toBe('eye-slash');
- expect(icon.attributes('title')).toBe('Confidential');
+ expect(findIcon().props('name')).toBe('eye-slash');
+ expect(findIcon().attributes('class')).toMatch('gl-text-orange-500');
+ expect(findIcon().attributes('title')).toBe('Confidential');
});
});
});
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 8503d9f0fc2..580e5edd652 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -1,13 +1,8 @@
import { GlBadge, GlPagination, GlTable } from '@gitlab/ui';
-import {
- getByText as getByTextHelper,
- getByTestId as getByTestIdHelper,
- within,
-} from '@testing-library/dom';
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
+import { createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import CreatedAt from '~/members/components/table/created_at.vue';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
@@ -15,7 +10,15 @@ 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, TAB_QUERY_PARAM_VALUES } from '~/members/constants';
+import {
+ MEMBER_TYPES,
+ MEMBER_STATE_CREATED,
+ MEMBER_STATE_AWAITING,
+ MEMBER_STATE_ACTIVE,
+ USER_STATE_BLOCKED_PENDING_APPROVAL,
+ BADGE_LABELS_PENDING_OWNER_APPROVAL,
+ TAB_QUERY_PARAM_VALUES,
+} from '~/members/constants';
import * as initUserPopovers from '~/user_popovers';
import {
member as memberMock,
@@ -52,7 +55,7 @@ describe('MembersTable', () => {
};
const createComponent = (state, provide = {}) => {
- wrapper = mount(MembersTable, {
+ wrapper = mountExtended(MembersTable, {
localVue,
propsData: {
tabQueryParamValue: TAB_QUERY_PARAM_VALUES.invite,
@@ -79,17 +82,11 @@ describe('MembersTable', () => {
const url = 'https://localhost/foo-bar/-/project_members?tab=invited';
- const getByText = (text, options) =>
- createWrapper(getByTextHelper(wrapper.element, text, options));
-
- const getByTestId = (id, options) =>
- createWrapper(getByTestIdHelper(wrapper.element, id, options));
-
const findTable = () => wrapper.find(GlTable);
const findTableCellByMemberId = (tableCellLabel, memberId) =>
- getByTestId(`members-table-row-${memberId}`).find(
- `[data-label="${tableCellLabel}"][role="cell"]`,
- );
+ wrapper
+ .findByTestId(`members-table-row-${memberId}`)
+ .find(`[data-label="${tableCellLabel}"][role="cell"]`);
const findPagination = () => extendedWrapper(wrapper.find(GlPagination));
@@ -101,7 +98,6 @@ describe('MembersTable', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('fields', () => {
@@ -125,7 +121,7 @@ describe('MembersTable', () => {
tableFields: [field],
});
- expect(getByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true);
+ expect(wrapper.findByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true);
if (expectedComponent) {
expect(
@@ -134,11 +130,50 @@ describe('MembersTable', () => {
}
});
+ describe('Invited column', () => {
+ describe.each`
+ state | userState | expectedBadgeLabel
+ ${MEMBER_STATE_CREATED} | ${null} | ${''}
+ ${MEMBER_STATE_CREATED} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
+ ${MEMBER_STATE_AWAITING} | ${''} | ${''}
+ ${MEMBER_STATE_AWAITING} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
+ ${MEMBER_STATE_AWAITING} | ${'something_else'} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
+ ${MEMBER_STATE_ACTIVE} | ${null} | ${''}
+ ${MEMBER_STATE_ACTIVE} | ${'something_else'} | ${''}
+ `('Invited Badge', ({ state, userState, expectedBadgeLabel }) => {
+ it(`${
+ expectedBadgeLabel ? 'shows' : 'hides'
+ } invited badge if user status: '${userState}' and member state: '${state}'`, () => {
+ createComponent({
+ members: [
+ {
+ ...invite,
+ state,
+ invite: {
+ ...invite.invite,
+ userState,
+ },
+ },
+ ],
+ tableFields: ['invited'],
+ });
+
+ const invitedTab = wrapper.findByTestId('invited-badge');
+
+ if (expectedBadgeLabel) {
+ expect(invitedTab.text()).toBe(expectedBadgeLabel);
+ } else {
+ expect(invitedTab.exists()).toBe(false);
+ }
+ });
+ });
+ });
+
describe('"Actions" field', () => {
it('renders "Actions" field for screen readers', () => {
createComponent({ members: [memberCanUpdate], tableFields: ['actions'] });
- const actionField = getByTestId('col-actions');
+ const actionField = wrapper.findByTestId('col-actions');
expect(actionField.exists()).toBe(true);
expect(actionField.classes('gl-sr-only')).toBe(true);
@@ -151,7 +186,7 @@ describe('MembersTable', () => {
it('does not render the "Actions" field', () => {
createComponent({ tableFields: ['actions'] }, { currentUserId: null });
- expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null);
+ expect(wrapper.findByTestId('col-actions').exists()).toBe(false);
});
});
@@ -174,7 +209,7 @@ describe('MembersTable', () => {
it('renders the "Actions" field', () => {
createComponent({ members, tableFields: ['actions'] });
- expect(getByTestId('col-actions').exists()).toBe(true);
+ expect(wrapper.findByTestId('col-actions').exists()).toBe(true);
expect(findTableCellByMemberId('Actions', members[0].id).classes()).toStrictEqual([
'col-actions',
@@ -196,7 +231,7 @@ describe('MembersTable', () => {
it('does not render the "Actions" field', () => {
createComponent({ members, tableFields: ['actions'] });
- expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null);
+ expect(wrapper.findByTestId('col-actions').exists()).toBe(false);
});
});
});
@@ -206,7 +241,7 @@ describe('MembersTable', () => {
it('displays a "No members found" message', () => {
createComponent();
- expect(getByText('No members found').exists()).toBe(true);
+ expect(wrapper.findByText('No members found').exists()).toBe(true);
});
});
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 3afbe57a1aa..f42ee295511 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -1,4 +1,4 @@
-import { MEMBER_TYPES } from '~/members/constants';
+import { MEMBER_TYPES, MEMBER_STATE_CREATED } from '~/members/constants';
export const member = {
requestedAt: null,
@@ -14,6 +14,7 @@ export const member = {
webUrl: 'https://gitlab.com/groups/foo-bar',
},
type: 'GroupMember',
+ state: MEMBER_STATE_CREATED,
user: {
id: 123,
name: 'Administrator',
@@ -70,6 +71,7 @@ export const modalData = {
const { user, ...memberNoUser } = member;
export const invite = {
...memberNoUser,
+ state: MEMBER_STATE_CREATED,
invite: {
email: 'jewel@hudsonwalter.biz',
avatarUrl: 'https://www.gravatar.com/avatar/cbab7510da7eec2f60f638261b05436d?s=80&d=identicon',
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
index 279900edff2..f759fe7a81c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
@@ -9,9 +9,8 @@ import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/c
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
-const nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() };
-
describe('Nuget Metadata', () => {
+ let nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() };
let wrapper;
const mountComponent = () => {
@@ -52,4 +51,30 @@ describe('Nuget Metadata', () => {
expect(element.props('icon')).toBe(icon);
expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]);
});
+
+ describe('without source', () => {
+ beforeAll(() => {
+ nugetPackage = {
+ packageType: PACKAGE_TYPE_NUGET,
+ metadata: { iconUrl: 'iconUrl', licenseUrl: 'licenseUrl' },
+ };
+ });
+
+ it('does not show additional metadata', () => {
+ expect(findNugetSource().exists()).toBe(false);
+ });
+ });
+
+ describe('without license', () => {
+ beforeAll(() => {
+ nugetPackage = {
+ packageType: PACKAGE_TYPE_NUGET,
+ metadata: { iconUrl: 'iconUrl', projectUrl: 'projectUrl' },
+ };
+ });
+
+ it('does not show additional metadata', () => {
+ expect(findNugetLicense().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
index 753682d438b..44656b2b67d 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
@@ -5,22 +5,18 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineStatus, { i18n } from '~/pipeline_editor/components/header/pipeline_status.vue';
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
-const mockProvide = {
- projectFullPath: mockProjectFullPath,
-};
-
describe('Pipeline Status', () => {
let wrapper;
let mockApollo;
let mockPipelineQuery;
- const createComponentWithApollo = () => {
+ const createComponentWithApollo = (glFeatures = {}) => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers);
@@ -30,19 +26,23 @@ describe('Pipeline Status', () => {
propsData: {
commitSha: mockCommitSha,
},
- provide: mockProvide,
+ provide: {
+ glFeatures,
+ projectFullPath: mockProjectFullPath,
+ },
stubs: { GlLink, GlSprintf },
});
};
const findIcon = () => wrapper.findComponent(GlIcon);
- const findCiIcon = () => wrapper.findComponent(CiIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findPipelineEditorMiniGraph = () => wrapper.findComponent(PipelineEditorMiniGraph);
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]');
const findPipelineLoadingMsg = () => wrapper.find('[data-testid="pipeline-loading-msg"]');
const findPipelineViewBtn = () => wrapper.find('[data-testid="pipeline-view-btn"]');
+ const findStatusIcon = () => wrapper.find('[data-testid="pipeline-status-icon"]');
beforeEach(() => {
mockPipelineQuery = jest.fn();
@@ -50,9 +50,7 @@ describe('Pipeline Status', () => {
afterEach(() => {
mockPipelineQuery.mockReset();
-
wrapper.destroy();
- wrapper = null;
});
describe('loading icon', () => {
@@ -73,13 +71,13 @@ describe('Pipeline Status', () => {
describe('when querying data', () => {
describe('when data is set', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockPipelineQuery.mockResolvedValue({
- data: { project: mockProjectPipeline },
+ data: { project: mockProjectPipeline() },
});
createComponentWithApollo();
- await waitForPromises();
+ waitForPromises();
});
it('query is called with correct variables', async () => {
@@ -91,20 +89,24 @@ describe('Pipeline Status', () => {
});
it('does not render error', () => {
- expect(findIcon().exists()).toBe(false);
+ expect(findPipelineErrorMsg().exists()).toBe(false);
});
it('renders pipeline data', () => {
const {
id,
detailedStatus: { detailsPath },
- } = mockProjectPipeline.pipeline;
+ } = mockProjectPipeline().pipeline;
- expect(findCiIcon().exists()).toBe(true);
+ expect(findStatusIcon().exists()).toBe(true);
expect(findPipelineId().text()).toBe(`#${id.match(/\d+/g)[0]}`);
expect(findPipelineCommit().text()).toBe(mockCommitSha);
expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath);
});
+
+ it('does not render the pipeline mini graph', () => {
+ expect(findPipelineEditorMiniGraph().exists()).toBe(false);
+ });
});
describe('when data cannot be fetched', () => {
@@ -121,11 +123,26 @@ describe('Pipeline Status', () => {
});
it('does not render pipeline data', () => {
- expect(findCiIcon().exists()).toBe(false);
+ expect(findStatusIcon().exists()).toBe(false);
expect(findPipelineId().exists()).toBe(false);
expect(findPipelineCommit().exists()).toBe(false);
expect(findPipelineViewBtn().exists()).toBe(false);
});
});
});
+
+ describe('when feature flag for pipeline mini graph is enabled', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue({
+ data: { project: mockProjectPipeline() },
+ });
+
+ createComponentWithApollo({ pipelineEditorMiniGraph: true });
+ waitForPromises();
+ });
+
+ it('renders the pipeline mini graph', () => {
+ expect(findPipelineEditorMiniGraph().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
new file mode 100644
index 00000000000..3d7c3c839da
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
@@ -0,0 +1,42 @@
+import { shallowMount } from '@vue/test-utils';
+import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
+import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
+import { mockProjectPipeline } from '../../mock_data';
+
+describe('Pipeline Status', () => {
+ let wrapper;
+
+ const createComponent = ({ hasStages = true } = {}) => {
+ wrapper = shallowMount(PipelineEditorMiniGraph, {
+ propsData: {
+ pipeline: mockProjectPipeline({ hasStages }).pipeline,
+ },
+ });
+ };
+
+ const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when there are stages', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders pipeline mini graph', () => {
+ expect(findPipelineMiniGraph().exists()).toBe(true);
+ });
+ });
+
+ describe('when there are no stages', () => {
+ beforeEach(() => {
+ createComponent({ hasStages: false });
+ });
+
+ it('does not render pipeline mini graph', () => {
+ expect(findPipelineMiniGraph().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index f2104f25324..0b0ff14486e 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -247,20 +247,47 @@ export const mockEmptySearchBranches = {
export const mockBranchPaginationLimit = 10;
export const mockTotalBranches = 20; // must be greater than mockBranchPaginationLimit to test pagination
-export const mockProjectPipeline = {
- pipeline: {
- commitPath: '/-/commit/aabbccdd',
- id: 'gid://gitlab/Ci::Pipeline/118',
- iid: '28',
- shortSha: mockCommitSha,
- status: 'SUCCESS',
- detailedStatus: {
- detailsPath: '/root/sample-ci-project/-/pipelines/118"',
- group: 'success',
- icon: 'status_success',
- text: 'passed',
+export const mockProjectPipeline = ({ hasStages = true } = {}) => {
+ const stages = hasStages
+ ? {
+ edges: [
+ {
+ node: {
+ id: 'gid://gitlab/Ci::Stage/605',
+ name: 'prepare',
+ status: 'success',
+ detailedStatus: {
+ detailsPath: '/root/sample-ci-project/-/pipelines/268#prepare',
+ group: 'success',
+ hasDetails: true,
+ icon: 'status_success',
+ id: 'success-605-605',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ },
+ },
+ ],
+ }
+ : null;
+
+ return {
+ pipeline: {
+ commitPath: '/-/commit/aabbccdd',
+ id: 'gid://gitlab/Ci::Pipeline/118',
+ iid: '28',
+ shortSha: mockCommitSha,
+ status: 'SUCCESS',
+ detailedStatus: {
+ detailsPath: '/root/sample-ci-project/-/pipelines/118',
+ group: 'success',
+ icon: 'status_success',
+ text: 'passed',
+ },
+ stages,
},
- },
+ };
};
export const mockLintResponse = {
diff --git a/spec/frontend/search_settings/components/search_settings_spec.js b/spec/frontend/search_settings/components/search_settings_spec.js
index 173936e1ce3..6beaea8dba5 100644
--- a/spec/frontend/search_settings/components/search_settings_spec.js
+++ b/spec/frontend/search_settings/components/search_settings_spec.js
@@ -11,6 +11,7 @@ describe('search_settings/components/search_settings.vue', () => {
const GENERAL_SETTINGS_ID = 'js-general-settings';
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
const EXTRA_SETTINGS_ID = 'js-extra-settings';
+ const TEXT_CONTAIN_SEARCH_TERM = `This text contain ${SEARCH_TERM} and <script>alert("111")</script> others.`;
let wrapper;
@@ -33,6 +34,21 @@ describe('search_settings/components/search_settings.vue', () => {
const visibleSectionsCount = () =>
document.querySelectorAll(`${SECTION_SELECTOR}:not(.${HIDE_CLASS})`).length;
const highlightedElementsCount = () => document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).length;
+
+ const highlightedTextNodes = () => {
+ const highlightedList = Array.from(document.querySelectorAll(`.${HIGHLIGHT_CLASS}`));
+ return highlightedList.every((element) => {
+ return element.textContent.toLowerCase() === SEARCH_TERM.toLowerCase();
+ });
+ };
+
+ const matchParentElement = () => {
+ const highlightedList = Array.from(document.querySelectorAll(`.${HIGHLIGHT_CLASS}`));
+ return highlightedList.map((element) => {
+ return element.parentNode;
+ });
+ };
+
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const search = (term) => {
findSearchBox().vm.$emit('input', term);
@@ -52,6 +68,7 @@ describe('search_settings/components/search_settings.vue', () => {
</section>
<section id="${EXTRA_SETTINGS_ID}" class="settings">
<span>${SEARCH_TERM}</span>
+ <span>${TEXT_CONTAIN_SEARCH_TERM}</span>
</section>
</div>
</div>
@@ -82,7 +99,23 @@ describe('search_settings/components/search_settings.vue', () => {
it('highlight elements that match the search term', () => {
search(SEARCH_TERM);
- expect(highlightedElementsCount()).toBe(1);
+ expect(highlightedElementsCount()).toBe(2);
+ });
+
+ it('highlight only search term and not the whole line', () => {
+ search(SEARCH_TERM);
+
+ expect(highlightedTextNodes()).toBe(true);
+ });
+
+ it('prevents search xss', () => {
+ search(SEARCH_TERM);
+
+ const parentNodeList = matchParentElement();
+ parentNodeList.forEach((element) => {
+ const scriptElement = element.getElementsByTagName('script');
+ expect(scriptElement.length).toBe(0);
+ });
});
describe('default', () => {
diff --git a/spec/frontend/sidebar/sidebar_labels_spec.js b/spec/frontend/sidebar/sidebar_labels_spec.js
index 1141db5b812..8437ee1b723 100644
--- a/spec/frontend/sidebar/sidebar_labels_spec.js
+++ b/spec/frontend/sidebar/sidebar_labels_spec.js
@@ -27,6 +27,7 @@ describe('sidebar labels', () => {
labelsManagePath: '/gitlab-org/gitlab-test/-/labels',
projectIssuesPath: '/gitlab-org/gitlab-test/-/issues',
projectPath: 'gitlab-org/gitlab-test',
+ fullPath: 'gitlab-org/gitlab-test',
};
const $apollo = {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index 2e43ebe3f80..8931584e12c 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -67,6 +67,7 @@ describe('DropdownContentsCreateView', () => {
apolloProvider: mockApollo,
propsData: {
issuableType,
+ fullPath: '',
},
});
};
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index bd027d7aaf3..fac3331a2b8 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -50,8 +50,6 @@ describe('DropdownContentsLabelsView', () => {
localVue,
apolloProvider: mockApollo,
provide: {
- fullPath: 'test',
- iid: 1,
variant: DropdownVariant.Sidebar,
...injected,
},
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
index 43103c7bc99..36704ac5ef3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
@@ -38,6 +38,7 @@ describe('DropdownContent', () => {
dropdownButtonText: 'Labels',
variant: 'sidebar',
issuableType: 'issue',
+ fullPath: 'test',
...props,
},
data() {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index 72cb7b369ba..b5441d711a5 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -46,8 +46,6 @@ describe('LabelsSelectRoot', () => {
SidebarEditableItem,
},
provide: {
- iid: '1',
- fullPath: 'test',
canUpdate: true,
allowLabelEdit: true,
allowLabelCreate: true,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index 4ff33f578e1..23a457848d9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -34,6 +34,8 @@ export const mockLabels = [
];
export const mockConfig = {
+ iid: '1',
+ fullPath: 'test',
allowMultiselect: true,
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
diff --git a/spec/graphql/types/packages/nuget/metadatum_type_spec.rb b/spec/graphql/types/packages/nuget/metadatum_type_spec.rb
index e5baa7522e4..94a1dbaee43 100644
--- a/spec/graphql/types/packages/nuget/metadatum_type_spec.rb
+++ b/spec/graphql/types/packages/nuget/metadatum_type_spec.rb
@@ -10,4 +10,10 @@ RSpec.describe GitlabSchema.types['NugetMetadata'] do
expect(described_class).to include_graphql_fields(*expected_fields)
end
+
+ %w[projectUrl licenseUrl iconUrl].each do |optional_field|
+ it "#{optional_field} can be null" do
+ expect(described_class.fields[optional_field].type).to be_nullable
+ end
+ end
end
diff --git a/spec/lib/bulk_imports/clients/graphql_spec.rb b/spec/lib/bulk_imports/clients/graphql_spec.rb
index 2f212458c4a..a5b5e96e594 100644
--- a/spec/lib/bulk_imports/clients/graphql_spec.rb
+++ b/spec/lib/bulk_imports/clients/graphql_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe BulkImports::Clients::Graphql do
let(:version) { '13.0.0' }
it 'raises an error' do
- expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MINIMUM_GITLAB_MAJOR_VERSION}.")
+ expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MIN_MAJOR_VERSION}.")
end
end
end
diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb
index 023562626a1..623f9aa453a 100644
--- a/spec/lib/bulk_imports/clients/http_spec.rb
+++ b/spec/lib/bulk_imports/clients/http_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe BulkImports::Clients::HTTP do
let(:url) { 'http://gitlab.example' }
let(:token) { 'token' }
let(:resource) { 'resource' }
- let(:version) { "#{BulkImport::MINIMUM_GITLAB_MAJOR_VERSION}.0.0" }
+ let(:version) { "#{BulkImport::MIN_MAJOR_VERSION}.0.0" }
let(:response_double) { double(code: 200, success?: true, parsed_response: {}) }
let(:version_response) { double(code: 200, success?: true, parsed_response: { 'version' => version }) }
@@ -176,6 +176,28 @@ RSpec.describe BulkImports::Clients::HTTP do
end
end
+ describe '#instance_version' do
+ it 'returns version as an instance of Gitlab::VersionInfo' do
+ expect(subject.instance_version).to eq(Gitlab::VersionInfo.parse(version))
+ end
+ end
+
+ describe '#compatible_for_project_migration?' do
+ context 'when instance version is lower the the expected minimum' do
+ it 'returns false' do
+ expect(subject.compatible_for_project_migration?).to be false
+ end
+ end
+
+ context 'when instance version is at least the expected minimum' do
+ let(:version) { "14.4.4" }
+
+ it 'returns true' do
+ expect(subject.compatible_for_project_migration?).to be true
+ end
+ end
+ end
+
context 'when source instance is incompatible' do
let(:version) { '13.0.0' }
@@ -183,7 +205,7 @@ RSpec.describe BulkImports::Clients::HTTP do
expect { subject.get(resource) }
.to raise_error(
::BulkImports::Error,
- "Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MINIMUM_GITLAB_MAJOR_VERSION}."
+ "Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MIN_MAJOR_VERSION}."
)
end
end
diff --git a/spec/lib/bulk_imports/groups/stage_spec.rb b/spec/lib/bulk_imports/groups/stage_spec.rb
index 8609e57806d..696e68c2f65 100644
--- a/spec/lib/bulk_imports/groups/stage_spec.rb
+++ b/spec/lib/bulk_imports/groups/stage_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe BulkImports::Groups::Stage do
+ let(:bulk_import) { build(:bulk_import) }
+
let(:pipelines) do
[
[0, BulkImports::Groups::Pipelines::GroupPipeline],
@@ -16,23 +18,27 @@ RSpec.describe BulkImports::Groups::Stage do
]
end
+ it 'raises error when initialized without a BulkImport' do
+ expect { described_class.new({}) }.to raise_error(ArgumentError, 'Expected an argument of type ::BulkImport')
+ end
+
describe '.pipelines' do
it 'list all the pipelines with their stage number, ordered by stage' do
- expect(described_class.pipelines & pipelines).to eq(pipelines)
- expect(described_class.pipelines.last.last).to eq(BulkImports::Common::Pipelines::EntityFinisher)
+ expect(described_class.new(bulk_import).pipelines & pipelines).to eq(pipelines)
+ expect(described_class.new(bulk_import).pipelines.last.last).to eq(BulkImports::Common::Pipelines::EntityFinisher)
end
it 'includes project entities pipeline' do
stub_feature_flags(bulk_import_projects: true)
- expect(described_class.pipelines).to include([1, BulkImports::Groups::Pipelines::ProjectEntitiesPipeline])
+ expect(described_class.new(bulk_import).pipelines).to include([1, BulkImports::Groups::Pipelines::ProjectEntitiesPipeline])
end
context 'when bulk_import_projects feature flag is disabled' do
it 'does not include project entities pipeline' do
stub_feature_flags(bulk_import_projects: false)
- expect(described_class.pipelines.flatten).not_to include(BulkImports::Groups::Pipelines::ProjectEntitiesPipeline)
+ expect(described_class.new(bulk_import).pipelines.flatten).not_to include(BulkImports::Groups::Pipelines::ProjectEntitiesPipeline)
end
end
end
diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb
index 61f1be1a35d..57f63e8f9fb 100644
--- a/spec/lib/bulk_imports/projects/stage_spec.rb
+++ b/spec/lib/bulk_imports/projects/stage_spec.rb
@@ -13,9 +13,15 @@ RSpec.describe BulkImports::Projects::Stage do
]
end
- describe '.pipelines' do
+ subject do
+ bulk_import = build(:bulk_import)
+
+ described_class.new(bulk_import)
+ end
+
+ describe '#pipelines' do
it 'list all the pipelines with their stage number, ordered by stage' do
- expect(described_class.pipelines).to eq(pipelines)
+ expect(subject.pipelines).to eq(pipelines)
end
end
end
diff --git a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
index a46846e9820..e160e88487b 100644
--- a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
+++ b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
@@ -75,7 +75,68 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
checkbox_html
- expect(fake_template).to have_received(:label).with(:user, :view_diffs_file_by_file, { class: %w(custom-control-label label-foo-bar), object: user })
+ expect(fake_template).to have_received(:label).with(:user, :view_diffs_file_by_file, { class: %w(custom-control-label label-foo-bar), object: user, value: nil })
+ end
+ end
+ end
+
+ describe '#gitlab_ui_radio_component' do
+ let(:optional_args) { {} }
+
+ subject(:radio_html) { form_builder.gitlab_ui_radio_component(:access_level, :admin, "Access Level", **optional_args) }
+
+ context 'without optional arguments' do
+ it 'renders correct html' do
+ expected_html = <<~EOS
+ <div class="gl-form-radio custom-control custom-radio">
+ <input class="custom-control-input" type="radio" value="admin" name="user[access_level]" id="user_access_level_admin" />
+ <label class="custom-control-label" for="user_access_level_admin">
+ Access Level
+ </label>
+ </div>
+ EOS
+
+ expect(radio_html).to eq(html_strip_whitespace(expected_html))
+ end
+ end
+
+ context 'with optional arguments' do
+ let(:optional_args) do
+ {
+ help_text: 'Administrators have access to all groups, projects, and users and can manage all features in this installation',
+ radio_options: { class: 'radio-foo-bar' },
+ label_options: { class: 'label-foo-bar' }
+ }
+ end
+
+ it 'renders help text' do
+ expected_html = <<~EOS
+ <div class="gl-form-radio custom-control custom-radio">
+ <input class="custom-control-input radio-foo-bar" type="radio" value="admin" name="user[access_level]" id="user_access_level_admin" />
+ <label class="custom-control-label label-foo-bar" for="user_access_level_admin">
+ <span>Access Level</span>
+ <p class="help-text">Administrators have access to all groups, projects, and users and can manage all features in this installation</p>
+ </label>
+ </div>
+ EOS
+
+ expect(radio_html).to eq(html_strip_whitespace(expected_html))
+ end
+
+ it 'passes arguments to `radio_button` method' do
+ allow(fake_template).to receive(:radio_button).and_return('')
+
+ radio_html
+
+ expect(fake_template).to have_received(:radio_button).with(:user, :access_level, :admin, { class: %w(custom-control-input radio-foo-bar), object: user })
+ end
+
+ it 'passes arguments to `label` method' do
+ allow(fake_template).to receive(:label).and_return('')
+
+ radio_html
+
+ expect(fake_template).to have_received(:label).with(:user, :access_level, { class: %w(custom-control-label label-foo-bar), object: user, value: :admin })
end
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index daf7dac36a3..9514654204b 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -657,6 +657,7 @@ RSpec.describe Gitlab::Regex do
it { is_expected.to match('my_repo42') }
it { is_expected.to match('1.2.3') }
it { is_expected.to match('v1.2.3-beta-12') }
+ it { is_expected.to match('renovate_https-github.com-operator-framework-operator-lifecycle-manager.git-0.x') }
# Do not allow empty
it { is_expected.not_to match('') }
diff --git a/spec/models/bulk_import_spec.rb b/spec/models/bulk_import_spec.rb
index 4cfec6b20b7..ea002a7b174 100644
--- a/spec/models/bulk_import_spec.rb
+++ b/spec/models/bulk_import_spec.rb
@@ -21,4 +21,18 @@ RSpec.describe BulkImport, type: :model do
expect(described_class.all_human_statuses).to contain_exactly('created', 'started', 'finished', 'failed')
end
end
+
+ describe '.min_gl_version_for_project' do
+ it { expect(described_class.min_gl_version_for_project_migration).to be_a(Gitlab::VersionInfo) }
+ it { expect(described_class.min_gl_version_for_project_migration.to_s).to eq('14.4.0') }
+ end
+
+ describe '#source_version_info' do
+ it 'returns source_version as Gitlab::VersionInfo' do
+ bulk_import = build(:bulk_import, source_version: '9.13.2')
+
+ expect(bulk_import.source_version_info).to be_a(Gitlab::VersionInfo)
+ expect(bulk_import.source_version_info.to_s).to eq(bulk_import.source_version)
+ end
+ end
end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 94dc372f23a..278d7f4bc56 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -179,7 +179,7 @@ RSpec.describe BulkImports::Entity, type: :model do
entity = create(:bulk_import_entity, :group_entity)
entity.create_pipeline_trackers!
- expect(entity.trackers.count).to eq(BulkImports::Groups::Stage.pipelines.count)
+ expect(entity.trackers.count).to eq(BulkImports::Groups::Stage.new(entity.bulk_import).pipelines.count)
expect(entity.trackers.map(&:pipeline_name)).to include(BulkImports::Groups::Pipelines::GroupPipeline.to_s)
end
end
@@ -189,7 +189,7 @@ RSpec.describe BulkImports::Entity, type: :model do
entity = create(:bulk_import_entity, :project_entity)
entity.create_pipeline_trackers!
- expect(entity.trackers.count).to eq(BulkImports::Projects::Stage.pipelines.count)
+ expect(entity.trackers.count).to eq(BulkImports::Projects::Stage.new(entity.bulk_import).pipelines.count)
expect(entity.trackers.map(&:pipeline_name)).to include(BulkImports::Projects::Pipelines::ProjectPipeline.to_s)
end
end
diff --git a/spec/models/bulk_imports/tracker_spec.rb b/spec/models/bulk_imports/tracker_spec.rb
index 7f0a7d4f1ae..a72b628e329 100644
--- a/spec/models/bulk_imports/tracker_spec.rb
+++ b/spec/models/bulk_imports/tracker_spec.rb
@@ -66,7 +66,8 @@ RSpec.describe BulkImports::Tracker, type: :model do
describe '#pipeline_class' do
it 'returns the pipeline class' do
- pipeline_class = BulkImports::Groups::Stage.pipelines.first[1]
+ bulk_import = create(:bulk_import)
+ pipeline_class = BulkImports::Groups::Stage.new(bulk_import).pipelines.first[1]
tracker = create(:bulk_import_tracker, pipeline_name: pipeline_class)
expect(tracker.pipeline_class).to eq(pipeline_class)
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 0b546ce9b29..afe78adc547 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -169,6 +169,8 @@ RSpec.describe Member do
describe 'Scopes & finders' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:group) { create(:group) }
+ let_it_be(:blocked_pending_approval_user) { create(:user, :blocked_pending_approval ) }
+ let_it_be(:blocked_pending_approval_project_member) { create(:project_member, :invited, :developer, project: project, invite_email: blocked_pending_approval_user.email) }
before_all do
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@@ -538,6 +540,25 @@ RSpec.describe Member do
it { is_expected.to eq [example_member] }
end
end
+
+ describe '.with_invited_user_state' do
+ subject(:with_invited_user_state) { described_class.with_invited_user_state }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @maintainer }
+ it { is_expected.to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+
+ context 'with invited pending members' do
+ it 'includes invited user state' do
+ invited_pending_members = with_invited_user_state.select { |m| m.invited_user_state.present? }
+ expect(invited_pending_members.count).to eq 1
+ expect(invited_pending_members).to include blocked_pending_approval_project_member
+ end
+ end
+ end
end
describe 'Delegate methods' do
diff --git a/spec/models/packages/helm/file_metadatum_spec.rb b/spec/models/packages/helm/file_metadatum_spec.rb
index c7c17b157e4..995179b391d 100644
--- a/spec/models/packages/helm/file_metadatum_spec.rb
+++ b/spec/models/packages/helm/file_metadatum_spec.rb
@@ -31,8 +31,8 @@ RSpec.describe Packages::Helm::FileMetadatum, type: :model do
it 'validates #channel', :aggregate_failures do
is_expected.to validate_presence_of(:channel)
- is_expected.to allow_value('a' * 63).for(:channel)
- is_expected.not_to allow_value('a' * 64).for(:channel)
+ is_expected.to allow_value('a' * 255).for(:channel)
+ is_expected.not_to allow_value('a' * 256).for(:channel)
is_expected.to allow_value('release').for(:channel)
is_expected.to allow_value('my-repo').for(:channel)
diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb
index 1a28687c830..1602819a02e 100644
--- a/spec/requests/api/bulk_imports_spec.rb
+++ b/spec/requests/api/bulk_imports_spec.rb
@@ -21,6 +21,15 @@ RSpec.describe API::BulkImports do
end
describe 'POST /bulk_imports' do
+ before do
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
+ allow(instance)
+ .to receive(:instance_version)
+ .and_return(
+ Gitlab::VersionInfo.new(::BulkImport::MIN_MAJOR_VERSION, ::BulkImport::MIN_MINOR_VERSION_FOR_PROJECT))
+ end
+ end
+
it 'starts a new migration' do
post api('/bulk_imports', user), params: {
configuration: {
diff --git a/spec/serializers/member_entity_spec.rb b/spec/serializers/member_entity_spec.rb
index dc7aa4611f2..370fa14b1e8 100644
--- a/spec/serializers/member_entity_spec.rb
+++ b/spec/serializers/member_entity_spec.rb
@@ -39,6 +39,10 @@ RSpec.describe MemberEntity do
expect(entity_hash[:invite][:can_resend]).to be(true)
end
+
+ it 'exposes `invite.user_state` as empty string' do
+ expect(entity_hash[:invite][:user_state]).to eq('')
+ end
end
shared_examples 'is_direct_member' do
@@ -59,6 +63,12 @@ RSpec.describe MemberEntity do
end
end
+ shared_examples 'user state is blocked_pending_approval' do
+ it 'displays proper user state' do
+ expect(entity_hash[:invite][:user_state]).to eq('blocked_pending_approval')
+ end
+ end
+
context 'group member' do
let(:group) { create(:group) }
let(:source) { group }
@@ -79,6 +89,14 @@ RSpec.describe MemberEntity do
it_behaves_like 'is_direct_member'
end
+
+ context 'new member user state is blocked_pending_approval' do
+ let(:user) { create(:user, :blocked_pending_approval) }
+ let(:group_member) { create(:group_member, :invited, group: group, invite_email: user.email) }
+ let(:member) { GroupMemberPresenter.new(GroupMember.with_invited_user_state.find(group_member.id), current_user: current_user) }
+
+ it_behaves_like 'user state is blocked_pending_approval'
+ end
end
context 'project member' do
@@ -102,5 +120,13 @@ RSpec.describe MemberEntity do
it_behaves_like 'is_direct_member'
end
+
+ context 'new members user state is blocked_pending_approval' do
+ let(:user) { create(:user, :blocked_pending_approval) }
+ let(:project_member) { create(:project_member, :invited, project: project, invite_email: user.email) }
+ let(:member) { ProjectMemberPresenter.new(ProjectMember.with_invited_user_state.find(project_member.id), current_user: current_user) }
+
+ it_behaves_like 'user state is blocked_pending_approval'
+ end
end
end
diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb
index 2f07387451a..67ec6fee1ae 100644
--- a/spec/services/bulk_imports/create_service_spec.rb
+++ b/spec/services/bulk_imports/create_service_spec.rb
@@ -31,8 +31,25 @@ RSpec.describe BulkImports::CreateService do
subject { described_class.new(user, params, credentials) }
describe '#execute' do
+ let_it_be(:source_version) do
+ Gitlab::VersionInfo.new(::BulkImport::MIN_MAJOR_VERSION,
+ ::BulkImport::MIN_MINOR_VERSION_FOR_PROJECT)
+ end
+
+ before do
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
+ allow(instance).to receive(:instance_version).and_return(source_version)
+ end
+ end
+
it 'creates bulk import' do
expect { subject.execute }.to change { BulkImport.count }.by(1)
+
+ last_bulk_import = BulkImport.last
+
+ expect(last_bulk_import.user).to eq(user)
+ expect(last_bulk_import.source_version).to eq(source_version.to_s)
+ expect(last_bulk_import.user).to eq(user)
end
it 'creates bulk import entities' do
diff --git a/spec/services/bulk_imports/get_importable_data_service_spec.rb b/spec/services/bulk_imports/get_importable_data_service_spec.rb
new file mode 100644
index 00000000000..eccd3e5f49d
--- /dev/null
+++ b/spec/services/bulk_imports/get_importable_data_service_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::GetImportableDataService do
+ describe '#execute' do
+ include_context 'bulk imports requests context', 'https://gitlab.example.com'
+
+ let_it_be(:params) { { per_page: 20, page: 1 } }
+ let_it_be(:query_params) { { top_level_only: true, min_access_level: 50, search: '' } }
+ let_it_be(:credentials) { { url: 'https://gitlab.example.com', access_token: 'demo-pat' } }
+ let_it_be(:expected_version_validation) do
+ {
+ features: {
+ project_migration: {
+ available: true,
+ min_version: BulkImport.min_gl_version_for_project_migration.to_s
+ },
+ 'source_instance_version': BulkImport.min_gl_version_for_project_migration.to_s
+ }
+ }
+ end
+
+ let_it_be(:expected_parsed_response) do
+ [
+ {
+ 'id' => 2595438,
+ 'web_url' => 'https://gitlab.com/groups/auto-breakfast',
+ 'name' => 'Stub',
+ 'path' => 'stub-group',
+ 'full_name' => 'Stub',
+ 'full_path' => 'stub-group'
+ }
+ ]
+ end
+
+ subject do
+ described_class.new(params, query_params, credentials).execute
+ end
+
+ it 'returns version_validation and a response' do
+ expect(subject[:version_validation]).to eq(expected_version_validation)
+ expect(subject[:response].parsed_response).to eq(expected_parsed_response)
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb b/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb
new file mode 100644
index 00000000000..62d708420c3
--- /dev/null
+++ b/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'bulk imports requests context' do |url|
+ let(:page_response_headers) do
+ {
+ 'Content-Type' => 'application/json',
+ 'X-Next-Page' => 2,
+ 'X-Page' => 1,
+ 'X-Per-Page' => 20,
+ 'X-Total' => 42,
+ 'X-Total-Pages' => 2
+ }
+ end
+
+ let(:request_headers) { { 'Authorization' => 'Bearer demo-pat', 'Content-Type' => 'application/json' } }
+
+ before do
+ stub_request(:get, "#{url}/api/v4/version")
+ .with(headers: request_headers)
+ .to_return(
+ status: 200,
+ body: { version: ::BulkImport.min_gl_version_for_project_migration.to_s }.to_json,
+ headers: { 'Content-Type' => 'application/json' })
+
+ stub_request(:get, "https://gitlab.example.com/api/v4/groups?min_access_level=50&page=1&per_page=20&search=test&top_level_only=true")
+ .with(headers: request_headers)
+ .to_return(status: 200,
+ body: [{
+ id: 2595440,
+ web_url: 'https://gitlab.com/groups/test',
+ name: 'Test',
+ path: 'stub-test-group',
+ full_name: 'Test',
+ full_path: 'stub-test-group'
+ }].to_json,
+ headers: page_response_headers
+ )
+
+ stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=50&search=" % { url: url })
+ .to_return(
+ body: [{
+ id: 2595438,
+ web_url: 'https://gitlab.com/groups/auto-breakfast',
+ name: 'Stub',
+ path: 'stub-group',
+ full_name: 'Stub',
+ full_path: 'stub-group'
+ }].to_json,
+ headers: page_response_headers
+ )
+ end
+end
diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb
index 4072089e3a9..12e29573156 100644
--- a/spec/workers/bulk_import_worker_spec.rb
+++ b/spec/workers/bulk_import_worker_spec.rb
@@ -84,7 +84,7 @@ RSpec.describe BulkImportWorker do
expect { subject.perform(bulk_import.id) }
.to change(BulkImports::Tracker, :count)
- .by(BulkImports::Groups::Stage.pipelines.size * 2)
+ .by(BulkImports::Groups::Stage.new(bulk_import).pipelines.size * 2)
expect(entity_1.trackers).not_to be_empty
expect(entity_2.trackers).not_to be_empty
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index fc70a2582e4..c902d1f2034 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -22,9 +22,10 @@ RSpec.describe BulkImports::PipelineWorker do
before do
stub_const('FakePipeline', pipeline_class)
- allow(BulkImports::Groups::Stage)
- .to receive(:pipelines)
- .and_return([[0, pipeline_class]])
+ allow_next_instance_of(BulkImports::Groups::Stage) do |instance|
+ allow(instance).to receive(:pipelines)
+ .and_return([[0, pipeline_class]])
+ end
end
shared_examples 'successfully runs the pipeline' do
@@ -206,9 +207,10 @@ RSpec.describe BulkImports::PipelineWorker do
before do
stub_const('NdjsonPipeline', ndjson_pipeline)
- allow(BulkImports::Groups::Stage)
- .to receive(:pipelines)
- .and_return([[0, ndjson_pipeline]])
+ allow_next_instance_of(BulkImports::Groups::Stage) do |instance|
+ allow(instance).to receive(:pipelines)
+ .and_return([[0, ndjson_pipeline]])
+ end
end
it 'runs the pipeline successfully' do
diff --git a/workhorse/internal/git/info-refs.go b/workhorse/internal/git/info-refs.go
index 0d8edbeff06..8390143b99b 100644
--- a/workhorse/internal/git/info-refs.go
+++ b/workhorse/internal/git/info-refs.go
@@ -8,8 +8,8 @@ import (
"net/http"
"github.com/golang/gddo/httputil"
-
- "gitlab.com/gitlab-org/labkit/log"
+ grpccodes "google.golang.org/grpc/codes"
+ grpcstatus "google.golang.org/grpc/status"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/gitaly"
@@ -26,6 +26,7 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response)
defer responseWriter.Log(r, 0)
rpc := getService(r)
+
if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") {
// The 'dumb' Git HTTP protocol is not supported
http.Error(responseWriter, "Not Found", 404)
@@ -41,19 +42,26 @@ func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response)
encoding := httputil.NegotiateContentEncoding(r, offers)
if err := handleGetInfoRefsWithGitaly(r.Context(), responseWriter, a, rpc, gitProtocol, encoding); err != nil {
- helper.Fail500(responseWriter, r, fmt.Errorf("handleGetInfoRefs: %v", err))
+ status := grpcstatus.Convert(err)
+ err = fmt.Errorf("handleGetInfoRefs: %v", err)
+
+ if status != nil && status.Code() == grpccodes.Unavailable {
+ helper.CaptureAndFail(responseWriter, r, err, "The git server, Gitaly, is not available at this time. Please contact your administrator.", http.StatusServiceUnavailable)
+ } else {
+ helper.Fail500(responseWriter, r, err)
+ }
}
}
func handleGetInfoRefsWithGitaly(ctx context.Context, responseWriter *HttpResponseWriter, a *api.Response, rpc, gitProtocol, encoding string) error {
ctx, smarthttp, err := gitaly.NewSmartHTTPClient(ctx, a.GitalyServer)
if err != nil {
- return fmt.Errorf("GetInfoRefsHandler: %v", err)
+ return err
}
infoRefsResponseReader, err := smarthttp.InfoRefsResponseReader(ctx, &a.Repository, rpc, gitConfigOptions(a), gitProtocol)
if err != nil {
- return fmt.Errorf("GetInfoRefsHandler: %v", err)
+ return err
}
var w io.Writer
@@ -69,7 +77,7 @@ func handleGetInfoRefsWithGitaly(ctx context.Context, responseWriter *HttpRespon
}
if _, err = io.Copy(w, infoRefsResponseReader); err != nil {
- log.WithError(err).Error("GetInfoRefsHandler: error copying gitaly response")
+ return err
}
return nil
diff --git a/workhorse/internal/git/info-refs_test.go b/workhorse/internal/git/info-refs_test.go
new file mode 100644
index 00000000000..4f23d1ac174
--- /dev/null
+++ b/workhorse/internal/git/info-refs_test.go
@@ -0,0 +1,42 @@
+package git
+
+import (
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ grpccodes "google.golang.org/grpc/codes"
+ grpcstatus "google.golang.org/grpc/status"
+
+ "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb"
+
+ "gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
+ "gitlab.com/gitlab-org/gitlab/workhorse/internal/gitaly"
+)
+
+type smartHTTPServiceServerWithInfoRefs struct {
+ gitalypb.UnimplementedSmartHTTPServiceServer
+ InfoRefsUploadPackFunc func(*gitalypb.InfoRefsRequest, gitalypb.SmartHTTPService_InfoRefsUploadPackServer) error
+}
+
+func (srv *smartHTTPServiceServerWithInfoRefs) InfoRefsUploadPack(r *gitalypb.InfoRefsRequest, s gitalypb.SmartHTTPService_InfoRefsUploadPackServer) error {
+ return srv.InfoRefsUploadPackFunc(r, s)
+}
+
+func TestGetInfoRefsHandler(t *testing.T) {
+ addr := startSmartHTTPServer(t, &smartHTTPServiceServerWithInfoRefs{
+ InfoRefsUploadPackFunc: func(r *gitalypb.InfoRefsRequest, s gitalypb.SmartHTTPService_InfoRefsUploadPackServer) error {
+ return grpcstatus.Error(grpccodes.Unavailable, "error")
+ },
+ })
+
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", "/?service=git-upload-pack", nil)
+ a := &api.Response{GitalyServer: gitaly.Server{Address: addr}}
+
+ handleGetInfoRefs(NewHttpResponseWriter(w), r, a)
+ require.Equal(t, 503, w.Code)
+
+ msg := "The git server, Gitaly, is not available at this time. Please contact your administrator.\n"
+ require.Equal(t, msg, w.Body.String())
+}
diff --git a/workhorse/internal/git/upload-pack_test.go b/workhorse/internal/git/upload-pack_test.go
index ebefe360122..211f68a2608 100644
--- a/workhorse/internal/git/upload-pack_test.go
+++ b/workhorse/internal/git/upload-pack_test.go
@@ -45,14 +45,13 @@ func TestUploadPackTimesOut(t *testing.T) {
uploadPackTimeout = time.Millisecond
defer func() { uploadPackTimeout = originalUploadPackTimeout }()
- addr, cleanUp := startSmartHTTPServer(t, &smartHTTPServiceServer{
+ addr := startSmartHTTPServer(t, &smartHTTPServiceServer{
PostUploadPackFunc: func(stream gitalypb.SmartHTTPService_PostUploadPackServer) error {
_, err := stream.Recv() // trigger a read on the client request body
require.NoError(t, err)
return nil
},
})
- defer cleanUp()
body := &fakeReader{n: 0, err: nil}
@@ -64,7 +63,9 @@ func TestUploadPackTimesOut(t *testing.T) {
require.EqualError(t, err, "smarthttp.UploadPack: busyReader: context deadline exceeded")
}
-func startSmartHTTPServer(t testing.TB, s gitalypb.SmartHTTPServiceServer) (string, func()) {
+func startSmartHTTPServer(t testing.TB, s gitalypb.SmartHTTPServiceServer) string {
+ t.Helper()
+
tmp, err := ioutil.TempDir("", "")
require.NoError(t, err)
@@ -78,8 +79,10 @@ func startSmartHTTPServer(t testing.TB, s gitalypb.SmartHTTPServiceServer) (stri
require.NoError(t, srv.Serve(ln))
}()
- return fmt.Sprintf("%s://%s", ln.Addr().Network(), ln.Addr().String()), func() {
+ t.Cleanup(func() {
srv.GracefulStop()
require.NoError(t, os.RemoveAll(tmp), "error removing temp dir %q", tmp)
- }
+ })
+
+ return fmt.Sprintf("%s://%s", ln.Addr().Network(), ln.Addr().String())
}