Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a2b7b398c7
commit
283c7bb302
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ query getPackageDetails(
|
|||
nodes {
|
||||
id
|
||||
name
|
||||
canDestroy
|
||||
createdAt
|
||||
version
|
||||
status
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
);
|
||||
listBoxItems() {
|
||||
return formatListBoxItems(this.branches, this.tags, this.commits);
|
||||
},
|
||||
showTagsSection() {
|
||||
return (
|
||||
this.enabledRefTypes.includes(REF_TYPE_TAGS) &&
|
||||
Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error)
|
||||
);
|
||||
branches() {
|
||||
return this.enabledRefTypes.includes(REF_TYPE_BRANCHES) ? this.matches.branches.list : [];
|
||||
},
|
||||
showCommitsSection() {
|
||||
return (
|
||||
this.enabledRefTypes.includes(REF_TYPE_COMMITS) &&
|
||||
Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error)
|
||||
);
|
||||
tags() {
|
||||
return this.enabledRefTypes.includes(REF_TYPE_TAGS) ? this.matches.tags.list : [];
|
||||
},
|
||||
showNoResults() {
|
||||
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
|
||||
},
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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+'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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?}" }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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_`?
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
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);
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mountExtended,
|
||||
props: { selectedVariable: validRawMaskedVariable },
|
||||
});
|
||||
});
|
||||
|
||||
describe('and FF is disabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mountExtended,
|
||||
props: { selectedVariable: validRawMaskedVariable },
|
||||
provide: { glFeatures: { ciRemoveCharacterLimitationRawMaskedVar: false } },
|
||||
});
|
||||
});
|
||||
it('should not show an error with symbols', async () => {
|
||||
await findMaskedVariableCheckbox().trigger('click');
|
||||
|
||||
it('should show an error with symbols', async () => {
|
||||
await findMaskedVariableCheckbox().trigger('click');
|
||||
expect(findModal().text()).not.toContain(maskError);
|
||||
});
|
||||
|
||||
expect(findModal().text()).toContain(maskError);
|
||||
});
|
||||
it('should not show an error when length is less than 8', async () => {
|
||||
await findValueField().vm.$emit('input', 'a');
|
||||
await findMaskedVariableCheckbox().trigger('click');
|
||||
|
||||
expect(findModal().text()).toContain(maskError);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 findNoResults = () => wrapper.find('[data-testid="no-results"]');
|
||||
const findButtonToggle = () => wrapper.findByTestId('base-dropdown-toggle');
|
||||
|
||||
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 findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
|
||||
const findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem);
|
||||
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
|
||||
const findErrorListWrapper = () => wrapper.findByTestId('red-selector-error-list');
|
||||
|
||||
const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
|
||||
const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem);
|
||||
const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
|
||||
const findBranchesSection = () => findListBoxSection('Branches');
|
||||
const findBranchDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
|
||||
|
||||
const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
|
||||
const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem);
|
||||
const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
|
||||
const findTagsSection = () => findListBoxSection('Tags');
|
||||
|
||||
const findHiddenInputField = () => wrapper.find('[data-testid="selected-ref-form-field"]');
|
||||
const findCommitsSection = () => findListBoxSection('Commits');
|
||||
|
||||
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');
|
||||
expect(findCommitsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
|
||||
`Commits 1`,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not render an error message in the comits section's body", () => {
|
||||
expect(commitsSectionContainsErrorMessage()).toBe(false);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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'),
|
||||
};
|
||||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue