summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-14 18:12:38 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-14 18:12:38 +0000
commit283c7bb302510ed1fc55f0d333c484ce7fa781fd (patch)
treeadbb03e34cc91f339395f6417502c075ee84b8bb
parenta2b7b398c7855bccee5d2f0f9a021b2efea0838e (diff)
downloadgitlab-ce-283c7bb302510ed1fc55f0d333c484ce7fa781fd.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/rake/require.yml1
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue5
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue62
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue25
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue55
-rw-r--r--app/assets/javascripts/ref/components/ref_results_section.vue138
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue195
-rw-r--r--app/assets/javascripts/ref/constants.js4
-rw-r--r--app/assets/javascripts/ref/format_refs.js60
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue133
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue58
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue1
-rw-r--r--app/graphql/types/ci/runner_type.rb2
-rw-r--r--app/models/ci/runner.rb23
-rw-r--r--app/uploaders/object_storage/s3.rb14
-rw-r--r--app/views/projects/merge_requests/_page.html.haml3
-rw-r--r--config/feature_flags/development/revoke_ssh_signatures.yml2
-rw-r--r--doc/api/product_analytics.md28
-rw-r--r--doc/api/projects.md13
-rw-r--r--doc/architecture/blueprints/runner_tokens/index.md86
-rw-r--r--doc/ci/environments/index.md52
-rw-r--r--doc/development/testing_guide/end_to_end/style_guide.md2
-rw-r--r--doc/user/packages/container_registry/index.md6
-rw-r--r--lib/api/helpers.rb15
-rw-r--r--lib/gitlab/pages/cache_control.rb11
-rw-r--r--lib/tasks/gitlab/cleanup.rake3
-rw-r--r--locale/gitlab.pot17
-rw-r--r--qa/qa/page/project/settings/default_branch.rb11
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb85
-rw-r--r--spec/factories/ci/runners.rb4
-rw-r--r--spec/features/projects/branches/new_branch_ref_dropdown_spec.rb23
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb25
-rw-r--r--spec/features/projects/files/user_find_file_spec.rb6
-rw-r--r--spec/features/projects/graph_spec.rb10
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb5
-rw-r--r--spec/features/projects/settings/user_changes_default_branch_spec.rb8
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb13
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb27
-rw-r--r--spec/features/tags/developer_creates_tag_spec.rb6
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js42
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js90
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js60
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js47
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js209
-rw-r--r--spec/frontend/ref/format_refs_spec.js38
-rw-r--r--spec/frontend/ref/mock_data.js87
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap24
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js87
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/mock_data.js24
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js47
-rw-r--r--spec/lib/api/helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/pages/cache_control_spec.rb26
-rw-r--r--spec/models/ci/runner_spec.rb23
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb12
-rw-r--r--spec/requests/api/maven_packages_spec.rb2
-rw-r--r--spec/services/auto_merge_service_spec.rb2
-rw-r--r--spec/support/helpers/features/branches_helpers.rb9
-rw-r--r--spec/support/helpers/features/releases_helpers.rb7
-rw-r--r--spec/uploaders/object_storage/s3_spec.rb39
65 files changed, 1352 insertions, 777 deletions
diff --git a/.rubocop_todo/rake/require.yml b/.rubocop_todo/rake/require.yml
index 1f7a488ba35..d098c28bf80 100644
--- a/.rubocop_todo/rake/require.yml
+++ b/.rubocop_todo/rake/require.yml
@@ -3,7 +3,6 @@ Rake/Require:
Details: grace period
Exclude:
- 'lib/tasks/gitlab/assets.rake'
- - 'lib/tasks/gitlab/cleanup.rake'
- 'lib/tasks/gitlab/dependency_proxy/migrate.rake'
- 'lib/tasks/gitlab/docs/redirect.rake'
- 'lib/tasks/gitlab/graphql.rake'
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
index 1d6a06a08dc..16034cce381 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
@@ -130,9 +130,6 @@ export default {
displayMaskedError() {
return !this.canMask && this.variable.masked;
},
- isUsingRawRegexFlag() {
- return this.glFeatures.ciRemoveCharacterLimitationRawMaskedVar;
- },
isEditing() {
return this.mode === EDIT_VARIABLE_ACTION;
},
@@ -177,7 +174,7 @@ export default {
return true;
},
useRawMaskableRegexp() {
- return this.isRaw && this.isUsingRawRegexFlag;
+ return this.isRaw;
},
variableValidationFeedback() {
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index e36a7864878..525094271d9 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -77,7 +77,7 @@ export default {
<template>
<gl-intersection-observer
- class="gl-relative gl-top-2"
+ class="gl-relative gl-top-n5"
@appear="setStickyHeaderVisible(false)"
@disappear="setStickyHeaderVisible(true)"
>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
index 787f21d9419..d982df4f984 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
@@ -1,15 +1,30 @@
<script>
+import { n__ } from '~/locale';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
+import {
+ CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
+import Tracking from '~/tracking';
export default {
components: {
+ DeleteModal,
VersionRow,
PackagesListLoader,
RegistryList,
},
+ mixins: [Tracking.mixin()],
props: {
+ canDestroy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
versions: {
type: Array,
required: true,
@@ -25,11 +40,35 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ itemsToBeDeleted: [],
+ };
+ },
computed: {
+ listTitle() {
+ return n__('%d version', '%d versions', this.versions.length);
+ },
isListEmpty() {
return this.versions.length === 0;
},
},
+ methods: {
+ deleteItemsCanceled() {
+ this.track(CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION);
+ this.itemsToBeDeleted = [];
+ },
+ deleteItemsConfirmation() {
+ this.$emit('delete', this.itemsToBeDeleted);
+ this.track(DELETE_PACKAGE_VERSIONS_TRACKING_ACTION);
+ this.itemsToBeDeleted = [];
+ },
+ setItemsToBeDeleted(items) {
+ this.itemsToBeDeleted = items;
+ this.track(REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION);
+ this.$refs.deletePackagesModal.show();
+ },
+ },
};
</script>
<template>
@@ -40,17 +79,34 @@ export default {
<slot v-else-if="isListEmpty" name="empty-state"></slot>
<div v-else>
<registry-list
- :hidden-delete="true"
+ :hidden-delete="!canDestroy"
:is-loading="isLoading"
:items="versions"
:pagination="pageInfo"
+ :title="listTitle"
+ @delete="setItemsToBeDeleted"
@prev-page="$emit('prev-page')"
@next-page="$emit('next-page')"
>
- <template #default="{ item }">
- <version-row :package-entity="item" />
+ <template #default="{ first, item, isSelected, selectItem }">
+ <!-- `first` prop is used to decide whether to show the top border
+ for the first element. We want to show the top border only when
+ user has permission to bulk delete versions. -->
+ <version-row
+ :first="canDestroy && first"
+ :package-entity="item"
+ :selected="isSelected(item)"
+ @select="selectItem(item)"
+ />
</template>
</registry-list>
+
+ <delete-modal
+ ref="deletePackagesModal"
+ :items-to-be-deleted="itemsToBeDeleted"
+ @confirm="deleteItemsConfirmation"
+ @cancel="deleteItemsCanceled"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue
index d6732cad6c1..9f8f6328970 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue
@@ -1,5 +1,12 @@
<script>
-import { GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
+import {
+ GlFormCheckbox,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlTooltipDirective,
+ GlTruncate,
+} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
@@ -15,6 +22,7 @@ import {
export default {
name: 'PackageVersionRow',
components: {
+ GlFormCheckbox,
GlIcon,
GlLink,
GlSprintf,
@@ -32,6 +40,11 @@ export default {
type: Object,
required: true,
},
+ selected: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
containsWebPathLink() {
@@ -53,7 +66,15 @@ export default {
</script>
<template>
- <list-item>
+ <list-item :selected="selected" v-bind="$attrs">
+ <template #left-action>
+ <gl-form-checkbox
+ v-if="packageEntity.canDestroy"
+ class="gl-m-0"
+ :checked="selected"
+ @change="$emit('select')"
+ />
+ </template>
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<gl-link v-if="containsWebPathLink" class="gl-text-body gl-min-w-0" :href="packageLink">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index a8a96b89256..16f21bfe61d 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -100,7 +100,7 @@ export default {
</script>
<template>
- <list-item data-testid="package-row" v-bind="$attrs">
+ <list-item data-testid="package-row" :selected="selected" v-bind="$attrs">
<template #left-action>
<gl-form-checkbox
v-if="packageEntity.canDestroy"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index ddd2fda7733..d979ae5c08c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -115,6 +115,10 @@ export const DELETE_PACKAGES_TRACKING_ACTION = 'delete_packages';
export const REQUEST_DELETE_PACKAGES_TRACKING_ACTION = 'request_delete_packages';
export const CANCEL_DELETE_PACKAGES_TRACKING_ACTION = 'cancel_delete_packages';
+export const DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'delete_package_versions';
+export const REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'request_delete_package_versions';
+export const CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'cancel_delete_package_versions';
+
export const DELETE_PACKAGES_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting packages.',
);
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 31579cb4c72..109d535469b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -66,6 +66,7 @@ query getPackageDetails(
nodes {
id
name
+ canDestroy
createdAt
version
status
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index 8f0efd5e74f..4591c2eca87 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -95,6 +95,7 @@ export default {
deletePackageModalContent: DELETE_MODAL_CONTENT,
filesToDelete: [],
mutationLoading: false,
+ versionsMutationLoading: false,
packageEntity: {},
};
},
@@ -146,6 +147,9 @@ export default {
isLoading() {
return this.$apollo.queries.packageEntity.loading;
},
+ isVersionsLoading() {
+ return this.isLoading || this.versionsMutationLoading;
+ },
packageFilesLoading() {
return this.isLoading || this.mutationLoading;
},
@@ -157,9 +161,6 @@ export default {
category: packageTypeToTrackCategory(this.packageType),
};
},
- hasVersions() {
- return this.packageEntity.versions?.nodes?.length > 0;
- },
versionPageInfo() {
return this.packageEntity?.versions?.pageInfo ?? {};
},
@@ -181,6 +182,14 @@ export default {
PACKAGE_TYPE_PYPI,
].includes(this.packageType);
},
+ refetchQueriesData() {
+ return [
+ {
+ query: getPackageDetails,
+ variables: this.queryVariables,
+ },
+ ];
+ },
},
methods: {
formatSize(size) {
@@ -206,12 +215,7 @@ export default {
ids,
},
awaitRefetchQueries: true,
- refetchQueries: [
- {
- query: getPackageDetails,
- variables: this.queryVariables,
- },
- ],
+ refetchQueries: this.refetchQueriesData,
});
if (data?.destroyPackageFiles?.errors[0]) {
throw data.destroyPackageFiles.errors[0];
@@ -403,19 +407,30 @@ export default {
}}</gl-badge>
</template>
- <package-versions-list
- :is-loading="isLoading"
- :page-info="versionPageInfo"
- :versions="packageEntity.versions.nodes"
- @prev-page="fetchPreviousVersionsPage"
- @next-page="fetchNextVersionsPage"
+ <delete-packages
+ :refetch-queries="refetchQueriesData"
+ show-success-alert
+ @start="versionsMutationLoading = true"
+ @end="versionsMutationLoading = false"
>
- <template #empty-state>
- <p class="gl-mt-3" data-testid="no-versions-message">
- {{ s__('PackageRegistry|There are no other versions of this package.') }}
- </p>
+ <template #default="{ deletePackages }">
+ <package-versions-list
+ :can-destroy="packageEntity.canDestroy"
+ :is-loading="isVersionsLoading"
+ :page-info="versionPageInfo"
+ :versions="packageEntity.versions.nodes"
+ @delete="deletePackages"
+ @prev-page="fetchPreviousVersionsPage"
+ @next-page="fetchNextVersionsPage"
+ >
+ <template #empty-state>
+ <p class="gl-mt-3" data-testid="no-versions-message">
+ {{ s__('PackageRegistry|There are no other versions of this package.') }}
+ </p>
+ </template>
+ </package-versions-list>
</template>
- </package-versions-list>
+ </delete-packages>
</gl-tab>
</gl-tabs>
diff --git a/app/assets/javascripts/ref/components/ref_results_section.vue b/app/assets/javascripts/ref/components/ref_results_section.vue
deleted file mode 100644
index 52d1ed96b21..00000000000
--- a/app/assets/javascripts/ref/components/ref_results_section.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<script>
-import { GlDropdownSectionHeader, GlDropdownItem, GlBadge, GlIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-
-export default {
- name: 'RefResultsSection',
- components: {
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlBadge,
- GlIcon,
- },
- props: {
- showHeader: {
- type: Boolean,
- required: false,
- default: true,
- },
-
- sectionTitle: {
- type: String,
- required: true,
- },
-
- totalCount: {
- type: Number,
- required: true,
- },
-
- /**
- * An array of object that have the following properties:
- *
- * - name (String, required): The name of the ref that will be displayed
- * - value (String, optional): The value that will be selected when the ref
- * is selected. If not provided, `name` will be used as the value.
- * For example, commits use the short SHA for `name`
- * and long SHA for `value`.
- * - subtitle (String, optional): Text to render underneath the name.
- * For example, used to render the commit's title underneath its SHA.
- * - default (Boolean, optional): Whether or not to render a "default"
- * indicator next to the item. Used to indicate
- * the project's default branch.
- *
- */
- items: {
- type: Array,
- required: true,
- validator: (items) => Array.isArray(items) && items.every((item) => item.name),
- },
-
- /**
- * The currently selected ref.
- * Used to render a check mark by the selected item.
- * */
- selectedRef: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * An error object that indicates that an error
- * occurred while fetching items for this section
- */
- error: {
- type: Error,
- required: false,
- default: null,
- },
-
- /** The message to display if an error occurs */
- errorMessage: {
- type: String,
- required: false,
- default: '',
- },
- shouldShowCheck: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- computed: {
- totalCountText() {
- return this.totalCount > 999 ? s__('TotalRefCountIndicator|1000+') : `${this.totalCount}`;
- },
- },
- methods: {
- showCheck(item) {
- if (!this.shouldShowCheck) {
- return false;
- }
- return item.name === this.selectedRef || item.value === this.selectedRef;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <gl-dropdown-section-header v-if="showHeader">
- <div class="gl-display-flex align-items-center" data-testid="section-header">
- <span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span>
- <gl-badge variant="neutral">{{ totalCountText }}</gl-badge>
- </div>
- </gl-dropdown-section-header>
- <template v-if="error">
- <div class="gl-display-flex align-items-start text-danger gl-ml-4 gl-mr-4 gl-mb-3">
- <gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" />
- <span>{{ errorMessage }}</span>
- </div>
- </template>
- <template v-else>
- <gl-dropdown-item
- v-for="item in items"
- :key="item.name"
- @click="$emit('selected', item.value || item.name)"
- >
- <div class="gl-display-flex align-items-start">
- <gl-icon
- name="mobile-issue-close"
- class="gl-mr-2 gl-flex-shrink-0"
- :class="{ 'gl-visibility-hidden': !showCheck(item) }"
- />
-
- <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column">
- <span class="gl-font-monospace">{{ item.name }}</span>
- <span class="gl-text-gray-400">{{ item.subtitle }}</span>
- </div>
-
- <gl-badge v-if="item.default" size="sm" variant="info">{{
- s__('DefaultBranchLabel|default')
- }}</gl-badge>
- </div>
- </gl-dropdown-item>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index 2f5b5bcebd6..359909b8f3b 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -1,13 +1,8 @@
<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlSearchBoxByType,
- GlSprintf,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlBadge, GlIcon, GlCollapsibleListbox } from '@gitlab/ui';
import { debounce, isArray } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
+import { sprintf } from '~/locale';
import {
ALL_REF_TYPES,
SEARCH_DEBOUNCE_MS,
@@ -15,21 +10,16 @@ import {
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
- BRANCH_REF_TYPE,
- TAG_REF_TYPE,
} from '../constants';
import createStore from '../stores';
-import RefResultsSection from './ref_results_section.vue';
+import { formatListBoxItems, formatErrors } from '../format_refs';
export default {
name: 'RefSelector',
components: {
- GlDropdown,
- GlDropdownDivider,
- GlSearchBoxByType,
- GlSprintf,
- GlLoadingIcon,
- RefResultsSection,
+ GlBadge,
+ GlIcon,
+ GlCollapsibleListbox,
},
inheritAttrs: false,
props: {
@@ -87,7 +77,6 @@ export default {
required: false,
default: '',
},
-
toggleButtonClass: {
type: [String, Object, Array],
required: false,
@@ -112,29 +101,17 @@ export default {
...this.translations,
};
},
- showBranchesSection() {
- return (
- this.enabledRefTypes.includes(REF_TYPE_BRANCHES) &&
- Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error)
- );
- },
- showTagsSection() {
- return (
- this.enabledRefTypes.includes(REF_TYPE_TAGS) &&
- Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error)
- );
+ listBoxItems() {
+ return formatListBoxItems(this.branches, this.tags, this.commits);
},
- showCommitsSection() {
- return (
- this.enabledRefTypes.includes(REF_TYPE_COMMITS) &&
- Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error)
- );
+ branches() {
+ return this.enabledRefTypes.includes(REF_TYPE_BRANCHES) ? this.matches.branches.list : [];
},
- showNoResults() {
- return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
+ tags() {
+ return this.enabledRefTypes.includes(REF_TYPE_TAGS) ? this.matches.tags.list : [];
},
- showSectionHeaders() {
- return this.enabledRefTypes.length > 1;
+ commits() {
+ return this.enabledRefTypes.includes(REF_TYPE_COMMITS) ? this.matches.commits.list : [];
},
extendedToggleButtonClass() {
const classes = [
@@ -142,7 +119,6 @@ export default {
'gl-inset-border-1-red-500!': !this.state,
'gl-font-monospace': Boolean(this.selectedRef),
},
- 'gl-max-w-26',
];
if (Array.isArray(this.toggleButtonClass)) {
@@ -160,6 +136,9 @@ export default {
query: this.lastQuery,
};
},
+ errors() {
+ return formatErrors(this.matches.branches, this.matches.tags, this.matches.commits);
+ },
selectedRefForDisplay() {
if (this.useSymbolicRefNames && this.selectedRef) {
return this.selectedRef.replace(/^refs\/(tags|heads)\//, '');
@@ -170,11 +149,12 @@ export default {
buttonText() {
return this.selectedRefForDisplay || this.i18n.noRefSelected;
},
- isTagRefType() {
- return this.refType === TAG_REF_TYPE;
- },
- isBranchRefType() {
- return this.refType === BRANCH_REF_TYPE;
+ noResultsMessage() {
+ return this.lastQuery
+ ? sprintf(this.i18n.noResultsWithQuery, {
+ query: this.lastQuery,
+ })
+ : this.i18n.noResults;
},
},
watch: {
@@ -202,9 +182,7 @@ export default {
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue.
- this.debouncedSearch = debounce(function search() {
- this.search();
- }, SEARCH_DEBOUNCE_MS);
+ this.debouncedSearch = debounce(this.search, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId);
@@ -231,14 +209,8 @@ export default {
'setSelectedRef',
]),
...mapActions({ storeSearch: 'search' }),
- focusSearchBox() {
- this.$refs.searchBox.$el.querySelector('input').focus();
- },
- onSearchBoxEnter() {
- this.debouncedSearch.cancel();
- this.search();
- },
- onSearchBoxInput() {
+ onSearchBoxInput(searchQuery = '') {
+ this.query = searchQuery?.trim();
this.debouncedSearch();
},
selectRef(ref) {
@@ -248,104 +220,55 @@ export default {
search() {
this.storeSearch(this.query);
},
+ totalCountText(count) {
+ return count > 999 ? this.i18n.totalCountLabel : `${count}`;
+ },
},
};
</script>
<template>
<div>
- <gl-dropdown
+ <gl-collapsible-listbox
+ class="ref-selector gl-w-full"
+ block
+ searchable
+ :selected="selectedRef"
:header-text="i18n.dropdownHeader"
+ :items="listBoxItems"
+ :no-results-text="noResultsMessage"
+ :searching="isLoading"
+ :search-placeholder="i18n.searchPlaceholder"
:toggle-class="extendedToggleButtonClass"
- :text="buttonText"
- class="ref-selector"
+ :toggle-text="buttonText"
v-bind="$attrs"
v-on="$listeners"
- @shown="focusSearchBox"
+ @hidden="$emit('hide')"
+ @search="onSearchBoxInput"
+ @select="selectRef"
>
- <template #header>
- <gl-search-box-by-type
- ref="searchBox"
- v-model.trim="query"
- :placeholder="i18n.searchPlaceholder"
- autocomplete="off"
- data-qa-selector="ref_selector_searchbox"
- @input="onSearchBoxInput"
- @keydown.enter.prevent="onSearchBoxEnter"
- />
+ <template #group-label="{ group }">
+ {{ group.text }} <gl-badge size="sm">{{ totalCountText(group.options.length) }}</gl-badge>
</template>
-
- <gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" />
-
- <div
- v-else-if="showNoResults"
- class="gl-text-center gl-mx-3 gl-py-3"
- data-testid="no-results"
- >
- <gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery">
- <template #query>
- <b class="gl-word-break-all">{{ lastQuery }}</b>
- </template>
- </gl-sprintf>
-
- <span v-else>{{ i18n.noResults }}</span>
- </div>
-
- <template v-else>
- <template v-if="showBranchesSection">
- <ref-results-section
- :section-title="i18n.branches"
- :total-count="matches.branches.totalCount"
- :items="matches.branches.list"
- :selected-ref="selectedRef"
- :error="matches.branches.error"
- :error-message="i18n.branchesErrorMessage"
- :show-header="showSectionHeaders"
- data-testid="branches-section"
- data-qa-selector="branches_section"
- :should-show-check="!useSymbolicRefNames || isBranchRefType"
- @selected="selectRef($event)"
- />
-
- <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" />
- </template>
-
- <template v-if="showTagsSection">
- <ref-results-section
- :section-title="i18n.tags"
- :total-count="matches.tags.totalCount"
- :items="matches.tags.list"
- :selected-ref="selectedRef"
- :error="matches.tags.error"
- :error-message="i18n.tagsErrorMessage"
- :show-header="showSectionHeaders"
- data-testid="tags-section"
- :should-show-check="!useSymbolicRefNames || isTagRefType"
- @selected="selectRef($event)"
- />
-
- <gl-dropdown-divider v-if="showCommitsSection" />
- </template>
-
- <template v-if="showCommitsSection">
- <ref-results-section
- :section-title="i18n.commits"
- :total-count="matches.commits.totalCount"
- :items="matches.commits.list"
- :selected-ref="selectedRef"
- :error="matches.commits.error"
- :error-message="i18n.commitsErrorMessage"
- :show-header="showSectionHeaders"
- data-testid="commits-section"
- @selected="selectRef($event)"
- />
- </template>
+ <template #list-item="{ item }">
+ {{ item.text }}
+ <gl-badge v-if="item.default" size="sm" variant="info">{{
+ i18n.defaultLabelText
+ }}</gl-badge>
</template>
-
<template #footer>
<slot name="footer" v-bind="footerSlotProps"></slot>
+ <div
+ v-for="errorMessage in errors"
+ :key="errorMessage"
+ data-testid="red-selector-error-list"
+ class="gl-display-flex gl-align-items-flex-start gl-text-red-500 gl-mx-4 gl-my-3"
+ >
+ <gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" />
+ <span>{{ errorMessage }}</span>
+ </div>
</template>
- </gl-dropdown>
+ </gl-collapsible-listbox>
<input
v-if="name"
data-testid="selected-ref-form-field"
diff --git a/app/assets/javascripts/ref/constants.js b/app/assets/javascripts/ref/constants.js
index f4faa535166..4b5b18cf6c1 100644
--- a/app/assets/javascripts/ref/constants.js
+++ b/app/assets/javascripts/ref/constants.js
@@ -1,5 +1,5 @@
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { __ } from '~/locale';
+import { s__, __ } from '~/locale';
export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES';
export const REF_TYPE_TAGS = 'REF_TYPE_TAGS';
@@ -13,6 +13,7 @@ export const X_TOTAL_HEADER = 'x-total';
export const SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
export const DEFAULT_I18N = Object.freeze({
+ defaultLabelText: __('default'),
dropdownHeader: __('Select Git revision'),
searchPlaceholder: __('Search by Git revision'),
noResultsWithQuery: __('No matching results for "%{query}"'),
@@ -24,4 +25,5 @@ export const DEFAULT_I18N = Object.freeze({
tags: __('Tags'),
commits: __('Commits'),
noRefSelected: __('No ref selected'),
+ totalCountLabel: s__('TotalRefCountIndicator|1000+'),
});
diff --git a/app/assets/javascripts/ref/format_refs.js b/app/assets/javascripts/ref/format_refs.js
new file mode 100644
index 00000000000..af310a35ef4
--- /dev/null
+++ b/app/assets/javascripts/ref/format_refs.js
@@ -0,0 +1,60 @@
+import { DEFAULT_I18N } from './constants';
+
+function convertToListBoxItems(items) {
+ return items.map((item) => ({
+ text: item.name,
+ value: item.value || item.name,
+ default: item.default,
+ }));
+}
+
+/**
+ * Format multiple lists to array of group options for listbox
+ * @param branches list of branches
+ * @param tags list of tags
+ * @param commits list of commits
+ * @returns {*[]} array of group items with header and options
+ */
+export const formatListBoxItems = (branches, tags, commits) => {
+ const listBoxItems = [];
+
+ const addToFinalResult = (items, header) => {
+ if (items && items.length > 0) {
+ listBoxItems.push({
+ text: header,
+ options: convertToListBoxItems(items),
+ });
+ }
+ };
+
+ addToFinalResult(branches, DEFAULT_I18N.branches);
+ addToFinalResult(tags, DEFAULT_I18N.tags);
+ addToFinalResult(commits, DEFAULT_I18N.commits);
+
+ return listBoxItems;
+};
+
+/**
+ * Check error existence and add to final array
+ * @param branches list of branches
+ * @param tags list of tags
+ * @param commits list of commits
+ * @returns {*[]} array of error messages
+ */
+export const formatErrors = (branches, tags, commits) => {
+ const errorsList = [];
+
+ if (branches && branches.error) {
+ errorsList.push(DEFAULT_I18N.branchesErrorMessage);
+ }
+
+ if (tags && tags.error) {
+ errorsList.push(DEFAULT_I18N.tagsErrorMessage);
+ }
+
+ if (commits && commits.error) {
+ errorsList.push(DEFAULT_I18N.commitsErrorMessage);
+ }
+
+ return errorsList;
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
new file mode 100644
index 00000000000..092e8ba6c15
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
@@ -0,0 +1,133 @@
+<script>
+import { GlIntersectionObserver } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { getPageParamValue, getPageSearchString } from '~/blob/utils';
+
+/*
+ * We only highlight the chunk that is currently visible to the user.
+ * By making use of the Intersection Observer API we can determine when a chunk becomes visible and highlight it accordingly.
+ *
+ * Content that is not visible to the user (i.e. not highlighted) does not need to look nice,
+ * so by rendering raw (non-highlighted) text, the browser spends less resources on painting
+ * content that is not immediately relevant.
+ * Why use plaintext as opposed to hiding content entirely?
+ * If content is hidden entirely, native find text (⌘ + F) won't work.
+ */
+export default {
+ components: {
+ GlIntersectionObserver,
+ },
+ directives: {
+ SafeHtml,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ isHighlighted: {
+ type: Boolean,
+ required: true,
+ },
+ chunkIndex: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ rawContent: {
+ type: String,
+ required: true,
+ },
+ highlightedContent: {
+ type: String,
+ required: true,
+ },
+ totalLines: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ startingFrom: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ blamePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ hasAppeared: false,
+ isLoading: true,
+ };
+ },
+ computed: {
+ shouldHighlight() {
+ return Boolean(this.highlightedContent) && (this.hasAppeared || this.isHighlighted);
+ },
+ lines() {
+ return this.content.split('\n');
+ },
+ pageSearchString() {
+ if (!this.glFeatures.fileLineBlame) return '';
+ const page = getPageParamValue(this.number);
+ return getPageSearchString(this.blamePath, page);
+ },
+ },
+ created() {
+ if (this.chunkIndex === 0) {
+ // Display first chunk ASAP in order to improve perceived performance
+ this.isLoading = false;
+ return;
+ }
+
+ window.requestIdleCallback(() => {
+ this.isLoading = false;
+ });
+ },
+ methods: {
+ handleChunkAppear() {
+ this.hasAppeared = true;
+ },
+ calculateLineNumber(index) {
+ return this.startingFrom + index + 1;
+ },
+ },
+};
+</script>
+<template>
+ <gl-intersection-observer @appear="handleChunkAppear">
+ <div class="gl-display-flex">
+ <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
+ <div
+ v-for="(n, index) in totalLines"
+ :key="index"
+ data-testid="line-numbers"
+ class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ >
+ <a
+ v-if="glFeatures.fileLineBlame"
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`"
+ ></a>
+ <a
+ :id="`L${calculateLineNumber(index)}`"
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ :href="`#L${calculateLineNumber(index)}`"
+ :data-line-number="calculateLineNumber(index)"
+ >
+ {{ calculateLineNumber(index) }}
+ </a>
+ </div>
+ </div>
+
+ <div v-else-if="!isLoading" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
+ <!-- Placeholder for line numbers while content is not highlighted -->
+ </div>
+
+ <pre
+ class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
+ ><code v-if="shouldHighlight" v-once v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
+ </div>
+ </gl-intersection-observer>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
new file mode 100644
index 00000000000..11708b6f1f6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -0,0 +1,58 @@
+<script>
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import Tracking from '~/tracking';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants';
+import Chunk from './components/chunk.vue';
+
+export default {
+ components: {
+ Chunk,
+ },
+ directives: {
+ SafeHtml,
+ },
+ mixins: [Tracking.mixin()],
+ inject: {
+ highlightWorker: { default: null },
+ },
+ props: {
+ blob: {
+ type: Object,
+ required: true,
+ },
+ chunks: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ created() {
+ this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language });
+ addBlobLinksTracking();
+ },
+ userColorScheme: window.gon.user_color_scheme,
+};
+</script>
+
+<template>
+ <div
+ class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto"
+ :class="$options.userColorScheme"
+ data-type="simple"
+ :data-path="blob.path"
+ data-qa-selector="blob_viewer_file_content"
+ >
+ <chunk
+ v-for="(chunk, _, index) in chunks"
+ :key="index"
+ :chunk-index="index"
+ :is-highlighted="Boolean(chunk.isHighlighted)"
+ :raw-content="chunk.rawContent"
+ :highlighted-content="chunk.highlightedContent"
+ :total-lines="chunk.totalLines"
+ :starting-from="chunk.startingFrom"
+ :blame-path="blob.blamePath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
index 5eb3da3c62e..d78530239a5 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
@@ -173,6 +173,7 @@ export default {
:can-edit="enableEdit"
:task-list-update-path="taskListUpdatePath"
/>
+ <slot name="secondary-content"></slot>
<small v-if="isUpdated" class="edited-text gl-font-sm!">
{{ __('Edited') }}
<time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" />
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
index 3cab2b7aba7..10d18f9ad2a 100644
--- a/app/graphql/types/ci/runner_type.rb
+++ b/app/graphql/types/ci/runner_type.rb
@@ -142,7 +142,7 @@ module Types
end
def ephemeral_authentication_token
- return unless runner.created_via_ui?
+ return unless runner.authenticated_user_registration_type?
return unless runner.created_at > RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME.ago
return if runner.runner_machines.any?
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 565df2ce621..09ac0fa69e7 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -30,6 +30,11 @@ module Ci
project_type: 3
}
+ enum registration_type: {
+ registration_token: 0,
+ authenticated_user: 1
+ }, _suffix: true
+
# Prefix assigned to runners created from the UI, instead of registered via the command line
CREATED_RUNNER_TOKEN_PREFIX = 'glrt-'
@@ -184,6 +189,7 @@ module Ci
validate :tag_constraints
validates :access_level, presence: true
validates :runner_type, presence: true
+ validates :registration_type, presence: true
validate :no_projects, unless: :project_type?
validate :no_groups, unless: :group_type?
@@ -196,8 +202,6 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at, :executor_type
- attr_writer :legacy_registered
-
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout,
error_message: 'Maximum job timeout has a value which could not be accepted'
@@ -297,13 +301,6 @@ module Ci
end
end
- def initialize(params)
- @legacy_registered = params&.delete(:legacy_registered)
- @legacy_registered = true if @legacy_registered.nil?
-
- super(params)
- end
-
def assign_to(project, current_user = nil)
if instance_type?
raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported'
@@ -389,7 +386,7 @@ module Ci
def short_sha
return unless token
- start_index = created_via_ui? ? CREATED_RUNNER_TOKEN_PREFIX.length : 0
+ start_index = authenticated_user_registration_type? ? CREATED_RUNNER_TOKEN_PREFIX.length : 0
token[start_index..start_index + 8]
end
@@ -493,15 +490,11 @@ module Ci
override :format_token
def format_token(token)
- return token if @legacy_registered
+ return token if registration_token_registration_type?
"#{CREATED_RUNNER_TOKEN_PREFIX}#{token}"
end
- def created_via_ui?
- token.start_with?(CREATED_RUNNER_TOKEN_PREFIX)
- end
-
def ensure_machine(system_xid, &blk)
RunnerMachine.safe_find_or_create_by!(runner_id: id, system_xid: system_xid.to_s, &blk) # rubocop: disable Performance/ActiveRecordSubtransactionMethods
end
diff --git a/app/uploaders/object_storage/s3.rb b/app/uploaders/object_storage/s3.rb
new file mode 100644
index 00000000000..063af8b5389
--- /dev/null
+++ b/app/uploaders/object_storage/s3.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module ObjectStorage
+ module S3
+ def self.signed_head_url(file)
+ fog_storage = ::Fog::Storage.new(file.fog_credentials)
+ fog_dir = fog_storage.directories.new(key: file.fog_directory)
+ fog_file = fog_dir.files.new(key: file.path)
+ expire_at = ::Fog::Time.now + file.fog_authenticated_url_expiration
+
+ fog_file.collection.head_url(fog_file.key, expire_at)
+ end
+ end
+end
diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml
index be67b317746..880bffc43ab 100644
--- a/app/views/projects/merge_requests/_page.html.haml
+++ b/app/views/projects/merge_requests/_page.html.haml
@@ -20,10 +20,9 @@
- add_page_startup_api_call @endpoint_diff_batch_url
.merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version, diffs_batch_cache_key: @diffs_batch_cache_key } }
+ = render "projects/merge_requests/mr_title"
- if moved_mr_sidebar_enabled?
#js-merge-sticky-header{ data: { data: sticky_header_data.to_json } }
- = render "projects/merge_requests/mr_title"
-
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/mr_box"
.merge-request-tabs-holder{ class: "#{'js-tabs-affix' unless ENV['RAILS_ENV'] == 'test'} #{'gl-static' if moved_mr_sidebar_enabled?}" }
diff --git a/config/feature_flags/development/revoke_ssh_signatures.yml b/config/feature_flags/development/revoke_ssh_signatures.yml
index 6232e699515..de50bae7d1d 100644
--- a/config/feature_flags/development/revoke_ssh_signatures.yml
+++ b/config/feature_flags/development/revoke_ssh_signatures.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388986
milestone: '15.9'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/doc/api/product_analytics.md b/doc/api/product_analytics.md
index 6a9f4774bce..c687acdb5db 100644
--- a/doc/api/product_analytics.md
+++ b/doc/api/product_analytics.md
@@ -34,6 +34,11 @@ POST /projects/:id/product_analytics/request/dry-run
The body of the load request must be a valid Cube query.
+NOTE:
+When measuring `TrackedEvents`, you must use `TrackedEvents.*` for `dimensions` and `timeDimensions`. The same rule applies when measuring `Sessions`.
+
+#### Tracked events example
+
```json
{
"query": {
@@ -69,6 +74,29 @@ The body of the load request must be a valid Cube query.
}
```
+#### Sessions example
+
+```json
+{
+ "query": {
+ "measures": [
+ "Sessions.count"
+ ],
+ "timeDimensions": [
+ {
+ "dimension": "Sessions.startAt",
+ "granularity": "day"
+ }
+ ],
+ "order": {
+ "Sessions.startAt": "asc"
+ },
+ "limit": 100
+ },
+ "queryType": "multi"
+}
+```
+
## Send metadata request to Cube
Return Cube Metadata for the Analytics data. For example:
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 529e86d2c94..3d126eeadab 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -210,7 +210,10 @@ When the user is authenticated and `simple` is not set this returns something li
"group_runners_enabled": true,
"lfs_enabled": true,
"creator_id": 1,
+ "import_url": null,
+ "import_type": null,
"import_status": "none",
+ "import_error": null,
"open_issues_count": 0,
"ci_default_git_depth": 20,
"ci_forward_deployment_enabled": true,
@@ -381,6 +384,10 @@ GET /users/:user_id/projects
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
+ "import_url": null,
+ "import_type": null,
+ "import_status": "none",
+ "import_error": null,
"namespace": {
"id": 3,
"name": "Diaspora",
@@ -482,6 +489,10 @@ GET /users/:user_id/projects
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
+ "import_url": null,
+ "import_type": null,
+ "import_status": "none",
+ "import_error": null,
"namespace": {
"id": 4,
"name": "Brightbox",
@@ -898,6 +909,8 @@ GET /projects/:id
"avatar_url": "http://localhost:3000/uploads/group/avatar/3/foo.jpg",
"web_url": "http://localhost:3000/groups/diaspora"
},
+ "import_url": null,
+ "import_type": null,
"import_status": "none",
"import_error": null,
"permissions": {
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index 4fda47a99a0..1c2ae3a56d8 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -421,6 +421,92 @@ scope.
| GitLab Rails app | `17.0` | Create database migrations to drop:<br/>- `runners_registration_token`/`runners_registration_token_encrypted` columns from `application_settings`;<br/>- `runners_token`/`runners_token_encrypted` from `namespaces` table;<br/>- `runners_token`/`runners_token_encrypted` from `projects` table. |
| GitLab Rails app | `17.0` | Remove `:enforce_create_runner_workflow` feature flag. |
+## FAQ
+
+### Will my runner registration workflow break?
+
+If no action is taken before your GitLab instance is upgraded to 16.0, then your runner registration
+worflow will break.
+For self-managed instances, to continue using the previous runner registration process,
+you can disable the `enforce_create_runner_workflow` feature flag until GitLab 17.0.
+
+To avoid a broken workflow, you need to first create a runner in the GitLab runners admin page.
+After that, you'll need to replace the registration token you're using in your runner registration
+workflow with the obtained runner authentication token.
+
+### What is the new runner registration process?
+
+When the new runner registration process is introduced, you will:
+
+1. Create a runner directly in the GitLab UI.
+1. Receive an authentication token in return.
+1. Use the authentication token instead of the registration token.
+
+This has added benefits such as preserved ownership records for runners, and minimizes
+impact on users.
+The addition of a unique system ID ensures that you can reuse the same authentication token across
+multiple runners.
+For example, in an auto-scaling scenario where a runner manager spawns a runner process with a
+fixed authentication token.
+This ID generates once at the runner's startup, persists in a sidecar file, and is sent to the
+GitLab instance when requesting jobs.
+This allows the GitLab instance to display which system executed a given job.
+
+### What is the estimated timeframe for the planned changes?
+
+- In GitLab 15.10, we plan to implement runner creation directly in the runners administration page,
+ and prepare the runner to follow the new workflow.
+- In GitLab 16.0, we plan to disable registration tokens.
+ For self-managed instances, to continue using
+ registration tokens, you can disable the `enforce_create_runner_workflow` feature flag until
+ GitLab 17.0.
+
+ Previous `gitlab-runner` versions (that don't include the new `system_id` value) will start to be
+ rejected by the GitLab instance;
+- In GitLab 17.0, we plan to completely remove support for runner registration tokens.
+
+### How will the `gitlab-runner register` command syntax change?
+
+The `gitlab-runner register` command will stop accepting registration tokens and instead accept new
+authentication tokens generated in the GitLab runners administration page.
+These authentication tokens are recognizable by their `glrt-` prefix.
+
+Example command for GitLab 15.9:
+
+```shell
+gitlab-runner register
+ --executor "shell" \
+ --url "https://gitlab.com/" \
+ --tag-list "shell,mac,gdk,test" \
+ --run-untagged="false" \
+ --locked="false" \
+ --access-level="not_protected" \
+ --non-interactive \
+ --registration-token="GR1348941C6YcZVddc8kjtdU-yWYD"
+```
+
+In GitLab 16.0, the runner will be created in the UI where some of its attributes can be
+pre-configured by the creator.
+Examples are the tag list, locked status, or access level. These are no longer accepted as arguments
+to `register`. The following example shows the new command:
+
+```shell
+gitlab-runner register
+ --executor "shell" \
+ --url "https://gitlab.com/" \
+ --non-interactive \
+ --registration-token="grlt-2CR8_eVxiioB1QmzPZwa"
+```
+
+### How does this change impact auto-scaling scenarios?
+
+In auto-scaling scenarios such as GitLab Runner Operator or GitLab Runner Helm Chart, the
+registration token is replaced with the authentication token generated from the UI.
+This means that the same runner configuration is reused across jobs, instead of creating a runner
+for each job.
+The specific runner can be identified by the unique system ID that is generated when the runner
+process is started.
+
## Status
Status: RFC.
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index 8b2006b7ed4..60450692794 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -124,39 +124,41 @@ deploy_staging:
### Create a dynamic environment
-To create a dynamic name and URL for an environment, you can use
-[predefined CI/CD variables](../variables/predefined_variables.md). For example:
+To create a dynamic environment, you use [CI/CD variables](../variables/index.md) that are unique to each pipeline.
+
+Prerequisites:
+
+- You must have at least the Developer role.
+
+To create a dynamic environment, in your `.gitlab-ci.yml` file:
+
+1. Define a job in the `deploy` stage.
+1. In the job, define the following environment attributes:
+ - `name`: Use a related CI/CD variable like `$CI_COMMIT_REF_SLUG`. Optionally, add a static
+ prefix to the environment's name, which [groups in the UI](#group-similar-environments) all
+ environments with the same prefix.
+ - `url`: Optional. Prefix the hostname with a related CI/CD variable like `$CI_ENVIRONMENT_SLUG`.
+
+NOTE:
+Some characters cannot be used in environment names. For more information about the
+`environment` keywords, see the [`.gitlab-ci.yml` keyword reference](../yaml/index.md#environment).
+
+In the following example, every time the `deploy_review_app` job runs the environment's name and
+URL are defined using unique values.
```yaml
-deploy_review:
+deploy_review_app:
stage: deploy
- script:
- - echo "Deploy a review app"
+ script: make deploy
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_ENVIRONMENT_SLUG.example.com
- rules:
- - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- when: never
- - if: $CI_COMMIT_BRANCH
+ only:
+ - branches
+ except:
+ - main
```
-In this example:
-
-- The `name` is `review/$CI_COMMIT_REF_SLUG`. Because the [environment name](../yaml/index.md#environmentname)
- can contain slashes (`/`), you can use this pattern to distinguish between dynamic and static environments.
-- For the `url`, you could use `$CI_COMMIT_REF_SLUG`, but because this value
- may contain a `/` or other characters that would not be valid in a domain name or URL,
- use `$CI_ENVIRONMENT_SLUG` instead. The `$CI_ENVIRONMENT_SLUG` variable is guaranteed to be unique.
-
-You do not have to use the same prefix or only slashes (`/`) in the dynamic environment name.
-However, when you use this format, you can [group similar environments](#group-similar-environments).
-
-NOTE:
-Some variables cannot be used as environment names or URLs.
-For more information about the `environment` keywords, see
-[the `.gitlab-ci.yml` keyword reference](../yaml/index.md#environment).
-
## Deployment tier of environments
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300741) in GitLab 13.10.
diff --git a/doc/development/testing_guide/end_to_end/style_guide.md b/doc/development/testing_guide/end_to_end/style_guide.md
index 419942d6b8f..32d8bf339ed 100644
--- a/doc/development/testing_guide/end_to_end/style_guide.md
+++ b/doc/development/testing_guide/end_to_end/style_guide.md
@@ -8,6 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
This document describes the conventions used at GitLab for writing End-to-end (E2E) tests using the GitLab QA project.
+Please note that this guide is an extension of the primary [testing standards and style guidelines](../index.md). If this guide defines a rule that contradicts the primary guide, this guide takes precedence.
+
## `click_` versus `go_to_`
### When to use `click_`?
diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md
index e9cfae0fbdb..4d7b25e2d77 100644
--- a/doc/user/packages/container_registry/index.md
+++ b/doc/user/packages/container_registry/index.md
@@ -165,3 +165,9 @@ this setting. However, disabling the Container Registry disables all Container R
| Private project with Container Registry visibility <br/> set to **Everyone With Access** (UI) or `enabled` (API) | View Container Registry <br/> and pull images | No | No | Yes |
| Private project with Container Registry visibility <br/> set to **Only Project Members** (UI) or `private` (API) | View Container Registry <br/> and pull images | No | No | Yes |
| Any project with Container Registry `disabled` | All operations on Container Registry | No | No | No |
+
+## Supported image types
+
+The Container Registry supports [Docker V2](https://docs.docker.com/registry/spec/manifest-v2-2/) and [Open Container Initiative (OCI)](https://github.com/opencontainers/image-spec/blob/main/spec.md) image formats.
+
+OCI support means that you can host OCI-based image formats in the registry, such as [Helm 3+ chart packages](https://helm.sh/docs/topics/registries/). There is no distinction between image formats in the GitLab [API](../../../api/container_registry.md) and the UI. [Issue 38047](https://gitlab.com/gitlab-org/gitlab/-/issues/38047) addresses this distinction, starting with Helm.
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 197bd420295..aadcbe38b15 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -608,7 +608,7 @@ module API
if file.file_storage?
present_disk_file!(file.path, file.filename)
elsif supports_direct_download && file.class.direct_download_enabled?
- return redirect(signed_head_url(file)) if head_request_on_aws_file?(file)
+ return redirect(ObjectStorage::S3.signed_head_url(file)) if request.head? && file.fog_credentials[:provider] == 'AWS'
redirect(cdn_fronted_url(file))
else
@@ -701,19 +701,6 @@ module API
private
- def head_request_on_aws_file?(file)
- request.head? && file.fog_credentials[:provider] == 'AWS'
- end
-
- def signed_head_url(file)
- fog_storage = ::Fog::Storage.new(file.fog_credentials)
- fog_dir = fog_storage.directories.new(key: file.fog_directory)
- fog_file = fog_dir.files.new(key: file.path)
- expire_at = ::Fog::Time.now + file.fog_authenticated_url_expiration
-
- fog_file.collection.head_url(fog_file.key, expire_at)
- end
-
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
diff --git a/lib/gitlab/pages/cache_control.rb b/lib/gitlab/pages/cache_control.rb
index a24d958b7e5..a0b3ab87f38 100644
--- a/lib/gitlab/pages/cache_control.rb
+++ b/lib/gitlab/pages/cache_control.rb
@@ -47,8 +47,15 @@ module Gitlab
# cached settings hash to build the payload cache key to be invalidated.
def clear_cache
keys = cached_settings_hashes
- .map { |hash| payload_cache_key_for(hash) }
- .push(settings_cache_key)
+ .map { |hash| payload_cache_key_for(hash) }
+ .push(settings_cache_key)
+
+ ::Gitlab::AppLogger.info(
+ message: 'clear pages cache',
+ keys: keys,
+ type: @type,
+ id: @id
+ )
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
Rails.cache.delete_multi(keys)
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 49d2d9fed03..1753483b091 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -1,7 +1,8 @@
# frozen_string_literal: true
-require 'set'
namespace :gitlab do
+ require 'set'
+
namespace :cleanup do
desc "GitLab | Cleanup | Block users that have been removed in LDAP"
task block_removed_ldap_users: :gitlab_environment do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c3d86ffbc57..720e9bf647a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -426,6 +426,11 @@ msgid_plural "%d unresolved threads"
msgstr[0] ""
msgstr[1] ""
+msgid "%d version"
+msgid_plural "%d versions"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability"
msgid_plural "%d vulnerabilities"
msgstr[0] ""
@@ -36258,15 +36263,21 @@ msgstr ""
msgid "Requirement %{reference} has been updated"
msgstr ""
-msgid "Requirement title cannot have more than %{limit} characters."
-msgstr ""
-
msgid "Requirements"
msgstr ""
msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture."
msgstr ""
+msgid "Requirement|Legacy requirement ID: %{legacyId}"
+msgstr ""
+
+msgid "Requirement|Legacy requirement IDs are being deprecated. Update your links to reference this item's new ID %{id}. %{linkStart}Learn more%{linkEnd}."
+msgstr ""
+
+msgid "Requirement|Requirements have become work items and the legacy requirement IDs are being deprecated. Update your links to reference this item's new ID %{id}. %{linkStart}Learn more%{linkEnd}."
+msgstr ""
+
msgid "Requires %d approval from eligible users."
msgid_plural "Requires %d approvals from eligible users."
msgstr[0] ""
diff --git a/qa/qa/page/project/settings/default_branch.rb b/qa/qa/page/project/settings/default_branch.rb
index 69ac45ce72d..a59158966c1 100644
--- a/qa/qa/page/project/settings/default_branch.rb
+++ b/qa/qa/page/project/settings/default_branch.rb
@@ -5,6 +5,8 @@ module QA
module Project
module Settings
class DefaultBranch < Page::Base
+ include ::QA::Page::Component::Dropdown
+
view 'app/views/projects/branch_defaults/_show.html.haml' do
element :save_changes_button
end
@@ -13,14 +15,9 @@ module QA
element :default_branch_dropdown
end
- view 'app/assets/javascripts/ref/components/ref_selector.vue' do
- element :ref_selector_searchbox
- end
-
def set_default_branch(branch)
- find_element(:default_branch_dropdown, visible: false).click
- find_element(:ref_selector_searchbox, visible: false).fill_in(with: branch)
- click_button branch
+ expand_select_list
+ search_and_select(branch)
end
def click_save_changes_button
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb
deleted file mode 100644
index 5dff2db6f2b..00000000000
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- RSpec.describe 'Verify', :runner, product_group: :pipeline_authoring do
- context 'when job is configured to only run on merge_request_events' do
- let(:mr_only_job_name) { 'mr_only_job' }
- let(:non_mr_only_job_name) { 'non_mr_only_job' }
- let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
-
- let(:project) do
- Resource::Project.fabricate_via_api! do |project|
- project.name = 'merge-request-only-job'
- end
- end
-
- let!(:runner) do
- Resource::ProjectRunner.fabricate! do |runner|
- runner.project = project
- runner.name = executor
- runner.tags = [executor]
- end
- end
-
- let!(:ci_file) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files(
- [
- {
- file_path: '.gitlab-ci.yml',
- content: <<~YAML
- #{mr_only_job_name}:
- tags: ["#{executor}"]
- script: echo 'OK'
- rules:
- - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- #{non_mr_only_job_name}:
- tags: ["#{executor}"]
- script: echo 'OK'
- rules:
- - if: '$CI_PIPELINE_SOURCE != "merge_request_event"'
- YAML
- }
- ]
- )
- end
- end
-
- let(:merge_request) do
- Resource::MergeRequest.fabricate_via_api! do |merge_request|
- merge_request.project = project
- merge_request.description = Faker::Lorem.sentence
- merge_request.target_new_branch = false
- merge_request.file_name = 'new.txt'
- merge_request.file_content = Faker::Lorem.sentence
- end
- end
-
- before do
- Flow::Login.sign_in
- # TODO: We should remove (wait) revisiting logic when
- # https://gitlab.com/gitlab-org/gitlab/-/issues/385332 is resolved
- Support::Waiter.wait_until do
- merge_request.visit!
- Page::MergeRequest::Show.perform(&:click_pipeline_link)
- Page::Project::Pipeline::Show.perform(&:has_merge_request_badge_tag?)
- end
- end
-
- after do
- runner.remove_via_api!
- end
-
- it 'only runs the job configured to run on merge requests', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347662' do
- Page::Project::Pipeline::Show.perform do |pipeline|
- aggregate_failures do
- expect(pipeline).to have_job(mr_only_job_name)
- expect(pipeline).to have_no_job(non_mr_only_job_name)
- end
- end
- end
- end
- end
-end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 1f4a8eebca2..4758986b47c 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -58,10 +58,6 @@ FactoryBot.define do
end
end
- trait :created_via_ui do
- legacy_registered { false }
- end
-
trait :without_projects do
# we use that to create invalid runner:
# the one without projects
diff --git a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
index eb370cfc1fc..9afd8b3263a 100644
--- a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
+++ b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
+ include ListboxHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:sha) { project.commit.sha }
@@ -18,15 +20,13 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
it 'finds a tag in a list' do
tag_name = 'v1.0.0'
- toggle.click
-
filter_by(tag_name)
wait_for_requests
expect(items_count(tag_name)).to be(1)
- item(tag_name).click
+ select_listbox_item tag_name
expect(toggle).to have_content tag_name
end
@@ -34,22 +34,18 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
it 'finds a branch in a list' do
branch_name = 'audio'
- toggle.click
-
filter_by(branch_name)
wait_for_requests
expect(items_count(branch_name)).to be(1)
- item(branch_name).click
+ select_listbox_item branch_name
expect(toggle).to have_content branch_name
end
it 'finds a commit in a list' do
- toggle.click
-
filter_by(sha)
wait_for_requests
@@ -58,21 +54,19 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
expect(items_count(sha_short)).to be(1)
- item(sha_short).click
+ select_listbox_item sha_short
expect(toggle).to have_content sha_short
end
it 'shows no results when there is no branch, tag or commit sha found' do
non_existing_ref = 'non_existing_branch_name'
-
- toggle.click
-
filter_by(non_existing_ref)
wait_for_requests
- expect(find('.gl-dropdown-contents')).not_to have_content(non_existing_ref)
+ click_button 'master'
+ expect(toggle).not_to have_content(non_existing_ref)
end
def item(ref_name)
@@ -84,6 +78,7 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
end
def filter_by(filter_text)
- fill_in _('Search by Git revision'), with: filter_text
+ click_button 'master'
+ send_keys filter_text
end
end
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 125f7209ab4..c797fd85555 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -4,6 +4,7 @@ require "spec_helper"
RSpec.describe "User browses files", :js, feature_category: :projects do
include RepoHelpers
+ include ListboxHelpers
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
@@ -282,17 +283,13 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
expect(page).to have_content(".gitignore").and have_content("LICENSE")
end
- it "shows files from a repository with apostroph in its name" do
- ref_name = 'test'
+ it "shows files from a repository with apostrophe in its name" do
+ ref_name = 'fix'
find(ref_selector).click
wait_for_requests
- page.within(ref_selector) do
- fill_in 'Search by Git revision', with: ref_name
- wait_for_requests
- find('li', text: ref_name, match: :prefer_exact).click
- end
+ filter_by(ref_name)
expect(find(ref_selector)).to have_text(ref_name)
@@ -307,11 +304,7 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
find(ref_selector).click
wait_for_requests
- page.within(ref_selector) do
- fill_in 'Search by Git revision', with: ref_name
- wait_for_requests
- find('li', text: ref_name, match: :prefer_exact).click
- end
+ filter_by(ref_name)
visit(project_tree_path(project, "fix/.testdir"))
@@ -394,4 +387,12 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
end
end
end
+
+ def filter_by(filter_text)
+ send_keys filter_text
+
+ wait_for_requests
+
+ select_listbox_item filter_text
+ end
end
diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb
index 0a3b8c19cc4..9cc2ce6a7b4 100644
--- a/spec/features/projects/files/user_find_file_spec.rb
+++ b/spec/features/projects/files/user_find_file_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User find project file', feature_category: :projects do
+ include ListboxHelpers
+
let(:user) { create :user }
let(:project) { create :project, :repository }
@@ -22,7 +24,7 @@ RSpec.describe 'User find project file', feature_category: :projects do
end
def ref_selector_dropdown
- find('.gl-dropdown-toggle > .gl-dropdown-button-text')
+ find('.gl-button-text')
end
it 'navigates to find file by shortcut', :js do
@@ -99,7 +101,7 @@ RSpec.describe 'User find project file', feature_category: :projects do
fill_in _('Switch branch/tag'), with: ref
wait_for_requests
- find('.gl-dropdown-item', text: ref).click
+ select_listbox_item(ref)
end
expect(ref_selector_dropdown).to have_text(ref)
end
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 9efbefdc7df..a1f047d9b43 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do
it 'HTML escapes branch name' do
expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>")
- expect(page.find('.gl-dropdown-button-text')['innerHTML']).to eq(ERB::Util.html_escape(branch_name))
+ expect(page.find('.gl-new-dropdown-button-text')['innerHTML']).to include(ERB::Util.html_escape(branch_name))
end
end
@@ -69,18 +69,18 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do
visit charts_project_graph_path(project, 'master')
# Not a huge fan of using a HTML (CSS) selectors here as any change of them will cause a failed test
- ref_selector = find('.ref-selector .gl-dropdown-toggle')
+ ref_selector = find('.ref-selector .gl-new-dropdown-toggle')
scroll_to(ref_selector)
ref_selector.click
- page.within '[data-testid="branches-section"]' do
- dropdown_branch_item = find('.gl-dropdown-item', text: 'add-pdf-file')
+ page.within '.gl-new-dropdown-contents' do
+ dropdown_branch_item = find('li', text: 'add-pdf-file')
scroll_to(dropdown_branch_item)
dropdown_branch_item.click
end
scroll_to(find('.tree-ref-header'), align: :center)
- expect(page).to have_selector '.gl-dropdown-toggle', text: ref_name
+ expect(page).to have_selector '.gl-new-dropdown-toggle', text: ref_name
page.within '.tree-ref-header' do
expect(page).to have_selector('h4', text: ref_name)
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 8beb8af1a8e..3ede76d3360 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do
- expect(first('.gl-dropdown-button-text').text).to eq('master')
+ expect(first('.gl-button-text').text).to eq('master')
end
end
end
@@ -77,7 +77,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do
- expect(first('.gl-dropdown-button-text').text).to eq('master')
+ expect(first('.gl-button-text').text).to eq('master')
end
end
end
@@ -319,7 +319,6 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
end
def select_target_branch
- find('[data-testid="schedule-target-ref"] .dropdown-toggle').click
click_button 'master'
end
diff --git a/spec/features/projects/settings/user_changes_default_branch_spec.rb b/spec/features/projects/settings/user_changes_default_branch_spec.rb
index 39704fdbbb2..67ba16a2716 100644
--- a/spec/features/projects/settings/user_changes_default_branch_spec.rb
+++ b/spec/features/projects/settings/user_changes_default_branch_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :projects do
+ include ListboxHelpers
+
let(:user) { create(:user) }
before do
@@ -20,10 +22,10 @@ RSpec.describe 'Projects > Settings > User changes default branch', feature_cate
wait_for_requests
expect(page).to have_selector(dropdown_selector)
- find(dropdown_selector).click
+ click_button 'master'
+ send_keys 'fix'
- fill_in 'Search branch', with: 'fix'
- click_button 'fix'
+ select_listbox_item 'fix'
page.within '#branch-defaults-settings' do
click_button 'Save changes'
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index 835a3cda65e..d73642a28bb 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
include WebIdeSpecHelpers
include RepoHelpers
+ include ListboxHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -160,17 +161,13 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
context 'ref switcher', :js do
it 'switches ref to branch' do
ref_selector = '.ref-selector'
- ref_name = 'feature'
+ ref_name = 'fix'
visit project_tree_path(project, 'master')
- find(ref_selector).click
- wait_for_requests
+ click_button 'master'
+ send_keys ref_name
- page.within(ref_selector) do
- fill_in 'Search by Git revision', with: ref_name
- wait_for_requests
- find('li', text: ref_name, match: :prefer_exact).click
- end
+ select_listbox_item ref_name
expect(find(ref_selector)).to have_text(ref_name)
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 6c9db029683..b7d06a3a962 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -3,6 +3,9 @@
require 'spec_helper'
RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_category: :global_search do
+ using RSpec::Parameterized::TableSyntax
+ include ListboxHelpers
+
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) }
@@ -83,14 +86,10 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
expect(page).to have_selector('.results', text: expected_result)
- find('.ref-selector').click
+ click_button 'master'
wait_for_requests
- page.within('.ref-selector') do
- fill_in 'Search by Git revision', with: ref_selector
- wait_for_requests
- find('li', text: ref_selector, match: :prefer_exact).click
- end
+ select_listbox_item(ref_selector)
expect(page).to have_selector('.results', text: expected_result)
@@ -137,18 +136,12 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
end
it 'search result changes when refs switched' do
- ref = 'master'
expect(find('.results')).not_to have_content('path = gitlab-grack')
find('.ref-selector').click
wait_for_requests
- page.within('.ref-selector') do
- fill_in _('Search by Git revision'), with: ref
- wait_for_requests
-
- find('li', text: ref).click
- end
+ select_listbox_item('add-ipython-files')
expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end
@@ -192,18 +185,12 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
end
it 'search result changes when refs switched' do
- ref = 'master'
expect(find('.results')).not_to have_content('path = gitlab-grack')
find('.ref-selector').click
wait_for_requests
- page.within('.ref-selector') do
- fill_in _('Search by Git revision'), with: ref
- wait_for_requests
-
- find('li', text: ref).click
- end
+ select_listbox_item('add-ipython-files')
expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end
diff --git a/spec/features/tags/developer_creates_tag_spec.rb b/spec/features/tags/developer_creates_tag_spec.rb
index 111710ba325..6a1db051e87 100644
--- a/spec/features/tags/developer_creates_tag_spec.rb
+++ b/spec/features/tags/developer_creates_tag_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
page.within(ref_selector) do
fill_in _('Search by Git revision'), with: ref_name
wait_for_requests
- expect(find('.gl-dropdown-contents')).not_to have_content(ref_name)
+ expect(find('.gl-new-dropdown-inner')).not_to have_content(ref_name)
end
end
@@ -60,9 +60,9 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
page.within ref_row do
ref_input = find('[name="ref"]', visible: false)
expect(ref_input.value).to eq 'master'
- expect(find('.gl-dropdown-button-text')).to have_content 'master'
+ expect(find('.gl-button-text')).to have_content 'master'
find('.ref-selector').click
- expect(find('.dropdown-menu')).to have_content 'test'
+ expect(find('.gl-new-dropdown-inner')).to have_content 'test'
end
end
end
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index d8bb03404f3..508af964ca3 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -438,42 +438,24 @@ describe('Ci variable modal', () => {
raw: true,
};
- describe('and FF is enabled', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: validRawMaskedVariable },
- });
- });
-
- it('should not show an error with symbols', async () => {
- await findMaskedVariableCheckbox().trigger('click');
-
- expect(findModal().text()).not.toContain(maskError);
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: validRawMaskedVariable },
});
+ });
- it('should not show an error when length is less than 8', async () => {
- await findValueField().vm.$emit('input', 'a');
- await findMaskedVariableCheckbox().trigger('click');
+ it('should not show an error with symbols', async () => {
+ await findMaskedVariableCheckbox().trigger('click');
- expect(findModal().text()).toContain(maskError);
- });
+ expect(findModal().text()).not.toContain(maskError);
});
- describe('and FF is disabled', () => {
- beforeEach(() => {
- createComponent({
- mountFn: mountExtended,
- props: { selectedVariable: validRawMaskedVariable },
- provide: { glFeatures: { ciRemoveCharacterLimitationRawMaskedVar: false } },
- });
- });
-
- it('should show an error with symbols', async () => {
- await findMaskedVariableCheckbox().trigger('click');
+ it('should not show an error when length is less than 8', async () => {
+ await findValueField().vm.$emit('input', 'a');
+ await findMaskedVariableCheckbox().trigger('click');
- expect(findModal().text()).toContain(maskError);
- });
+ expect(findModal().text()).toContain(maskError);
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
index 20a459e2c1a..27c0ab96cfc 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -1,8 +1,16 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import Tracking from '~/tracking';
+import {
+ CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
import { packageData } from '../../mock_data';
describe('PackageVersionsList', () => {
@@ -24,6 +32,7 @@ describe('PackageVersionsList', () => {
findRegistryList: () => wrapper.findComponent(RegistryList),
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
findListRow: () => wrapper.findAllComponents(VersionRow),
+ findDeletePackagesModal: () => wrapper.findComponent(DeleteModal),
};
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackageVersionsList, {
@@ -35,6 +44,11 @@ describe('PackageVersionsList', () => {
},
stubs: {
RegistryList,
+ DeleteModal: stubComponent(DeleteModal, {
+ methods: {
+ show: jest.fn(),
+ },
+ }),
},
slots: {
'empty-state': EmptySlotStub,
@@ -144,4 +158,80 @@ describe('PackageVersionsList', () => {
expect(wrapper.emitted('next-page')).toHaveLength(1);
});
});
+
+ describe('when the user can bulk destroy versions', () => {
+ let eventSpy;
+ const { findDeletePackagesModal, findRegistryList } = uiElements;
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ mountComponent({ canDestroy: true });
+ });
+
+ it('binds the right props', () => {
+ expect(uiElements.findRegistryList().props()).toMatchObject({
+ items: packageList,
+ pagination: {},
+ isLoading: false,
+ hiddenDelete: false,
+ title: '2 versions',
+ });
+ });
+
+ describe('upon deletion', () => {
+ beforeEach(() => {
+ findRegistryList().vm.$emit('delete', packageList);
+ });
+
+ it('passes itemsToBeDeleted to the modal', () => {
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(packageList);
+ expect(wrapper.emitted('delete')).toBeUndefined();
+ });
+
+ it('requesting delete tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+
+ describe('when modal confirms', () => {
+ beforeEach(() => {
+ findDeletePackagesModal().vm.$emit('confirm');
+ });
+
+ it('emits delete event', () => {
+ expect(wrapper.emitted('delete')[0]).toEqual([packageList]);
+ });
+
+ it('tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+
+ it.each(['confirm', 'cancel'])(
+ 'resets itemsToBeDeleted when modal emits %s',
+ async (event) => {
+ await findDeletePackagesModal().vm.$emit(event);
+
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0);
+ },
+ );
+
+ it('canceling delete tracks the right action', () => {
+ findDeletePackagesModal().vm.$emit('cancel');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
index 0d74b59ae5b..67340822fa5 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
@@ -1,6 +1,7 @@
-import { GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
+import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
@@ -15,17 +16,20 @@ const packageVersion = packageVersions()[0];
describe('VersionRow', () => {
let wrapper;
+ const findListItem = () => wrapper.findComponent(ListItem);
const findLink = () => wrapper.findComponent(GlLink);
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const findPackageName = () => wrapper.findComponent(GlTruncate);
const findWarningIcon = () => wrapper.findComponent(GlIcon);
+ const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
- function createComponent(packageEntity = packageVersion) {
+ function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
wrapper = shallowMountExtended(VersionRow, {
propsData: {
packageEntity,
+ selected,
},
stubs: {
GlSprintf,
@@ -76,13 +80,47 @@ describe('VersionRow', () => {
expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt);
});
+ describe('left action template', () => {
+ it('does not render checkbox if not permitted', () => {
+ createComponent({ packageEntity: { ...packageVersion, canDestroy: false } });
+
+ expect(findBulkDeleteAction().exists()).toBe(false);
+ });
+
+ it('renders checkbox', () => {
+ createComponent();
+
+ expect(findBulkDeleteAction().exists()).toBe(true);
+ expect(findBulkDeleteAction().attributes('checked')).toBeUndefined();
+ });
+
+ it('emits select when checked', () => {
+ createComponent();
+
+ findBulkDeleteAction().vm.$emit('change');
+
+ expect(wrapper.emitted('select')).toHaveLength(1);
+ });
+
+ it('renders checkbox in selected state if selected', () => {
+ createComponent({
+ selected: true,
+ });
+
+ expect(findBulkDeleteAction().attributes('checked')).toBe('true');
+ expect(findListItem().props('selected')).toBe(true);
+ });
+ });
+
describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
beforeEach(() => {
createComponent({
- ...packageVersion,
- status: PACKAGE_ERROR_STATUS,
- _links: {
- webPath: null,
+ packageEntity: {
+ ...packageVersion,
+ status: PACKAGE_ERROR_STATUS,
+ _links: {
+ webPath: null,
+ },
},
});
});
@@ -109,10 +147,12 @@ describe('VersionRow', () => {
describe('disabled status', () => {
beforeEach(() => {
createComponent({
- ...packageVersion,
- status: 'something',
- _links: {
- webPath: null,
+ packageEntity: {
+ ...packageVersion,
+ status: 'something',
+ _links: {
+ webPath: null,
+ },
},
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index f3d02e4e6ae..2a78cfb13f9 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -47,6 +47,7 @@ describe('packages_list_row', () => {
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findCreatedDateText = () => wrapper.findByTestId('created-date');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
+ const findListItem = () => wrapper.findComponent(ListItem);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const findPackageName = () => wrapper.findComponent(GlTruncate);
@@ -212,6 +213,9 @@ describe('packages_list_row', () => {
});
expect(findBulkDeleteAction().attributes('checked')).toBe('true');
+ expect(findListItem().props()).toMatchObject({
+ selected: true,
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index ae8cd54a00a..d897be1f344 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -109,6 +109,7 @@ export const packageVersions = () => [
id: 'gid://gitlab/Packages::Package/243',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.1',
...linksData,
@@ -119,6 +120,7 @@ export const packageVersions = () => [
id: 'gid://gitlab/Packages::Package/244',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.2',
...linksData,
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 6ea78de1bfb..b494965a3cb 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -128,6 +128,7 @@ describe('PackagesApp', () => {
const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge');
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
+ const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1);
const findDeletePackages = () => wrapper.findComponent(DeletePackages);
afterEach(() => {
@@ -267,7 +268,7 @@ describe('PackagesApp', () => {
await waitForPromises();
- findDeletePackages().vm.$emit('end');
+ findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'projectListUrl?showSuccessDeleteAlert=true',
@@ -281,7 +282,7 @@ describe('PackagesApp', () => {
await waitForPromises();
- findDeletePackages().vm.$emit('end');
+ findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'groupListUrl?showSuccessDeleteAlert=true',
@@ -600,9 +601,51 @@ describe('PackagesApp', () => {
await waitForPromises();
expect(findVersionsList().props()).toMatchObject({
+ canDestroy: true,
versions: expect.arrayContaining(versionNodes),
});
});
+
+ describe('delete packages', () => {
+ it('exists and has the correct props', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDeletePackages().props()).toMatchObject({
+ refetchQueries: [{ query: getPackageDetails, variables: {} }],
+ showSuccessAlert: true,
+ });
+ });
+
+ it('deletePackages is bound to package-versions-list delete event', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findVersionsList().vm.$emit('delete', [{ id: 1 }]);
+
+ expect(findDeletePackages().emitted('start')).toEqual([[]]);
+ });
+
+ it('start and end event set loading correctly', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findDeletePackages().vm.$emit('start');
+
+ await nextTick();
+
+ expect(findVersionsList().props('isLoading')).toBe(true);
+
+ findDeletePackages().vm.$emit('end');
+
+ await nextTick();
+
+ expect(findVersionsList().props('isLoading')).toBe(false);
+ });
+ });
});
describe('dependency links', () => {
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index bfff0c2fedf..40d3a291074 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -1,5 +1,4 @@
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@@ -8,13 +7,13 @@ import Vuex from 'vuex';
import commit from 'test_fixtures/api/commits/commit.json';
import branches from 'test_fixtures/api/branches/branches.json';
import tags from 'test_fixtures/api/tags/tags.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import {
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NOT_FOUND,
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
-import { ENTER_KEY } from '~/lib/utils/keys';
import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import {
@@ -42,7 +41,7 @@ describe('Ref selector component', () => {
let requestSpies;
const createComponent = (mountOverrides = {}, propsData = {}) => {
- wrapper = mount(
+ wrapper = mountExtended(
RefSelector,
merge(
{
@@ -57,9 +56,6 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: selectedRef });
},
},
- stubs: {
- GlSearchBoxByType: true,
- },
store: createStore(),
},
mountOverrides,
@@ -91,76 +87,63 @@ describe('Ref selector component', () => {
.reply((config) => commitApiCallSpy(config));
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
//
// Finders
//
- const findButtonContent = () => wrapper.find('button');
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ const findButtonToggle = () => wrapper.findByTestId('base-dropdown-toggle');
- const findNoResults = () => wrapper.find('[data-testid="no-results"]');
+ const findNoResults = () => wrapper.findByTestId('listbox-no-results-text');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findListBoxSection = (section) => {
+ const foundSections = wrapper
+ .findAll('[role="group"]')
+ .filter((ul) => ul.text().includes(section));
+ return foundSections.length > 0 ? foundSections.at(0) : foundSections;
+ };
+
+ const findErrorListWrapper = () => wrapper.findByTestId('red-selector-error-list');
- const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
- const findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem);
- const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
+ const findBranchesSection = () => findListBoxSection('Branches');
+ const findBranchDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
- const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
- const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem);
- const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
+ const findTagsSection = () => findListBoxSection('Tags');
- const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
- const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem);
- const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
+ const findCommitsSection = () => findListBoxSection('Commits');
- const findHiddenInputField = () => wrapper.find('[data-testid="selected-ref-form-field"]');
+ const findHiddenInputField = () => wrapper.findByTestId('selected-ref-form-field');
//
// Expecters
//
- const branchesSectionContainsErrorMessage = () => {
- const branchesSection = findBranchesSection();
+ const sectionContainsErrorMessage = (message) => {
+ const errorSection = findErrorListWrapper();
- return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage);
- };
-
- const tagsSectionContainsErrorMessage = () => {
- const tagsSection = findTagsSection();
-
- return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage);
- };
-
- const commitsSectionContainsErrorMessage = () => {
- const commitsSection = findCommitsSection();
-
- return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage);
+ return errorSection ? errorSection.text().includes(message) : false;
};
//
// Convenience methods
//
const updateQuery = (newQuery) => {
- findSearchBox().vm.$emit('input', newQuery);
+ findListbox().vm.$emit('search', newQuery);
};
const selectFirstBranch = async () => {
- findFirstBranchDropdownItem().vm.$emit('click');
+ findListbox().vm.$emit('select', fixtures.branches[0].name);
await nextTick();
};
const selectFirstTag = async () => {
- findFirstTagDropdownItem().vm.$emit('click');
+ findListbox().vm.$emit('select', fixtures.tags[0].name);
await nextTick();
};
const selectFirstCommit = async () => {
- findFirstCommitDropdownItem().vm.$emit('click');
+ findListbox().vm.$emit('select', fixtures.commit.id);
await nextTick();
};
@@ -195,7 +178,7 @@ describe('Ref selector component', () => {
});
describe('when name property is provided', () => {
- it('renders an forrm input hidden field', () => {
+ it('renders an form input hidden field', () => {
const name = 'default_tag';
createComponent({ propsData: { name } });
@@ -205,7 +188,7 @@ describe('Ref selector component', () => {
});
describe('when name property is not provided', () => {
- it('renders an forrm input hidden field', () => {
+ it('renders an form input hidden field', () => {
createComponent();
expect(findHiddenInputField().exists()).toBe(false);
@@ -224,7 +207,7 @@ describe('Ref selector component', () => {
});
it('adds the provided ID to the GlDropdown instance', () => {
- expect(wrapper.findComponent(GlDropdown).attributes().id).toBe(id);
+ expect(findListbox().attributes().id).toBe(id);
});
});
@@ -238,7 +221,7 @@ describe('Ref selector component', () => {
});
it('renders the pre-selected ref name', () => {
- expect(findButtonContent().text()).toBe(preselectedRef);
+ expect(findButtonToggle().text()).toBe(preselectedRef);
});
it('binds hidden input field to the pre-selected ref', () => {
@@ -259,7 +242,7 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: updatedRef });
await nextTick();
- expect(findButtonContent().text()).toBe(updatedRef);
+ expect(findButtonToggle().text()).toBe(updatedRef);
});
});
@@ -296,23 +279,6 @@ describe('Ref selector component', () => {
});
});
- describe('when the Enter is pressed', () => {
- beforeEach(() => {
- createComponent();
-
- return waitForRequests({ andClearMocks: true });
- });
-
- it('requeries the endpoints when Enter is pressed', () => {
- findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
-
- return waitForRequests().then(() => {
- expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
- expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
- });
- });
- });
-
describe('when no results are found', () => {
beforeEach(() => {
branchesApiCallSpy = jest
@@ -357,27 +323,10 @@ describe('Ref selector component', () => {
it('renders the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(true);
- expect(findBranchesSection().props('shouldShowCheck')).toBe(true);
- });
-
- it('renders the "Branches" heading with a total number indicator', () => {
- expect(
- findBranchesSection().find('[data-testid="section-header"]').text(),
- ).toMatchInterpolatedText('Branches 123');
});
it("does not render an error message in the branches section's body", () => {
- expect(branchesSectionContainsErrorMessage()).toBe(false);
- });
-
- it('renders each non-default branch as a selectable item', () => {
- const dropdownItems = findBranchDropdownItems();
-
- fixtures.branches.forEach((b, i) => {
- if (!b.default) {
- expect(dropdownItems.at(i).text()).toBe(b.name);
- }
- });
+ expect(findErrorListWrapper().exists()).toBe(false);
});
it('renders the default branch as a selectable item with a "default" badge', () => {
@@ -418,11 +367,11 @@ describe('Ref selector component', () => {
});
it('renders the branches section in the dropdown', () => {
- expect(findBranchesSection().exists()).toBe(true);
+ expect(findBranchesSection().exists()).toBe(false);
});
it("renders an error message in the branches section's body", () => {
- expect(branchesSectionContainsErrorMessage()).toBe(true);
+ expect(sectionContainsErrorMessage(DEFAULT_I18N.branchesErrorMessage)).toBe(true);
});
});
});
@@ -437,25 +386,16 @@ describe('Ref selector component', () => {
it('renders the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(true);
- expect(findTagsSection().props('shouldShowCheck')).toBe(true);
});
it('renders the "Tags" heading with a total number indicator', () => {
- expect(
- findTagsSection().find('[data-testid="section-header"]').text(),
- ).toMatchInterpolatedText('Tags 456');
+ expect(findTagsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
+ `Tags ${fixtures.tags.length}`,
+ );
});
it("does not render an error message in the tags section's body", () => {
- expect(tagsSectionContainsErrorMessage()).toBe(false);
- });
-
- it('renders each tag as a selectable item', () => {
- const dropdownItems = findTagDropdownItems();
-
- fixtures.tags.forEach((t, i) => {
- expect(dropdownItems.at(i).text()).toBe(t.name);
- });
+ expect(findErrorListWrapper().exists()).toBe(false);
});
});
@@ -485,11 +425,11 @@ describe('Ref selector component', () => {
});
it('renders the tags section in the dropdown', () => {
- expect(findTagsSection().exists()).toBe(true);
+ expect(findTagsSection().exists()).toBe(false);
});
it("renders an error message in the tags section's body", () => {
- expect(tagsSectionContainsErrorMessage()).toBe(true);
+ expect(sectionContainsErrorMessage(DEFAULT_I18N.tagsErrorMessage)).toBe(true);
});
});
});
@@ -509,19 +449,13 @@ describe('Ref selector component', () => {
});
it('renders the "Commits" heading with a total number indicator', () => {
- expect(
- findCommitsSection().find('[data-testid="section-header"]').text(),
- ).toMatchInterpolatedText('Commits 1');
- });
-
- it("does not render an error message in the comits section's body", () => {
- expect(commitsSectionContainsErrorMessage()).toBe(false);
+ expect(findCommitsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
+ `Commits 1`,
+ );
});
- it('renders each commit as a selectable item with the short SHA and commit title', () => {
- const dropdownItems = findCommitDropdownItems();
-
- expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`);
+ it("does not render an error message in the commits section's body", () => {
+ expect(findErrorListWrapper().exists()).toBe(false);
});
});
@@ -553,11 +487,11 @@ describe('Ref selector component', () => {
});
it('renders the commits section in the dropdown', () => {
- expect(findCommitsSection().exists()).toBe(true);
+ expect(findCommitsSection().exists()).toBe(false);
});
it("renders an error message in the commits section's body", () => {
- expect(commitsSectionContainsErrorMessage()).toBe(true);
+ expect(sectionContainsErrorMessage(DEFAULT_I18N.commitsErrorMessage)).toBe(true);
});
});
});
@@ -571,26 +505,13 @@ describe('Ref selector component', () => {
return waitForRequests();
});
- it('renders a checkmark by the selected item', async () => {
- expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).toHaveClass(
- 'gl-visibility-hidden',
- );
-
- await selectFirstBranch();
-
- expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).not.toHaveClass(
- 'gl-visibility-hidden',
- );
- });
-
- describe('when a branch is seleceted', () => {
+ describe('when a branch is selected', () => {
it("displays the branch name in the dropdown's button", async () => {
- expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
+ expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstBranch();
- await nextTick();
- expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
+ expect(findButtonToggle().text()).toBe(fixtures.branches[0].name);
});
it("updates the v-model binding with the branch's name", async () => {
@@ -604,12 +525,11 @@ describe('Ref selector component', () => {
describe('when a tag is seleceted', () => {
it("displays the tag name in the dropdown's button", async () => {
- expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
+ expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstTag();
- await nextTick();
- expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
+ expect(findButtonToggle().text()).toBe(fixtures.tags[0].name);
});
it("updates the v-model binding with the tag's name", async () => {
@@ -623,12 +543,11 @@ describe('Ref selector component', () => {
describe('when a commit is selected', () => {
it("displays the full SHA in the dropdown's button", async () => {
- expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
+ expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstCommit();
- await nextTick();
- expect(findButtonContent().text()).toBe(fixtures.commit.id);
+ expect(findButtonToggle().text()).toBe(fixtures.commit.id);
});
it("updates the v-model binding with the commit's full SHA", async () => {
@@ -688,21 +607,6 @@ describe('Ref selector component', () => {
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
});
- it('if a ref type becomes disabled, its section is hidden, even if it had some results in store', async () => {
- createComponent({ propsData: { enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] } });
- updateQuery('abcd1234');
- await waitForRequests();
-
- expect(findBranchesSection().exists()).toBe(true);
- expect(findCommitsSection().exists()).toBe(true);
-
- wrapper.setProps({ enabledRefTypes: [REF_TYPE_COMMITS] });
- await waitForRequests();
-
- expect(findBranchesSection().exists()).toBe(false);
- expect(findCommitsSection().exists()).toBe(true);
- });
-
it.each`
enabledRefType | findVisibleSection | findHiddenSections
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
@@ -726,8 +630,7 @@ describe('Ref selector component', () => {
describe('validation state', () => {
const invalidClass = 'gl-inset-border-1-red-500!';
- const isInvalidClassApplied = () =>
- wrapper.findComponent(GlDropdown).props('toggleClass')[0][invalidClass];
+ const isInvalidClassApplied = () => findListbox().props('toggleClass')[0][invalidClass];
describe('valid state', () => {
describe('when the state prop is not provided', () => {
diff --git a/spec/frontend/ref/format_refs_spec.js b/spec/frontend/ref/format_refs_spec.js
new file mode 100644
index 00000000000..6dd49574721
--- /dev/null
+++ b/spec/frontend/ref/format_refs_spec.js
@@ -0,0 +1,38 @@
+import { formatListBoxItems, formatErrors } from '~/ref/format_refs';
+import { DEFAULT_I18N } from '~/ref/constants';
+import {
+ MOCK_BRANCHES,
+ MOCK_COMMITS,
+ MOCK_ERROR,
+ MOCK_TAGS,
+ FORMATTED_BRANCHES,
+ FORMATTED_TAGS,
+ FORMATTED_COMMITS,
+} from './mock_data';
+
+describe('formatListBoxItems', () => {
+ it.each`
+ branches | tags | commits | expectedResult
+ ${MOCK_BRANCHES} | ${MOCK_TAGS} | ${MOCK_COMMITS} | ${[FORMATTED_BRANCHES, FORMATTED_TAGS, FORMATTED_COMMITS]}
+ ${MOCK_BRANCHES} | ${[]} | ${MOCK_COMMITS} | ${[FORMATTED_BRANCHES, FORMATTED_COMMITS]}
+ ${[]} | ${[]} | ${MOCK_COMMITS} | ${[FORMATTED_COMMITS]}
+ ${undefined} | ${undefined} | ${MOCK_COMMITS} | ${[FORMATTED_COMMITS]}
+ ${MOCK_BRANCHES} | ${undefined} | ${null} | ${[FORMATTED_BRANCHES]}
+ `('should correctly format listbox items', ({ branches, tags, commits, expectedResult }) => {
+ expect(formatListBoxItems(branches, tags, commits)).toEqual(expectedResult);
+ });
+});
+
+describe('formatErrors', () => {
+ const { branchesErrorMessage, tagsErrorMessage, commitsErrorMessage } = DEFAULT_I18N;
+ it.each`
+ branches | tags | commits | expectedResult
+ ${MOCK_ERROR} | ${MOCK_ERROR} | ${MOCK_ERROR} | ${[branchesErrorMessage, tagsErrorMessage, commitsErrorMessage]}
+ ${MOCK_ERROR} | ${[]} | ${MOCK_ERROR} | ${[branchesErrorMessage, commitsErrorMessage]}
+ ${[]} | ${[]} | ${MOCK_ERROR} | ${[commitsErrorMessage]}
+ ${undefined} | ${undefined} | ${MOCK_ERROR} | ${[commitsErrorMessage]}
+ ${MOCK_ERROR} | ${undefined} | ${null} | ${[branchesErrorMessage]}
+ `('should correctly format listbox errors', ({ branches, tags, commits, expectedResult }) => {
+ expect(formatErrors(branches, tags, commits)).toEqual(expectedResult);
+ });
+});
diff --git a/spec/frontend/ref/mock_data.js b/spec/frontend/ref/mock_data.js
new file mode 100644
index 00000000000..c02d4da7aed
--- /dev/null
+++ b/spec/frontend/ref/mock_data.js
@@ -0,0 +1,87 @@
+export const MOCK_BRANCHES = [
+ {
+ default: true,
+ name: 'main',
+ value: undefined,
+ },
+ {
+ default: false,
+ name: 'test1',
+ value: undefined,
+ },
+ {
+ default: false,
+ name: 'test2',
+ value: undefined,
+ },
+];
+
+export const MOCK_TAGS = [
+ {
+ name: 'test_tag',
+ value: undefined,
+ },
+ {
+ name: 'test_tag2',
+ value: undefined,
+ },
+];
+
+export const MOCK_COMMITS = [
+ {
+ name: 'test_commit',
+ value: undefined,
+ },
+];
+
+export const FORMATTED_BRANCHES = {
+ text: 'Branches',
+ options: [
+ {
+ default: true,
+ text: 'main',
+ value: 'main',
+ },
+ {
+ default: false,
+ text: 'test1',
+ value: 'test1',
+ },
+ {
+ default: false,
+ text: 'test2',
+ value: 'test2',
+ },
+ ],
+};
+
+export const FORMATTED_TAGS = {
+ text: 'Tags',
+ options: [
+ {
+ text: 'test_tag',
+ value: 'test_tag',
+ default: undefined,
+ },
+ {
+ text: 'test_tag2',
+ value: 'test_tag2',
+ default: undefined,
+ },
+ ],
+};
+
+export const FORMATTED_COMMITS = {
+ text: 'Commits',
+ options: [
+ {
+ text: 'test_commit',
+ value: 'test_commit',
+ default: undefined,
+ },
+ ],
+};
+
+export const MOCK_ERROR = {
+ error: new Error('test_error'),
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap
new file mode 100644
index 00000000000..26c9a6f8d5a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Chunk component rendering isHighlighted is true renders line numbers 1`] = `
+<div
+ class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ data-testid="line-numbers"
+>
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ href="some/blame/path.js#L71"
+ />
+
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ data-line-number="71"
+ href="#L71"
+ id="L71"
+ >
+
+ 71
+
+ </a>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
new file mode 100644
index 00000000000..95ef11d776a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -0,0 +1,87 @@
+import { nextTick } from 'vue';
+import { GlIntersectionObserver } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
+import { CHUNK_1, CHUNK_2 } from '../mock_data';
+
+describe('Chunk component', () => {
+ let wrapper;
+ let idleCallbackSpy;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(Chunk, {
+ propsData: { ...CHUNK_1, ...props },
+ provide: { glFeatures: { fileLineBlame: true } },
+ });
+ };
+
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findLineNumbers = () => wrapper.findAllByTestId('line-numbers');
+ const findContent = () => wrapper.findByTestId('content');
+
+ beforeEach(() => {
+ idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
+ createComponent();
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ describe('Intersection observer', () => {
+ it('renders an Intersection observer component', () => {
+ expect(findIntersectionObserver().exists()).toBe(true);
+ });
+
+ it('renders highlighted content if appear event is emitted', async () => {
+ createComponent({ chunkIndex: 1, isHighlighted: false });
+ findIntersectionObserver().vm.$emit('appear');
+
+ await nextTick();
+
+ expect(findContent().exists()).toBe(true);
+ });
+ });
+
+ describe('rendering', () => {
+ it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
+ jest.clearAllMocks();
+
+ expect(window.requestIdleCallback).not.toHaveBeenCalled();
+ expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
+ });
+
+ it('does not render content if browser is not in idle state', () => {
+ idleCallbackSpy.mockRestore();
+ createComponent({ chunkIndex: 1, ...CHUNK_2 });
+
+ expect(findLineNumbers()).toHaveLength(0);
+ expect(findContent().exists()).toBe(false);
+ });
+
+ describe('isHighlighted is false', () => {
+ beforeEach(() => createComponent(CHUNK_2));
+
+ it('does not render line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(0);
+ });
+
+ it('renders raw content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.rawContent);
+ });
+ });
+
+ describe('isHighlighted is true', () => {
+ beforeEach(() => createComponent({ ...CHUNK_2, isHighlighted: true }));
+
+ it('renders line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(CHUNK_2.totalLines);
+
+ // Opted for a snapshot test here since the output is simple and verifies native HTML elements
+ expect(findLineNumbers().at(0).element).toMatchSnapshot();
+ });
+
+ it('renders highlighted content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.highlightedContent);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
new file mode 100644
index 00000000000..f35e9607d5c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
@@ -0,0 +1,24 @@
+const path = 'some/path.js';
+const blamePath = 'some/blame/path.js';
+
+export const LANGUAGE_MOCK = 'docker';
+
+export const BLOB_DATA_MOCK = { language: LANGUAGE_MOCK, path, blamePath };
+
+export const CHUNK_1 = {
+ isHighlighted: true,
+ rawContent: 'chunk 1 raw',
+ highlightedContent: 'chunk 1 highlighted',
+ totalLines: 70,
+ startingFrom: 0,
+ blamePath,
+};
+
+export const CHUNK_2 = {
+ isHighlighted: false,
+ rawContent: 'chunk 2 raw',
+ highlightedContent: 'chunk 2 highlighted',
+ totalLines: 40,
+ startingFrom: 70,
+ blamePath,
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
new file mode 100644
index 00000000000..1c75442b4a8
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -0,0 +1,47 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
+import Tracking from '~/tracking';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
+
+jest.mock('~/blob/blob_links_tracking');
+
+describe('Source Viewer component', () => {
+ let wrapper;
+ const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(SourceViewer, {
+ propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
+ });
+ };
+
+ const findChunks = () => wrapper.findAllComponents(Chunk);
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ return createComponent();
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ describe('event tracking', () => {
+ it('fires a tracking event when the component is created', () => {
+ const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ });
+
+ it('adds blob links tracking', () => {
+ expect(addBlobLinksTracking).toHaveBeenCalled();
+ });
+ });
+
+ describe('rendering', () => {
+ it('renders a Chunk component for each chunk', () => {
+ expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
+ expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
+ });
+ });
+});
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index a0f5ee1ea95..0fcf36ca9dd 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -821,7 +821,7 @@ RSpec.describe API::Helpers, feature_category: :not_owned do
it 'redirects to a CDN-fronted URL' do
expect(helper).to receive(:redirect)
- expect(helper).to receive(:signed_head_url).and_call_original
+ expect(ObjectStorage::S3).to receive(:signed_head_url).and_call_original
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: artifact.file.model).and_call_original
subject
diff --git a/spec/lib/gitlab/pages/cache_control_spec.rb b/spec/lib/gitlab/pages/cache_control_spec.rb
index dd15aa87441..3ffc1998ebc 100644
--- a/spec/lib/gitlab/pages/cache_control_spec.rb
+++ b/spec/lib/gitlab/pages/cache_control_spec.rb
@@ -13,15 +13,23 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
end
it 'clears the cache' do
+ cached_keys = [
+ "pages_domain_for_#{type}_1_settings-hash",
+ "pages_domain_for_#{type}_1"
+ ]
+
+ expect(::Gitlab::AppLogger)
+ .to receive(:info)
+ .with(
+ message: 'clear pages cache',
+ keys: cached_keys,
+ type: type,
+ id: 1
+ )
+
expect(Rails.cache)
.to receive(:delete_multi)
- .with(
- array_including(
- [
- "pages_domain_for_#{type}_1",
- "pages_domain_for_#{type}_1_settings-hash"
- ]
- ))
+ .with(cached_keys)
subject.clear_cache
end
@@ -31,13 +39,13 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
describe '.for_namespace' do
subject(:cache_control) { described_class.for_namespace(1) }
- it_behaves_like 'cache_control', 'namespace'
+ it_behaves_like 'cache_control', :namespace
end
describe '.for_domain' do
subject(:cache_control) { described_class.for_domain(1) }
- it_behaves_like 'cache_control', 'domain'
+ it_behaves_like 'cache_control', :domain
end
describe '#cache_key' do
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 95b87c26c01..01d5fe7f90b 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Runner, feature_category: :runner do
+RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
include StubGitlabCalls
it_behaves_like 'having unique enum values'
@@ -85,6 +85,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
describe 'validation' do
it { is_expected.to validate_presence_of(:access_level) }
it { is_expected.to validate_presence_of(:runner_type) }
+ it { is_expected.to validate_presence_of(:registration_type) }
context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do
@@ -1748,7 +1749,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
context 'when creating new runner via UI' do
- let(:runner) { create(:ci_runner, :created_via_ui) }
+ let(:runner) { create(:ci_runner, registration_type: :authenticated_user) }
specify { expect(runner.token).to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
it { is_expected.not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
@@ -1765,7 +1766,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
context 'when runner is created via UI' do
- let(:runner) { create(:ci_runner, :created_via_ui) }
+ let(:runner) { create(:ci_runner, registration_type: :authenticated_user) }
it { is_expected.to start_with('glrt-') }
end
@@ -1993,20 +1994,4 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
end
end
-
- describe '#created_via_ui?' do
- subject(:created_via_ui) { runner.created_via_ui? }
-
- context 'when runner registered from command line' do
- let(:runner) { create(:ci_runner) }
-
- it { is_expected.to eq false }
- end
-
- context 'when runner created via UI' do
- let(:runner) { create(:ci_runner, :created_via_ui) }
-
- it { is_expected.to eq true }
- end
- end
end
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 89a0e793431..986e3ce9e52 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -525,7 +525,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let_it_be(:creator) { create(:user) }
let(:created_at) { Time.current }
- let(:token_prefix) { '' }
+ let(:token_prefix) { registration_type == :authenticated_user ? 'glrt-' : '' }
+ let(:registration_type) {}
let(:query) do
%(
query {
@@ -539,7 +540,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:runner) do
create(:ci_runner, :group,
- groups: [group], creator: creator, created_at: created_at, token: "#{token_prefix}abc123")
+ groups: [group], creator: creator, created_at: created_at,
+ registration_type: registration_type, token: "#{token_prefix}abc123")
end
before_all do
@@ -570,7 +572,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:user) { creator }
context 'with runner created in UI' do
- let(:token_prefix) { ::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX }
+ let(:registration_type) { :authenticated_user }
context 'with runner created in last 3 hours' do
let(:created_at) { (3.hours - 1.second).ago }
@@ -600,7 +602,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
context 'with runner registered from command line' do
- let(:token_prefix) { '' }
+ let(:registration_type) { :registration_token }
context 'with runner created in last 3 hours' do
let(:created_at) { (3.hours - 1.second).ago }
@@ -614,7 +616,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:user) { create(:admin) }
context 'with runner created in UI' do
- let(:token_prefix) { ::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX }
+ let(:registration_type) { :authenticated_user }
it_behaves_like 'a protected ephemeral_authentication_token'
end
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 092eb442f1f..20aa660d95b 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -125,6 +125,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url)
subject
+
+ expect(response).to have_gitlab_http_status(:redirect)
end
end
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
index 043b413acff..7584e44152e 100644
--- a/spec/services/auto_merge_service_spec.rb
+++ b/spec/services/auto_merge_service_spec.rb
@@ -131,7 +131,7 @@ RSpec.describe AutoMergeService do
subject
end
- context 'when the head piipeline succeeded' do
+ context 'when the head pipeline succeeded' do
let(:pipeline_status) { :success }
it 'returns failed' do
diff --git a/spec/support/helpers/features/branches_helpers.rb b/spec/support/helpers/features/branches_helpers.rb
index d4f96718cc0..dc4fa448167 100644
--- a/spec/support/helpers/features/branches_helpers.rb
+++ b/spec/support/helpers/features/branches_helpers.rb
@@ -22,15 +22,10 @@ module Spec
end
def select_branch(branch_name)
- ref_selector = '.ref-selector'
- find(ref_selector).click
wait_for_requests
- page.within(ref_selector) do
- fill_in _('Search by Git revision'), with: branch_name
- wait_for_requests
- find('li', text: branch_name, match: :prefer_exact).click
- end
+ click_button branch_name
+ send_keys branch_name
end
end
end
diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb
index a24b99bbe61..545e12341ef 100644
--- a/spec/support/helpers/features/releases_helpers.rb
+++ b/spec/support/helpers/features/releases_helpers.rb
@@ -15,17 +15,18 @@ module Spec
module Helpers
module Features
module ReleasesHelpers
+ include ListboxHelpers
+
def select_new_tag_name(tag_name)
page.within '[data-testid="tag-name-field"]' do
find('button').click
-
wait_for_all_requests
find('input[aria-label="Search or create tag"]').set(tag_name)
-
wait_for_all_requests
click_button("Create tag #{tag_name}")
+ click_button tag_name
end
end
@@ -39,7 +40,7 @@ module Spec
wait_for_all_requests
- click_button(branch_name.to_s)
+ select_listbox_item(branch_name.to_s, exact_text: true)
end
end
diff --git a/spec/uploaders/object_storage/s3_spec.rb b/spec/uploaders/object_storage/s3_spec.rb
new file mode 100644
index 00000000000..de86642c58d
--- /dev/null
+++ b/spec/uploaders/object_storage/s3_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ObjectStorage::S3, feature_category: :source_code_management do
+ describe '.signed_head_url' do
+ subject { described_class.signed_head_url(package_file.file) }
+
+ let(:package_file) { create(:package_file) }
+
+ context 'when the provider is AWS' do
+ before do
+ stub_lfs_object_storage(config: Gitlab.config.lfs.object_store.merge(
+ connection: {
+ provider: 'AWS',
+ aws_access_key_id: 'test',
+ aws_secret_access_key: 'test'
+ }
+ ))
+ end
+
+ it 'generates a signed url' do
+ expect_next_instance_of(Fog::AWS::Storage::Files) do |instance|
+ expect(instance).to receive(:head_url).and_return(a_valid_url)
+ end
+
+ subject
+ end
+
+ it 'delegates to Fog::AWS::Storage::Files#head_url' do
+ expect_next_instance_of(Fog::AWS::Storage::Files) do |instance|
+ expect(instance).to receive(:head_url).and_return('stubbed_url')
+ end
+
+ expect(subject).to eq('stubbed_url')
+ end
+ end
+ end
+end