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
|
Details: grace period
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/tasks/gitlab/assets.rake'
|
- 'lib/tasks/gitlab/assets.rake'
|
||||||
- 'lib/tasks/gitlab/cleanup.rake'
|
|
||||||
- 'lib/tasks/gitlab/dependency_proxy/migrate.rake'
|
- 'lib/tasks/gitlab/dependency_proxy/migrate.rake'
|
||||||
- 'lib/tasks/gitlab/docs/redirect.rake'
|
- 'lib/tasks/gitlab/docs/redirect.rake'
|
||||||
- 'lib/tasks/gitlab/graphql.rake'
|
- 'lib/tasks/gitlab/graphql.rake'
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,6 @@ export default {
|
||||||
displayMaskedError() {
|
displayMaskedError() {
|
||||||
return !this.canMask && this.variable.masked;
|
return !this.canMask && this.variable.masked;
|
||||||
},
|
},
|
||||||
isUsingRawRegexFlag() {
|
|
||||||
return this.glFeatures.ciRemoveCharacterLimitationRawMaskedVar;
|
|
||||||
},
|
|
||||||
isEditing() {
|
isEditing() {
|
||||||
return this.mode === EDIT_VARIABLE_ACTION;
|
return this.mode === EDIT_VARIABLE_ACTION;
|
||||||
},
|
},
|
||||||
|
|
@ -177,7 +174,7 @@ export default {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
useRawMaskableRegexp() {
|
useRawMaskableRegexp() {
|
||||||
return this.isRaw && this.isUsingRawRegexFlag;
|
return this.isRaw;
|
||||||
},
|
},
|
||||||
variableValidationFeedback() {
|
variableValidationFeedback() {
|
||||||
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
|
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<gl-intersection-observer
|
<gl-intersection-observer
|
||||||
class="gl-relative gl-top-2"
|
class="gl-relative gl-top-n5"
|
||||||
@appear="setStickyHeaderVisible(false)"
|
@appear="setStickyHeaderVisible(false)"
|
||||||
@disappear="setStickyHeaderVisible(true)"
|
@disappear="setStickyHeaderVisible(true)"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,30 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { n__ } from '~/locale';
|
||||||
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
|
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 PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
|
||||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.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 {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
DeleteModal,
|
||||||
VersionRow,
|
VersionRow,
|
||||||
PackagesListLoader,
|
PackagesListLoader,
|
||||||
RegistryList,
|
RegistryList,
|
||||||
},
|
},
|
||||||
|
mixins: [Tracking.mixin()],
|
||||||
props: {
|
props: {
|
||||||
|
canDestroy: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
versions: {
|
versions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
|
|
@ -25,11 +40,35 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
itemsToBeDeleted: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
listTitle() {
|
||||||
|
return n__('%d version', '%d versions', this.versions.length);
|
||||||
|
},
|
||||||
isListEmpty() {
|
isListEmpty() {
|
||||||
return this.versions.length === 0;
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -40,17 +79,34 @@ export default {
|
||||||
<slot v-else-if="isListEmpty" name="empty-state"></slot>
|
<slot v-else-if="isListEmpty" name="empty-state"></slot>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<registry-list
|
<registry-list
|
||||||
:hidden-delete="true"
|
:hidden-delete="!canDestroy"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
:items="versions"
|
:items="versions"
|
||||||
:pagination="pageInfo"
|
:pagination="pageInfo"
|
||||||
|
:title="listTitle"
|
||||||
|
@delete="setItemsToBeDeleted"
|
||||||
@prev-page="$emit('prev-page')"
|
@prev-page="$emit('prev-page')"
|
||||||
@next-page="$emit('next-page')"
|
@next-page="$emit('next-page')"
|
||||||
>
|
>
|
||||||
<template #default="{ item }">
|
<template #default="{ first, item, isSelected, selectItem }">
|
||||||
<version-row :package-entity="item" />
|
<!-- `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>
|
</template>
|
||||||
</registry-list>
|
</registry-list>
|
||||||
|
|
||||||
|
<delete-modal
|
||||||
|
ref="deletePackagesModal"
|
||||||
|
:items-to-be-deleted="itemsToBeDeleted"
|
||||||
|
@confirm="deleteItemsConfirmation"
|
||||||
|
@cancel="deleteItemsCanceled"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
<script>
|
<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 { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
|
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
|
||||||
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
|
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
|
||||||
|
|
@ -15,6 +22,7 @@ import {
|
||||||
export default {
|
export default {
|
||||||
name: 'PackageVersionRow',
|
name: 'PackageVersionRow',
|
||||||
components: {
|
components: {
|
||||||
|
GlFormCheckbox,
|
||||||
GlIcon,
|
GlIcon,
|
||||||
GlLink,
|
GlLink,
|
||||||
GlSprintf,
|
GlSprintf,
|
||||||
|
|
@ -32,6 +40,11 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
selected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
containsWebPathLink() {
|
containsWebPathLink() {
|
||||||
|
|
@ -53,7 +66,15 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<template #left-primary>
|
||||||
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
|
<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">
|
<gl-link v-if="containsWebPathLink" class="gl-text-body gl-min-w-0" :href="packageLink">
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<list-item data-testid="package-row" v-bind="$attrs">
|
<list-item data-testid="package-row" :selected="selected" v-bind="$attrs">
|
||||||
<template #left-action>
|
<template #left-action>
|
||||||
<gl-form-checkbox
|
<gl-form-checkbox
|
||||||
v-if="packageEntity.canDestroy"
|
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 REQUEST_DELETE_PACKAGES_TRACKING_ACTION = 'request_delete_packages';
|
||||||
export const CANCEL_DELETE_PACKAGES_TRACKING_ACTION = 'cancel_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__(
|
export const DELETE_PACKAGES_ERROR_MESSAGE = s__(
|
||||||
'PackageRegistry|Something went wrong while deleting packages.',
|
'PackageRegistry|Something went wrong while deleting packages.',
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ query getPackageDetails(
|
||||||
nodes {
|
nodes {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
canDestroy
|
||||||
createdAt
|
createdAt
|
||||||
version
|
version
|
||||||
status
|
status
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ export default {
|
||||||
deletePackageModalContent: DELETE_MODAL_CONTENT,
|
deletePackageModalContent: DELETE_MODAL_CONTENT,
|
||||||
filesToDelete: [],
|
filesToDelete: [],
|
||||||
mutationLoading: false,
|
mutationLoading: false,
|
||||||
|
versionsMutationLoading: false,
|
||||||
packageEntity: {},
|
packageEntity: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -146,6 +147,9 @@ export default {
|
||||||
isLoading() {
|
isLoading() {
|
||||||
return this.$apollo.queries.packageEntity.loading;
|
return this.$apollo.queries.packageEntity.loading;
|
||||||
},
|
},
|
||||||
|
isVersionsLoading() {
|
||||||
|
return this.isLoading || this.versionsMutationLoading;
|
||||||
|
},
|
||||||
packageFilesLoading() {
|
packageFilesLoading() {
|
||||||
return this.isLoading || this.mutationLoading;
|
return this.isLoading || this.mutationLoading;
|
||||||
},
|
},
|
||||||
|
|
@ -157,9 +161,6 @@ export default {
|
||||||
category: packageTypeToTrackCategory(this.packageType),
|
category: packageTypeToTrackCategory(this.packageType),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
hasVersions() {
|
|
||||||
return this.packageEntity.versions?.nodes?.length > 0;
|
|
||||||
},
|
|
||||||
versionPageInfo() {
|
versionPageInfo() {
|
||||||
return this.packageEntity?.versions?.pageInfo ?? {};
|
return this.packageEntity?.versions?.pageInfo ?? {};
|
||||||
},
|
},
|
||||||
|
|
@ -181,6 +182,14 @@ export default {
|
||||||
PACKAGE_TYPE_PYPI,
|
PACKAGE_TYPE_PYPI,
|
||||||
].includes(this.packageType);
|
].includes(this.packageType);
|
||||||
},
|
},
|
||||||
|
refetchQueriesData() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
query: getPackageDetails,
|
||||||
|
variables: this.queryVariables,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatSize(size) {
|
formatSize(size) {
|
||||||
|
|
@ -206,12 +215,7 @@ export default {
|
||||||
ids,
|
ids,
|
||||||
},
|
},
|
||||||
awaitRefetchQueries: true,
|
awaitRefetchQueries: true,
|
||||||
refetchQueries: [
|
refetchQueries: this.refetchQueriesData,
|
||||||
{
|
|
||||||
query: getPackageDetails,
|
|
||||||
variables: this.queryVariables,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
if (data?.destroyPackageFiles?.errors[0]) {
|
if (data?.destroyPackageFiles?.errors[0]) {
|
||||||
throw data.destroyPackageFiles.errors[0];
|
throw data.destroyPackageFiles.errors[0];
|
||||||
|
|
@ -403,10 +407,19 @@ export default {
|
||||||
}}</gl-badge>
|
}}</gl-badge>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<delete-packages
|
||||||
|
:refetch-queries="refetchQueriesData"
|
||||||
|
show-success-alert
|
||||||
|
@start="versionsMutationLoading = true"
|
||||||
|
@end="versionsMutationLoading = false"
|
||||||
|
>
|
||||||
|
<template #default="{ deletePackages }">
|
||||||
<package-versions-list
|
<package-versions-list
|
||||||
:is-loading="isLoading"
|
:can-destroy="packageEntity.canDestroy"
|
||||||
|
:is-loading="isVersionsLoading"
|
||||||
:page-info="versionPageInfo"
|
:page-info="versionPageInfo"
|
||||||
:versions="packageEntity.versions.nodes"
|
:versions="packageEntity.versions.nodes"
|
||||||
|
@delete="deletePackages"
|
||||||
@prev-page="fetchPreviousVersionsPage"
|
@prev-page="fetchPreviousVersionsPage"
|
||||||
@next-page="fetchNextVersionsPage"
|
@next-page="fetchNextVersionsPage"
|
||||||
>
|
>
|
||||||
|
|
@ -416,6 +429,8 @@ export default {
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</package-versions-list>
|
</package-versions-list>
|
||||||
|
</template>
|
||||||
|
</delete-packages>
|
||||||
</gl-tab>
|
</gl-tab>
|
||||||
</gl-tabs>
|
</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>
|
<script>
|
||||||
import {
|
import { GlBadge, GlIcon, GlCollapsibleListbox } from '@gitlab/ui';
|
||||||
GlDropdown,
|
|
||||||
GlDropdownDivider,
|
|
||||||
GlSearchBoxByType,
|
|
||||||
GlSprintf,
|
|
||||||
GlLoadingIcon,
|
|
||||||
} from '@gitlab/ui';
|
|
||||||
import { debounce, isArray } from 'lodash';
|
import { debounce, isArray } from 'lodash';
|
||||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||||
|
import { sprintf } from '~/locale';
|
||||||
import {
|
import {
|
||||||
ALL_REF_TYPES,
|
ALL_REF_TYPES,
|
||||||
SEARCH_DEBOUNCE_MS,
|
SEARCH_DEBOUNCE_MS,
|
||||||
|
|
@ -15,21 +10,16 @@ import {
|
||||||
REF_TYPE_BRANCHES,
|
REF_TYPE_BRANCHES,
|
||||||
REF_TYPE_TAGS,
|
REF_TYPE_TAGS,
|
||||||
REF_TYPE_COMMITS,
|
REF_TYPE_COMMITS,
|
||||||
BRANCH_REF_TYPE,
|
|
||||||
TAG_REF_TYPE,
|
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import createStore from '../stores';
|
import createStore from '../stores';
|
||||||
import RefResultsSection from './ref_results_section.vue';
|
import { formatListBoxItems, formatErrors } from '../format_refs';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RefSelector',
|
name: 'RefSelector',
|
||||||
components: {
|
components: {
|
||||||
GlDropdown,
|
GlBadge,
|
||||||
GlDropdownDivider,
|
GlIcon,
|
||||||
GlSearchBoxByType,
|
GlCollapsibleListbox,
|
||||||
GlSprintf,
|
|
||||||
GlLoadingIcon,
|
|
||||||
RefResultsSection,
|
|
||||||
},
|
},
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -87,7 +77,6 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleButtonClass: {
|
toggleButtonClass: {
|
||||||
type: [String, Object, Array],
|
type: [String, Object, Array],
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -112,29 +101,17 @@ export default {
|
||||||
...this.translations,
|
...this.translations,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
showBranchesSection() {
|
listBoxItems() {
|
||||||
return (
|
return formatListBoxItems(this.branches, this.tags, this.commits);
|
||||||
this.enabledRefTypes.includes(REF_TYPE_BRANCHES) &&
|
|
||||||
Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
showTagsSection() {
|
branches() {
|
||||||
return (
|
return this.enabledRefTypes.includes(REF_TYPE_BRANCHES) ? this.matches.branches.list : [];
|
||||||
this.enabledRefTypes.includes(REF_TYPE_TAGS) &&
|
|
||||||
Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
showCommitsSection() {
|
tags() {
|
||||||
return (
|
return this.enabledRefTypes.includes(REF_TYPE_TAGS) ? this.matches.tags.list : [];
|
||||||
this.enabledRefTypes.includes(REF_TYPE_COMMITS) &&
|
|
||||||
Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
showNoResults() {
|
commits() {
|
||||||
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
|
return this.enabledRefTypes.includes(REF_TYPE_COMMITS) ? this.matches.commits.list : [];
|
||||||
},
|
|
||||||
showSectionHeaders() {
|
|
||||||
return this.enabledRefTypes.length > 1;
|
|
||||||
},
|
},
|
||||||
extendedToggleButtonClass() {
|
extendedToggleButtonClass() {
|
||||||
const classes = [
|
const classes = [
|
||||||
|
|
@ -142,7 +119,6 @@ export default {
|
||||||
'gl-inset-border-1-red-500!': !this.state,
|
'gl-inset-border-1-red-500!': !this.state,
|
||||||
'gl-font-monospace': Boolean(this.selectedRef),
|
'gl-font-monospace': Boolean(this.selectedRef),
|
||||||
},
|
},
|
||||||
'gl-max-w-26',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (Array.isArray(this.toggleButtonClass)) {
|
if (Array.isArray(this.toggleButtonClass)) {
|
||||||
|
|
@ -160,6 +136,9 @@ export default {
|
||||||
query: this.lastQuery,
|
query: this.lastQuery,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
errors() {
|
||||||
|
return formatErrors(this.matches.branches, this.matches.tags, this.matches.commits);
|
||||||
|
},
|
||||||
selectedRefForDisplay() {
|
selectedRefForDisplay() {
|
||||||
if (this.useSymbolicRefNames && this.selectedRef) {
|
if (this.useSymbolicRefNames && this.selectedRef) {
|
||||||
return this.selectedRef.replace(/^refs\/(tags|heads)\//, '');
|
return this.selectedRef.replace(/^refs\/(tags|heads)\//, '');
|
||||||
|
|
@ -170,11 +149,12 @@ export default {
|
||||||
buttonText() {
|
buttonText() {
|
||||||
return this.selectedRefForDisplay || this.i18n.noRefSelected;
|
return this.selectedRefForDisplay || this.i18n.noRefSelected;
|
||||||
},
|
},
|
||||||
isTagRefType() {
|
noResultsMessage() {
|
||||||
return this.refType === TAG_REF_TYPE;
|
return this.lastQuery
|
||||||
},
|
? sprintf(this.i18n.noResultsWithQuery, {
|
||||||
isBranchRefType() {
|
query: this.lastQuery,
|
||||||
return this.refType === BRANCH_REF_TYPE;
|
})
|
||||||
|
: this.i18n.noResults;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -202,9 +182,7 @@ export default {
|
||||||
// because we need to access the .cancel() method
|
// because we need to access the .cancel() method
|
||||||
// lodash attaches to the function, which is
|
// lodash attaches to the function, which is
|
||||||
// made inaccessible by Vue.
|
// made inaccessible by Vue.
|
||||||
this.debouncedSearch = debounce(function search() {
|
this.debouncedSearch = debounce(this.search, SEARCH_DEBOUNCE_MS);
|
||||||
this.search();
|
|
||||||
}, SEARCH_DEBOUNCE_MS);
|
|
||||||
|
|
||||||
this.setProjectId(this.projectId);
|
this.setProjectId(this.projectId);
|
||||||
|
|
||||||
|
|
@ -231,14 +209,8 @@ export default {
|
||||||
'setSelectedRef',
|
'setSelectedRef',
|
||||||
]),
|
]),
|
||||||
...mapActions({ storeSearch: 'search' }),
|
...mapActions({ storeSearch: 'search' }),
|
||||||
focusSearchBox() {
|
onSearchBoxInput(searchQuery = '') {
|
||||||
this.$refs.searchBox.$el.querySelector('input').focus();
|
this.query = searchQuery?.trim();
|
||||||
},
|
|
||||||
onSearchBoxEnter() {
|
|
||||||
this.debouncedSearch.cancel();
|
|
||||||
this.search();
|
|
||||||
},
|
|
||||||
onSearchBoxInput() {
|
|
||||||
this.debouncedSearch();
|
this.debouncedSearch();
|
||||||
},
|
},
|
||||||
selectRef(ref) {
|
selectRef(ref) {
|
||||||
|
|
@ -248,104 +220,55 @@ export default {
|
||||||
search() {
|
search() {
|
||||||
this.storeSearch(this.query);
|
this.storeSearch(this.query);
|
||||||
},
|
},
|
||||||
|
totalCountText(count) {
|
||||||
|
return count > 999 ? this.i18n.totalCountLabel : `${count}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<gl-dropdown
|
<gl-collapsible-listbox
|
||||||
|
class="ref-selector gl-w-full"
|
||||||
|
block
|
||||||
|
searchable
|
||||||
|
:selected="selectedRef"
|
||||||
:header-text="i18n.dropdownHeader"
|
:header-text="i18n.dropdownHeader"
|
||||||
|
:items="listBoxItems"
|
||||||
|
:no-results-text="noResultsMessage"
|
||||||
|
:searching="isLoading"
|
||||||
|
:search-placeholder="i18n.searchPlaceholder"
|
||||||
:toggle-class="extendedToggleButtonClass"
|
:toggle-class="extendedToggleButtonClass"
|
||||||
:text="buttonText"
|
:toggle-text="buttonText"
|
||||||
class="ref-selector"
|
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
@shown="focusSearchBox"
|
@hidden="$emit('hide')"
|
||||||
|
@search="onSearchBoxInput"
|
||||||
|
@select="selectRef"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #group-label="{ group }">
|
||||||
<gl-search-box-by-type
|
{{ group.text }} <gl-badge size="sm">{{ totalCountText(group.options.length) }}</gl-badge>
|
||||||
ref="searchBox"
|
|
||||||
v-model.trim="query"
|
|
||||||
:placeholder="i18n.searchPlaceholder"
|
|
||||||
autocomplete="off"
|
|
||||||
data-qa-selector="ref_selector_searchbox"
|
|
||||||
@input="onSearchBoxInput"
|
|
||||||
@keydown.enter.prevent="onSearchBoxEnter"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
<template #list-item="{ item }">
|
||||||
<gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" />
|
{{ item.text }}
|
||||||
|
<gl-badge v-if="item.default" size="sm" variant="info">{{
|
||||||
<div
|
i18n.defaultLabelText
|
||||||
v-else-if="showNoResults"
|
}}</gl-badge>
|
||||||
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>
|
</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>
|
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<slot name="footer" v-bind="footerSlotProps"></slot>
|
<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>
|
</template>
|
||||||
</gl-dropdown>
|
</gl-collapsible-listbox>
|
||||||
<input
|
<input
|
||||||
v-if="name"
|
v-if="name"
|
||||||
data-testid="selected-ref-form-field"
|
data-testid="selected-ref-form-field"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
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_BRANCHES = 'REF_TYPE_BRANCHES';
|
||||||
export const REF_TYPE_TAGS = 'REF_TYPE_TAGS';
|
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 SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
|
||||||
|
|
||||||
export const DEFAULT_I18N = Object.freeze({
|
export const DEFAULT_I18N = Object.freeze({
|
||||||
|
defaultLabelText: __('default'),
|
||||||
dropdownHeader: __('Select Git revision'),
|
dropdownHeader: __('Select Git revision'),
|
||||||
searchPlaceholder: __('Search by Git revision'),
|
searchPlaceholder: __('Search by Git revision'),
|
||||||
noResultsWithQuery: __('No matching results for "%{query}"'),
|
noResultsWithQuery: __('No matching results for "%{query}"'),
|
||||||
|
|
@ -24,4 +25,5 @@ export const DEFAULT_I18N = Object.freeze({
|
||||||
tags: __('Tags'),
|
tags: __('Tags'),
|
||||||
commits: __('Commits'),
|
commits: __('Commits'),
|
||||||
noRefSelected: __('No ref selected'),
|
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"
|
:can-edit="enableEdit"
|
||||||
:task-list-update-path="taskListUpdatePath"
|
:task-list-update-path="taskListUpdatePath"
|
||||||
/>
|
/>
|
||||||
|
<slot name="secondary-content"></slot>
|
||||||
<small v-if="isUpdated" class="edited-text gl-font-sm!">
|
<small v-if="isUpdated" class="edited-text gl-font-sm!">
|
||||||
{{ __('Edited') }}
|
{{ __('Edited') }}
|
||||||
<time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" />
|
<time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" />
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ module Types
|
||||||
end
|
end
|
||||||
|
|
||||||
def ephemeral_authentication_token
|
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 unless runner.created_at > RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME.ago
|
||||||
return if runner.runner_machines.any?
|
return if runner.runner_machines.any?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ module Ci
|
||||||
project_type: 3
|
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
|
# Prefix assigned to runners created from the UI, instead of registered via the command line
|
||||||
CREATED_RUNNER_TOKEN_PREFIX = 'glrt-'
|
CREATED_RUNNER_TOKEN_PREFIX = 'glrt-'
|
||||||
|
|
||||||
|
|
@ -184,6 +189,7 @@ module Ci
|
||||||
validate :tag_constraints
|
validate :tag_constraints
|
||||||
validates :access_level, presence: true
|
validates :access_level, presence: true
|
||||||
validates :runner_type, presence: true
|
validates :runner_type, presence: true
|
||||||
|
validates :registration_type, presence: true
|
||||||
|
|
||||||
validate :no_projects, unless: :project_type?
|
validate :no_projects, unless: :project_type?
|
||||||
validate :no_groups, unless: :group_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
|
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,
|
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout,
|
||||||
error_message: 'Maximum job timeout has a value which could not be accepted'
|
error_message: 'Maximum job timeout has a value which could not be accepted'
|
||||||
|
|
||||||
|
|
@ -297,13 +301,6 @@ module Ci
|
||||||
end
|
end
|
||||||
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)
|
def assign_to(project, current_user = nil)
|
||||||
if instance_type?
|
if instance_type?
|
||||||
raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported'
|
raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported'
|
||||||
|
|
@ -389,7 +386,7 @@ module Ci
|
||||||
def short_sha
|
def short_sha
|
||||||
return unless token
|
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]
|
token[start_index..start_index + 8]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -493,15 +490,11 @@ module Ci
|
||||||
|
|
||||||
override :format_token
|
override :format_token
|
||||||
def format_token(token)
|
def format_token(token)
|
||||||
return token if @legacy_registered
|
return token if registration_token_registration_type?
|
||||||
|
|
||||||
"#{CREATED_RUNNER_TOKEN_PREFIX}#{token}"
|
"#{CREATED_RUNNER_TOKEN_PREFIX}#{token}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def created_via_ui?
|
|
||||||
token.start_with?(CREATED_RUNNER_TOKEN_PREFIX)
|
|
||||||
end
|
|
||||||
|
|
||||||
def ensure_machine(system_xid, &blk)
|
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
|
RunnerMachine.safe_find_or_create_by!(runner_id: id, system_xid: system_xid.to_s, &blk) # rubocop: disable Performance/ActiveRecordSubtransactionMethods
|
||||||
end
|
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
|
- 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 } }
|
.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?
|
- if moved_mr_sidebar_enabled?
|
||||||
#js-merge-sticky-header{ data: { data: sticky_header_data.to_json } }
|
#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 } }
|
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
|
||||||
= render "projects/merge_requests/mr_box"
|
= 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?}" }
|
.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'
|
milestone: '15.9'
|
||||||
type: development
|
type: development
|
||||||
group: group::source code
|
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.
|
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
|
```json
|
||||||
{
|
{
|
||||||
"query": {
|
"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
|
## Send metadata request to Cube
|
||||||
|
|
||||||
Return Cube Metadata for the Analytics data. For example:
|
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,
|
"group_runners_enabled": true,
|
||||||
"lfs_enabled": true,
|
"lfs_enabled": true,
|
||||||
"creator_id": 1,
|
"creator_id": 1,
|
||||||
|
"import_url": null,
|
||||||
|
"import_type": null,
|
||||||
"import_status": "none",
|
"import_status": "none",
|
||||||
|
"import_error": null,
|
||||||
"open_issues_count": 0,
|
"open_issues_count": 0,
|
||||||
"ci_default_git_depth": 20,
|
"ci_default_git_depth": 20,
|
||||||
"ci_forward_deployment_enabled": true,
|
"ci_forward_deployment_enabled": true,
|
||||||
|
|
@ -381,6 +384,10 @@ GET /users/:user_id/projects
|
||||||
"created_at": "2013-09-30T13:46:02Z",
|
"created_at": "2013-09-30T13:46:02Z",
|
||||||
"last_activity_at": "2013-09-30T13:46:02Z",
|
"last_activity_at": "2013-09-30T13:46:02Z",
|
||||||
"creator_id": 3,
|
"creator_id": 3,
|
||||||
|
"import_url": null,
|
||||||
|
"import_type": null,
|
||||||
|
"import_status": "none",
|
||||||
|
"import_error": null,
|
||||||
"namespace": {
|
"namespace": {
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"name": "Diaspora",
|
"name": "Diaspora",
|
||||||
|
|
@ -482,6 +489,10 @@ GET /users/:user_id/projects
|
||||||
"created_at": "2013-09-30T13:46:02Z",
|
"created_at": "2013-09-30T13:46:02Z",
|
||||||
"last_activity_at": "2013-09-30T13:46:02Z",
|
"last_activity_at": "2013-09-30T13:46:02Z",
|
||||||
"creator_id": 3,
|
"creator_id": 3,
|
||||||
|
"import_url": null,
|
||||||
|
"import_type": null,
|
||||||
|
"import_status": "none",
|
||||||
|
"import_error": null,
|
||||||
"namespace": {
|
"namespace": {
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"name": "Brightbox",
|
"name": "Brightbox",
|
||||||
|
|
@ -898,6 +909,8 @@ GET /projects/:id
|
||||||
"avatar_url": "http://localhost:3000/uploads/group/avatar/3/foo.jpg",
|
"avatar_url": "http://localhost:3000/uploads/group/avatar/3/foo.jpg",
|
||||||
"web_url": "http://localhost:3000/groups/diaspora"
|
"web_url": "http://localhost:3000/groups/diaspora"
|
||||||
},
|
},
|
||||||
|
"import_url": null,
|
||||||
|
"import_type": null,
|
||||||
"import_status": "none",
|
"import_status": "none",
|
||||||
"import_error": null,
|
"import_error": null,
|
||||||
"permissions": {
|
"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` | 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. |
|
| 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
|
||||||
|
|
||||||
Status: RFC.
|
Status: RFC.
|
||||||
|
|
|
||||||
|
|
@ -124,39 +124,41 @@ deploy_staging:
|
||||||
|
|
||||||
### Create a dynamic environment
|
### Create a dynamic environment
|
||||||
|
|
||||||
To create a dynamic name and URL for an environment, you can use
|
To create a dynamic environment, you use [CI/CD variables](../variables/index.md) that are unique to each pipeline.
|
||||||
[predefined CI/CD variables](../variables/predefined_variables.md). For example:
|
|
||||||
|
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
|
```yaml
|
||||||
deploy_review:
|
deploy_review_app:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
script:
|
script: make deploy
|
||||||
- echo "Deploy a review app"
|
|
||||||
environment:
|
environment:
|
||||||
name: review/$CI_COMMIT_REF_SLUG
|
name: review/$CI_COMMIT_REF_SLUG
|
||||||
url: https://$CI_ENVIRONMENT_SLUG.example.com
|
url: https://$CI_ENVIRONMENT_SLUG.example.com
|
||||||
rules:
|
only:
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
- branches
|
||||||
when: never
|
except:
|
||||||
- if: $CI_COMMIT_BRANCH
|
- 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
|
## Deployment tier of environments
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300741) in GitLab 13.10.
|
> [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.
|
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_`
|
## `click_` versus `go_to_`
|
||||||
|
|
||||||
### When to use `click_`?
|
### 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 **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 |
|
| 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 |
|
| 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?
|
if file.file_storage?
|
||||||
present_disk_file!(file.path, file.filename)
|
present_disk_file!(file.path, file.filename)
|
||||||
elsif supports_direct_download && file.class.direct_download_enabled?
|
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))
|
redirect(cdn_fronted_url(file))
|
||||||
else
|
else
|
||||||
|
|
@ -701,19 +701,6 @@ module API
|
||||||
|
|
||||||
private
|
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
|
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||||
def initial_current_user
|
def initial_current_user
|
||||||
return @initial_current_user if defined?(@initial_current_user)
|
return @initial_current_user if defined?(@initial_current_user)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,13 @@ module Gitlab
|
||||||
.map { |hash| payload_cache_key_for(hash) }
|
.map { |hash| payload_cache_key_for(hash) }
|
||||||
.push(settings_cache_key)
|
.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
|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
||||||
Rails.cache.delete_multi(keys)
|
Rails.cache.delete_multi(keys)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'set'
|
|
||||||
|
|
||||||
namespace :gitlab do
|
namespace :gitlab do
|
||||||
|
require 'set'
|
||||||
|
|
||||||
namespace :cleanup do
|
namespace :cleanup do
|
||||||
desc "GitLab | Cleanup | Block users that have been removed in LDAP"
|
desc "GitLab | Cleanup | Block users that have been removed in LDAP"
|
||||||
task block_removed_ldap_users: :gitlab_environment do
|
task block_removed_ldap_users: :gitlab_environment do
|
||||||
|
|
|
||||||
|
|
@ -426,6 +426,11 @@ msgid_plural "%d unresolved threads"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
msgid "%d version"
|
||||||
|
msgid_plural "%d versions"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
msgid "%d vulnerability"
|
msgid "%d vulnerability"
|
||||||
msgid_plural "%d vulnerabilities"
|
msgid_plural "%d vulnerabilities"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
|
|
@ -36258,15 +36263,21 @@ msgstr ""
|
||||||
msgid "Requirement %{reference} has been updated"
|
msgid "Requirement %{reference} has been updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Requirement title cannot have more than %{limit} characters."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Requirements"
|
msgid "Requirements"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture."
|
msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture."
|
||||||
msgstr ""
|
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 "Requires %d approval from eligible users."
|
||||||
msgid_plural "Requires %d approvals from eligible users."
|
msgid_plural "Requires %d approvals from eligible users."
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ module QA
|
||||||
module Project
|
module Project
|
||||||
module Settings
|
module Settings
|
||||||
class DefaultBranch < Page::Base
|
class DefaultBranch < Page::Base
|
||||||
|
include ::QA::Page::Component::Dropdown
|
||||||
|
|
||||||
view 'app/views/projects/branch_defaults/_show.html.haml' do
|
view 'app/views/projects/branch_defaults/_show.html.haml' do
|
||||||
element :save_changes_button
|
element :save_changes_button
|
||||||
end
|
end
|
||||||
|
|
@ -13,14 +15,9 @@ module QA
|
||||||
element :default_branch_dropdown
|
element :default_branch_dropdown
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/assets/javascripts/ref/components/ref_selector.vue' do
|
|
||||||
element :ref_selector_searchbox
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_default_branch(branch)
|
def set_default_branch(branch)
|
||||||
find_element(:default_branch_dropdown, visible: false).click
|
expand_select_list
|
||||||
find_element(:ref_selector_searchbox, visible: false).fill_in(with: branch)
|
search_and_select(branch)
|
||||||
click_button branch
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def click_save_changes_button
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
trait :created_via_ui do
|
|
||||||
legacy_registered { false }
|
|
||||||
end
|
|
||||||
|
|
||||||
trait :without_projects do
|
trait :without_projects do
|
||||||
# we use that to create invalid runner:
|
# we use that to create invalid runner:
|
||||||
# the one without projects
|
# the one without projects
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
|
RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
|
||||||
|
include ListboxHelpers
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:project) { create(:project, :public, :repository) }
|
let(:project) { create(:project, :public, :repository) }
|
||||||
let(:sha) { project.commit.sha }
|
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
|
it 'finds a tag in a list' do
|
||||||
tag_name = 'v1.0.0'
|
tag_name = 'v1.0.0'
|
||||||
|
|
||||||
toggle.click
|
|
||||||
|
|
||||||
filter_by(tag_name)
|
filter_by(tag_name)
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
expect(items_count(tag_name)).to be(1)
|
expect(items_count(tag_name)).to be(1)
|
||||||
|
|
||||||
item(tag_name).click
|
select_listbox_item tag_name
|
||||||
|
|
||||||
expect(toggle).to have_content tag_name
|
expect(toggle).to have_content tag_name
|
||||||
end
|
end
|
||||||
|
|
@ -34,22 +34,18 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
|
||||||
it 'finds a branch in a list' do
|
it 'finds a branch in a list' do
|
||||||
branch_name = 'audio'
|
branch_name = 'audio'
|
||||||
|
|
||||||
toggle.click
|
|
||||||
|
|
||||||
filter_by(branch_name)
|
filter_by(branch_name)
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
expect(items_count(branch_name)).to be(1)
|
expect(items_count(branch_name)).to be(1)
|
||||||
|
|
||||||
item(branch_name).click
|
select_listbox_item branch_name
|
||||||
|
|
||||||
expect(toggle).to have_content branch_name
|
expect(toggle).to have_content branch_name
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'finds a commit in a list' do
|
it 'finds a commit in a list' do
|
||||||
toggle.click
|
|
||||||
|
|
||||||
filter_by(sha)
|
filter_by(sha)
|
||||||
|
|
||||||
wait_for_requests
|
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)
|
expect(items_count(sha_short)).to be(1)
|
||||||
|
|
||||||
item(sha_short).click
|
select_listbox_item sha_short
|
||||||
|
|
||||||
expect(toggle).to have_content sha_short
|
expect(toggle).to have_content sha_short
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows no results when there is no branch, tag or commit sha found' do
|
it 'shows no results when there is no branch, tag or commit sha found' do
|
||||||
non_existing_ref = 'non_existing_branch_name'
|
non_existing_ref = 'non_existing_branch_name'
|
||||||
|
|
||||||
toggle.click
|
|
||||||
|
|
||||||
filter_by(non_existing_ref)
|
filter_by(non_existing_ref)
|
||||||
|
|
||||||
wait_for_requests
|
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
|
end
|
||||||
|
|
||||||
def item(ref_name)
|
def item(ref_name)
|
||||||
|
|
@ -84,6 +78,7 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by(filter_text)
|
def filter_by(filter_text)
|
||||||
fill_in _('Search by Git revision'), with: filter_text
|
click_button 'master'
|
||||||
|
send_keys filter_text
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ require "spec_helper"
|
||||||
|
|
||||||
RSpec.describe "User browses files", :js, feature_category: :projects do
|
RSpec.describe "User browses files", :js, feature_category: :projects do
|
||||||
include RepoHelpers
|
include RepoHelpers
|
||||||
|
include ListboxHelpers
|
||||||
|
|
||||||
let(:fork_message) do
|
let(:fork_message) do
|
||||||
"You're not allowed to make changes to this project directly. "\
|
"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")
|
expect(page).to have_content(".gitignore").and have_content("LICENSE")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "shows files from a repository with apostroph in its name" do
|
it "shows files from a repository with apostrophe in its name" do
|
||||||
ref_name = 'test'
|
ref_name = 'fix'
|
||||||
|
|
||||||
find(ref_selector).click
|
find(ref_selector).click
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
page.within(ref_selector) do
|
filter_by(ref_name)
|
||||||
fill_in 'Search by Git revision', with: ref_name
|
|
||||||
wait_for_requests
|
|
||||||
find('li', text: ref_name, match: :prefer_exact).click
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(find(ref_selector)).to have_text(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
|
find(ref_selector).click
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
page.within(ref_selector) do
|
filter_by(ref_name)
|
||||||
fill_in 'Search by Git revision', with: ref_name
|
|
||||||
wait_for_requests
|
|
||||||
find('li', text: ref_name, match: :prefer_exact).click
|
|
||||||
end
|
|
||||||
|
|
||||||
visit(project_tree_path(project, "fix/.testdir"))
|
visit(project_tree_path(project, "fix/.testdir"))
|
||||||
|
|
||||||
|
|
@ -394,4 +387,12 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_by(filter_text)
|
||||||
|
send_keys filter_text
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
select_listbox_item filter_text
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'User find project file', feature_category: :projects do
|
RSpec.describe 'User find project file', feature_category: :projects do
|
||||||
|
include ListboxHelpers
|
||||||
|
|
||||||
let(:user) { create :user }
|
let(:user) { create :user }
|
||||||
let(:project) { create :project, :repository }
|
let(:project) { create :project, :repository }
|
||||||
|
|
||||||
|
|
@ -22,7 +24,7 @@ RSpec.describe 'User find project file', feature_category: :projects do
|
||||||
end
|
end
|
||||||
|
|
||||||
def ref_selector_dropdown
|
def ref_selector_dropdown
|
||||||
find('.gl-dropdown-toggle > .gl-dropdown-button-text')
|
find('.gl-button-text')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'navigates to find file by shortcut', :js do
|
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
|
fill_in _('Switch branch/tag'), with: ref
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
find('.gl-dropdown-item', text: ref).click
|
select_listbox_item(ref)
|
||||||
end
|
end
|
||||||
expect(ref_selector_dropdown).to have_text(ref)
|
expect(ref_selector_dropdown).to have_text(ref)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do
|
||||||
|
|
||||||
it 'HTML escapes branch name' 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.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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -69,18 +69,18 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do
|
||||||
visit charts_project_graph_path(project, 'master')
|
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
|
# 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)
|
scroll_to(ref_selector)
|
||||||
ref_selector.click
|
ref_selector.click
|
||||||
|
|
||||||
page.within '[data-testid="branches-section"]' do
|
page.within '.gl-new-dropdown-contents' do
|
||||||
dropdown_branch_item = find('.gl-dropdown-item', text: 'add-pdf-file')
|
dropdown_branch_item = find('li', text: 'add-pdf-file')
|
||||||
scroll_to(dropdown_branch_item)
|
scroll_to(dropdown_branch_item)
|
||||||
dropdown_branch_item.click
|
dropdown_branch_item.click
|
||||||
end
|
end
|
||||||
|
|
||||||
scroll_to(find('.tree-ref-header'), align: :center)
|
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
|
page.within '.tree-ref-header' do
|
||||||
expect(page).to have_selector('h4', text: ref_name)
|
expect(page).to have_selector('h4', text: ref_name)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
|
||||||
|
|
||||||
it 'shows the pipeline schedule with default ref' do
|
it 'shows the pipeline schedule with default ref' do
|
||||||
page.within('[data-testid="schedule-target-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
|
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
|
it 'shows the pipeline schedule with default ref' do
|
||||||
page.within('[data-testid="schedule-target-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
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -319,7 +319,6 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_target_branch
|
def select_target_branch
|
||||||
find('[data-testid="schedule-target-ref"] .dropdown-toggle').click
|
|
||||||
click_button 'master'
|
click_button 'master'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :projects do
|
RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :projects do
|
||||||
|
include ListboxHelpers
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
@ -20,10 +22,10 @@ RSpec.describe 'Projects > Settings > User changes default branch', feature_cate
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
expect(page).to have_selector(dropdown_selector)
|
expect(page).to have_selector(dropdown_selector)
|
||||||
find(dropdown_selector).click
|
click_button 'master'
|
||||||
|
send_keys 'fix'
|
||||||
|
|
||||||
fill_in 'Search branch', with: 'fix'
|
select_listbox_item 'fix'
|
||||||
click_button 'fix'
|
|
||||||
|
|
||||||
page.within '#branch-defaults-settings' do
|
page.within '#branch-defaults-settings' do
|
||||||
click_button 'Save changes'
|
click_button 'Save changes'
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
|
RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
|
||||||
include WebIdeSpecHelpers
|
include WebIdeSpecHelpers
|
||||||
include RepoHelpers
|
include RepoHelpers
|
||||||
|
include ListboxHelpers
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:project) { create(:project, :repository) }
|
let(:project) { create(:project, :repository) }
|
||||||
|
|
@ -160,17 +161,13 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
|
||||||
context 'ref switcher', :js do
|
context 'ref switcher', :js do
|
||||||
it 'switches ref to branch' do
|
it 'switches ref to branch' do
|
||||||
ref_selector = '.ref-selector'
|
ref_selector = '.ref-selector'
|
||||||
ref_name = 'feature'
|
ref_name = 'fix'
|
||||||
visit project_tree_path(project, 'master')
|
visit project_tree_path(project, 'master')
|
||||||
|
|
||||||
find(ref_selector).click
|
click_button 'master'
|
||||||
wait_for_requests
|
send_keys ref_name
|
||||||
|
|
||||||
page.within(ref_selector) do
|
select_listbox_item ref_name
|
||||||
fill_in 'Search by Git revision', with: ref_name
|
|
||||||
wait_for_requests
|
|
||||||
find('li', text: ref_name, match: :prefer_exact).click
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(find(ref_selector)).to have_text(ref_name)
|
expect(find(ref_selector)).to have_text(ref_name)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_category: :global_search do
|
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(:user) { create(:user) }
|
||||||
let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) }
|
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)
|
expect(page).to have_selector('.results', text: expected_result)
|
||||||
|
|
||||||
find('.ref-selector').click
|
click_button 'master'
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
page.within('.ref-selector') do
|
select_listbox_item(ref_selector)
|
||||||
fill_in 'Search by Git revision', with: ref_selector
|
|
||||||
wait_for_requests
|
|
||||||
find('li', text: ref_selector, match: :prefer_exact).click
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page).to have_selector('.results', text: expected_result)
|
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
|
end
|
||||||
|
|
||||||
it 'search result changes when refs switched' do
|
it 'search result changes when refs switched' do
|
||||||
ref = 'master'
|
|
||||||
expect(find('.results')).not_to have_content('path = gitlab-grack')
|
expect(find('.results')).not_to have_content('path = gitlab-grack')
|
||||||
|
|
||||||
find('.ref-selector').click
|
find('.ref-selector').click
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
page.within('.ref-selector') do
|
select_listbox_item('add-ipython-files')
|
||||||
fill_in _('Search by Git revision'), with: ref
|
|
||||||
wait_for_requests
|
|
||||||
|
|
||||||
find('li', text: ref).click
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page).to have_selector('.results', text: 'path = gitlab-grack')
|
expect(page).to have_selector('.results', text: 'path = gitlab-grack')
|
||||||
end
|
end
|
||||||
|
|
@ -192,18 +185,12 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'search result changes when refs switched' do
|
it 'search result changes when refs switched' do
|
||||||
ref = 'master'
|
|
||||||
expect(find('.results')).not_to have_content('path = gitlab-grack')
|
expect(find('.results')).not_to have_content('path = gitlab-grack')
|
||||||
|
|
||||||
find('.ref-selector').click
|
find('.ref-selector').click
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
page.within('.ref-selector') do
|
select_listbox_item('add-ipython-files')
|
||||||
fill_in _('Search by Git revision'), with: ref
|
|
||||||
wait_for_requests
|
|
||||||
|
|
||||||
find('li', text: ref).click
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page).to have_selector('.results', text: 'path = gitlab-grack')
|
expect(page).to have_selector('.results', text: 'path = gitlab-grack')
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
|
||||||
page.within(ref_selector) do
|
page.within(ref_selector) do
|
||||||
fill_in _('Search by Git revision'), with: ref_name
|
fill_in _('Search by Git revision'), with: ref_name
|
||||||
wait_for_requests
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -60,9 +60,9 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
|
||||||
page.within ref_row do
|
page.within ref_row do
|
||||||
ref_input = find('[name="ref"]', visible: false)
|
ref_input = find('[name="ref"]', visible: false)
|
||||||
expect(ref_input.value).to eq 'master'
|
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
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -438,7 +438,6 @@ describe('Ci variable modal', () => {
|
||||||
raw: true,
|
raw: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('and FF is enabled', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({
|
createComponent({
|
||||||
mountFn: mountExtended,
|
mountFn: mountExtended,
|
||||||
|
|
@ -460,23 +459,6 @@ describe('Ci variable modal', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and FF is disabled', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
createComponent({
|
|
||||||
mountFn: mountExtended,
|
|
||||||
props: { selectedVariable: validRawMaskedVariable },
|
|
||||||
provide: { glFeatures: { ciRemoveCharacterLimitationRawMaskedVar: false } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show an error with symbols', async () => {
|
|
||||||
await findMaskedVariableCheckbox().trigger('click');
|
|
||||||
|
|
||||||
expect(findModal().text()).toContain(maskError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the mask state is invalid', () => {
|
describe('when the mask state is invalid', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const [variable] = mockVariables;
|
const [variable] = mockVariables;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
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 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 PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
|
||||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.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 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';
|
import { packageData } from '../../mock_data';
|
||||||
|
|
||||||
describe('PackageVersionsList', () => {
|
describe('PackageVersionsList', () => {
|
||||||
|
|
@ -24,6 +32,7 @@ describe('PackageVersionsList', () => {
|
||||||
findRegistryList: () => wrapper.findComponent(RegistryList),
|
findRegistryList: () => wrapper.findComponent(RegistryList),
|
||||||
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
|
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
|
||||||
findListRow: () => wrapper.findAllComponents(VersionRow),
|
findListRow: () => wrapper.findAllComponents(VersionRow),
|
||||||
|
findDeletePackagesModal: () => wrapper.findComponent(DeleteModal),
|
||||||
};
|
};
|
||||||
const mountComponent = (props) => {
|
const mountComponent = (props) => {
|
||||||
wrapper = shallowMountExtended(PackageVersionsList, {
|
wrapper = shallowMountExtended(PackageVersionsList, {
|
||||||
|
|
@ -35,6 +44,11 @@ describe('PackageVersionsList', () => {
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
RegistryList,
|
RegistryList,
|
||||||
|
DeleteModal: stubComponent(DeleteModal, {
|
||||||
|
methods: {
|
||||||
|
show: jest.fn(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
slots: {
|
slots: {
|
||||||
'empty-state': EmptySlotStub,
|
'empty-state': EmptySlotStub,
|
||||||
|
|
@ -144,4 +158,80 @@ describe('PackageVersionsList', () => {
|
||||||
expect(wrapper.emitted('next-page')).toHaveLength(1);
|
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 { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
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 PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
|
||||||
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.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';
|
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
|
||||||
|
|
@ -15,17 +16,20 @@ const packageVersion = packageVersions()[0];
|
||||||
describe('VersionRow', () => {
|
describe('VersionRow', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
const findListItem = () => wrapper.findComponent(ListItem);
|
||||||
const findLink = () => wrapper.findComponent(GlLink);
|
const findLink = () => wrapper.findComponent(GlLink);
|
||||||
const findPackageTags = () => wrapper.findComponent(PackageTags);
|
const findPackageTags = () => wrapper.findComponent(PackageTags);
|
||||||
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
|
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
|
||||||
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
|
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
|
||||||
const findPackageName = () => wrapper.findComponent(GlTruncate);
|
const findPackageName = () => wrapper.findComponent(GlTruncate);
|
||||||
const findWarningIcon = () => wrapper.findComponent(GlIcon);
|
const findWarningIcon = () => wrapper.findComponent(GlIcon);
|
||||||
|
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
|
||||||
|
|
||||||
function createComponent(packageEntity = packageVersion) {
|
function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
|
||||||
wrapper = shallowMountExtended(VersionRow, {
|
wrapper = shallowMountExtended(VersionRow, {
|
||||||
propsData: {
|
propsData: {
|
||||||
packageEntity,
|
packageEntity,
|
||||||
|
selected,
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
GlSprintf,
|
GlSprintf,
|
||||||
|
|
@ -76,14 +80,48 @@ describe('VersionRow', () => {
|
||||||
expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt);
|
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`, () => {
|
describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({
|
createComponent({
|
||||||
|
packageEntity: {
|
||||||
...packageVersion,
|
...packageVersion,
|
||||||
status: PACKAGE_ERROR_STATUS,
|
status: PACKAGE_ERROR_STATUS,
|
||||||
_links: {
|
_links: {
|
||||||
webPath: null,
|
webPath: null,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -109,11 +147,13 @@ describe('VersionRow', () => {
|
||||||
describe('disabled status', () => {
|
describe('disabled status', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({
|
createComponent({
|
||||||
|
packageEntity: {
|
||||||
...packageVersion,
|
...packageVersion,
|
||||||
status: 'something',
|
status: 'something',
|
||||||
_links: {
|
_links: {
|
||||||
webPath: null,
|
webPath: null,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ describe('packages_list_row', () => {
|
||||||
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
|
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
|
||||||
const findCreatedDateText = () => wrapper.findByTestId('created-date');
|
const findCreatedDateText = () => wrapper.findByTestId('created-date');
|
||||||
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
|
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
|
||||||
|
const findListItem = () => wrapper.findComponent(ListItem);
|
||||||
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
|
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
|
||||||
const findPackageName = () => wrapper.findComponent(GlTruncate);
|
const findPackageName = () => wrapper.findComponent(GlTruncate);
|
||||||
|
|
||||||
|
|
@ -212,6 +213,9 @@ describe('packages_list_row', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findBulkDeleteAction().attributes('checked')).toBe('true');
|
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',
|
id: 'gid://gitlab/Packages::Package/243',
|
||||||
name: '@gitlab-org/package-15',
|
name: '@gitlab-org/package-15',
|
||||||
status: 'DEFAULT',
|
status: 'DEFAULT',
|
||||||
|
canDestroy: true,
|
||||||
tags: { nodes: packageTags() },
|
tags: { nodes: packageTags() },
|
||||||
version: '1.0.1',
|
version: '1.0.1',
|
||||||
...linksData,
|
...linksData,
|
||||||
|
|
@ -119,6 +120,7 @@ export const packageVersions = () => [
|
||||||
id: 'gid://gitlab/Packages::Package/244',
|
id: 'gid://gitlab/Packages::Package/244',
|
||||||
name: '@gitlab-org/package-15',
|
name: '@gitlab-org/package-15',
|
||||||
status: 'DEFAULT',
|
status: 'DEFAULT',
|
||||||
|
canDestroy: true,
|
||||||
tags: { nodes: packageTags() },
|
tags: { nodes: packageTags() },
|
||||||
version: '1.0.2',
|
version: '1.0.2',
|
||||||
...linksData,
|
...linksData,
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ describe('PackagesApp', () => {
|
||||||
const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge');
|
const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge');
|
||||||
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
|
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
|
||||||
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
|
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
|
||||||
|
const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1);
|
||||||
const findDeletePackages = () => wrapper.findComponent(DeletePackages);
|
const findDeletePackages = () => wrapper.findComponent(DeletePackages);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -267,7 +268,7 @@ describe('PackagesApp', () => {
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
findDeletePackages().vm.$emit('end');
|
findDeletePackageModal().vm.$emit('end');
|
||||||
|
|
||||||
expect(window.location.replace).toHaveBeenCalledWith(
|
expect(window.location.replace).toHaveBeenCalledWith(
|
||||||
'projectListUrl?showSuccessDeleteAlert=true',
|
'projectListUrl?showSuccessDeleteAlert=true',
|
||||||
|
|
@ -281,7 +282,7 @@ describe('PackagesApp', () => {
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
findDeletePackages().vm.$emit('end');
|
findDeletePackageModal().vm.$emit('end');
|
||||||
|
|
||||||
expect(window.location.replace).toHaveBeenCalledWith(
|
expect(window.location.replace).toHaveBeenCalledWith(
|
||||||
'groupListUrl?showSuccessDeleteAlert=true',
|
'groupListUrl?showSuccessDeleteAlert=true',
|
||||||
|
|
@ -600,9 +601,51 @@ describe('PackagesApp', () => {
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(findVersionsList().props()).toMatchObject({
|
expect(findVersionsList().props()).toMatchObject({
|
||||||
|
canDestroy: true,
|
||||||
versions: expect.arrayContaining(versionNodes),
|
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', () => {
|
describe('dependency links', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui';
|
import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
|
||||||
import { mount } from '@vue/test-utils';
|
|
||||||
import Vue, { nextTick } from 'vue';
|
import Vue, { nextTick } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
|
@ -8,13 +7,13 @@ import Vuex from 'vuex';
|
||||||
import commit from 'test_fixtures/api/commits/commit.json';
|
import commit from 'test_fixtures/api/commits/commit.json';
|
||||||
import branches from 'test_fixtures/api/branches/branches.json';
|
import branches from 'test_fixtures/api/branches/branches.json';
|
||||||
import tags from 'test_fixtures/api/tags/tags.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 { trimText } from 'helpers/text_helper';
|
||||||
import {
|
import {
|
||||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||||
HTTP_STATUS_NOT_FOUND,
|
HTTP_STATUS_NOT_FOUND,
|
||||||
HTTP_STATUS_OK,
|
HTTP_STATUS_OK,
|
||||||
} from '~/lib/utils/http_status';
|
} from '~/lib/utils/http_status';
|
||||||
import { ENTER_KEY } from '~/lib/utils/keys';
|
|
||||||
import { sprintf } from '~/locale';
|
import { sprintf } from '~/locale';
|
||||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||||
import {
|
import {
|
||||||
|
|
@ -42,7 +41,7 @@ describe('Ref selector component', () => {
|
||||||
let requestSpies;
|
let requestSpies;
|
||||||
|
|
||||||
const createComponent = (mountOverrides = {}, propsData = {}) => {
|
const createComponent = (mountOverrides = {}, propsData = {}) => {
|
||||||
wrapper = mount(
|
wrapper = mountExtended(
|
||||||
RefSelector,
|
RefSelector,
|
||||||
merge(
|
merge(
|
||||||
{
|
{
|
||||||
|
|
@ -57,9 +56,6 @@ describe('Ref selector component', () => {
|
||||||
wrapper.setProps({ value: selectedRef });
|
wrapper.setProps({ value: selectedRef });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stubs: {
|
|
||||||
GlSearchBoxByType: true,
|
|
||||||
},
|
|
||||||
store: createStore(),
|
store: createStore(),
|
||||||
},
|
},
|
||||||
mountOverrides,
|
mountOverrides,
|
||||||
|
|
@ -91,76 +87,63 @@ describe('Ref selector component', () => {
|
||||||
.reply((config) => commitApiCallSpy(config));
|
.reply((config) => commitApiCallSpy(config));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
wrapper.destroy();
|
|
||||||
wrapper = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Finders
|
// 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 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 findErrorListWrapper = () => wrapper.findByTestId('red-selector-error-list');
|
||||||
const findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem);
|
|
||||||
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
|
|
||||||
|
|
||||||
const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
|
const findBranchesSection = () => findListBoxSection('Branches');
|
||||||
const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem);
|
const findBranchDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
|
||||||
const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
|
|
||||||
|
|
||||||
const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
|
const findTagsSection = () => findListBoxSection('Tags');
|
||||||
const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem);
|
|
||||||
const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
|
|
||||||
|
|
||||||
const findHiddenInputField = () => wrapper.find('[data-testid="selected-ref-form-field"]');
|
const findCommitsSection = () => findListBoxSection('Commits');
|
||||||
|
|
||||||
|
const findHiddenInputField = () => wrapper.findByTestId('selected-ref-form-field');
|
||||||
|
|
||||||
//
|
//
|
||||||
// Expecters
|
// Expecters
|
||||||
//
|
//
|
||||||
const branchesSectionContainsErrorMessage = () => {
|
const sectionContainsErrorMessage = (message) => {
|
||||||
const branchesSection = findBranchesSection();
|
const errorSection = findErrorListWrapper();
|
||||||
|
|
||||||
return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage);
|
return errorSection ? errorSection.text().includes(message) : false;
|
||||||
};
|
|
||||||
|
|
||||||
const tagsSectionContainsErrorMessage = () => {
|
|
||||||
const tagsSection = findTagsSection();
|
|
||||||
|
|
||||||
return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const commitsSectionContainsErrorMessage = () => {
|
|
||||||
const commitsSection = findCommitsSection();
|
|
||||||
|
|
||||||
return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Convenience methods
|
// Convenience methods
|
||||||
//
|
//
|
||||||
const updateQuery = (newQuery) => {
|
const updateQuery = (newQuery) => {
|
||||||
findSearchBox().vm.$emit('input', newQuery);
|
findListbox().vm.$emit('search', newQuery);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectFirstBranch = async () => {
|
const selectFirstBranch = async () => {
|
||||||
findFirstBranchDropdownItem().vm.$emit('click');
|
findListbox().vm.$emit('select', fixtures.branches[0].name);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectFirstTag = async () => {
|
const selectFirstTag = async () => {
|
||||||
findFirstTagDropdownItem().vm.$emit('click');
|
findListbox().vm.$emit('select', fixtures.tags[0].name);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectFirstCommit = async () => {
|
const selectFirstCommit = async () => {
|
||||||
findFirstCommitDropdownItem().vm.$emit('click');
|
findListbox().vm.$emit('select', fixtures.commit.id);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -195,7 +178,7 @@ describe('Ref selector component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when name property is provided', () => {
|
describe('when name property is provided', () => {
|
||||||
it('renders an forrm input hidden field', () => {
|
it('renders an form input hidden field', () => {
|
||||||
const name = 'default_tag';
|
const name = 'default_tag';
|
||||||
|
|
||||||
createComponent({ propsData: { name } });
|
createComponent({ propsData: { name } });
|
||||||
|
|
@ -205,7 +188,7 @@ describe('Ref selector component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when name property is not provided', () => {
|
describe('when name property is not provided', () => {
|
||||||
it('renders an forrm input hidden field', () => {
|
it('renders an form input hidden field', () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
expect(findHiddenInputField().exists()).toBe(false);
|
expect(findHiddenInputField().exists()).toBe(false);
|
||||||
|
|
@ -224,7 +207,7 @@ describe('Ref selector component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds the provided ID to the GlDropdown instance', () => {
|
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', () => {
|
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', () => {
|
it('binds hidden input field to the pre-selected ref', () => {
|
||||||
|
|
@ -259,7 +242,7 @@ describe('Ref selector component', () => {
|
||||||
wrapper.setProps({ value: updatedRef });
|
wrapper.setProps({ value: updatedRef });
|
||||||
|
|
||||||
await nextTick();
|
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', () => {
|
describe('when no results are found', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
branchesApiCallSpy = jest
|
branchesApiCallSpy = jest
|
||||||
|
|
@ -357,27 +323,10 @@ describe('Ref selector component', () => {
|
||||||
|
|
||||||
it('renders the branches section in the dropdown', () => {
|
it('renders the branches section in the dropdown', () => {
|
||||||
expect(findBranchesSection().exists()).toBe(true);
|
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", () => {
|
it("does not render an error message in the branches section's body", () => {
|
||||||
expect(branchesSectionContainsErrorMessage()).toBe(false);
|
expect(findErrorListWrapper().exists()).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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the default branch as a selectable item with a "default" badge', () => {
|
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', () => {
|
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", () => {
|
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', () => {
|
it('renders the tags section in the dropdown', () => {
|
||||||
expect(findTagsSection().exists()).toBe(true);
|
expect(findTagsSection().exists()).toBe(true);
|
||||||
expect(findTagsSection().props('shouldShowCheck')).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the "Tags" heading with a total number indicator', () => {
|
it('renders the "Tags" heading with a total number indicator', () => {
|
||||||
expect(
|
expect(findTagsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
|
||||||
findTagsSection().find('[data-testid="section-header"]').text(),
|
`Tags ${fixtures.tags.length}`,
|
||||||
).toMatchInterpolatedText('Tags 456');
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render an error message in the tags section's body", () => {
|
it("does not render an error message in the tags section's body", () => {
|
||||||
expect(tagsSectionContainsErrorMessage()).toBe(false);
|
expect(findErrorListWrapper().exists()).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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -485,11 +425,11 @@ describe('Ref selector component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the tags section in the dropdown', () => {
|
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", () => {
|
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', () => {
|
it('renders the "Commits" heading with a total number indicator', () => {
|
||||||
expect(
|
expect(findCommitsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
|
||||||
findCommitsSection().find('[data-testid="section-header"]').text(),
|
`Commits 1`,
|
||||||
).toMatchInterpolatedText('Commits 1');
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render an error message in the comits section's body", () => {
|
it("does not render an error message in the commits section's body", () => {
|
||||||
expect(commitsSectionContainsErrorMessage()).toBe(false);
|
expect(findErrorListWrapper().exists()).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}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -553,11 +487,11 @@ describe('Ref selector component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the commits section in the dropdown', () => {
|
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", () => {
|
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();
|
return waitForRequests();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a checkmark by the selected item', async () => {
|
describe('when a branch is selected', () => {
|
||||||
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', () => {
|
|
||||||
it("displays the branch name in the dropdown's button", async () => {
|
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 selectFirstBranch();
|
||||||
|
|
||||||
await nextTick();
|
expect(findButtonToggle().text()).toBe(fixtures.branches[0].name);
|
||||||
expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the v-model binding with the branch's name", async () => {
|
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', () => {
|
describe('when a tag is seleceted', () => {
|
||||||
it("displays the tag name in the dropdown's button", async () => {
|
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 selectFirstTag();
|
||||||
|
|
||||||
await nextTick();
|
expect(findButtonToggle().text()).toBe(fixtures.tags[0].name);
|
||||||
expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the v-model binding with the tag's name", async () => {
|
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', () => {
|
describe('when a commit is selected', () => {
|
||||||
it("displays the full SHA in the dropdown's button", async () => {
|
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 selectFirstCommit();
|
||||||
|
|
||||||
await nextTick();
|
expect(findButtonToggle().text()).toBe(fixtures.commit.id);
|
||||||
expect(findButtonContent().text()).toBe(fixtures.commit.id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the v-model binding with the commit's full SHA", async () => {
|
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);
|
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`
|
it.each`
|
||||||
enabledRefType | findVisibleSection | findHiddenSections
|
enabledRefType | findVisibleSection | findHiddenSections
|
||||||
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
|
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
|
||||||
|
|
@ -726,8 +630,7 @@ describe('Ref selector component', () => {
|
||||||
|
|
||||||
describe('validation state', () => {
|
describe('validation state', () => {
|
||||||
const invalidClass = 'gl-inset-border-1-red-500!';
|
const invalidClass = 'gl-inset-border-1-red-500!';
|
||||||
const isInvalidClassApplied = () =>
|
const isInvalidClassApplied = () => findListbox().props('toggleClass')[0][invalidClass];
|
||||||
wrapper.findComponent(GlDropdown).props('toggleClass')[0][invalidClass];
|
|
||||||
|
|
||||||
describe('valid state', () => {
|
describe('valid state', () => {
|
||||||
describe('when the state prop is not provided', () => {
|
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
|
it 'redirects to a CDN-fronted URL' do
|
||||||
expect(helper).to receive(:redirect)
|
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
|
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: artifact.file.model).and_call_original
|
||||||
|
|
||||||
subject
|
subject
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,23 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'clears the cache' do
|
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)
|
expect(Rails.cache)
|
||||||
.to receive(:delete_multi)
|
.to receive(:delete_multi)
|
||||||
.with(
|
.with(cached_keys)
|
||||||
array_including(
|
|
||||||
[
|
|
||||||
"pages_domain_for_#{type}_1",
|
|
||||||
"pages_domain_for_#{type}_1_settings-hash"
|
|
||||||
]
|
|
||||||
))
|
|
||||||
|
|
||||||
subject.clear_cache
|
subject.clear_cache
|
||||||
end
|
end
|
||||||
|
|
@ -31,13 +39,13 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
|
||||||
describe '.for_namespace' do
|
describe '.for_namespace' do
|
||||||
subject(:cache_control) { described_class.for_namespace(1) }
|
subject(:cache_control) { described_class.for_namespace(1) }
|
||||||
|
|
||||||
it_behaves_like 'cache_control', 'namespace'
|
it_behaves_like 'cache_control', :namespace
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.for_domain' do
|
describe '.for_domain' do
|
||||||
subject(:cache_control) { described_class.for_domain(1) }
|
subject(:cache_control) { described_class.for_domain(1) }
|
||||||
|
|
||||||
it_behaves_like 'cache_control', 'domain'
|
it_behaves_like 'cache_control', :domain
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#cache_key' do
|
describe '#cache_key' do
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Ci::Runner, feature_category: :runner do
|
RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
|
||||||
include StubGitlabCalls
|
include StubGitlabCalls
|
||||||
|
|
||||||
it_behaves_like 'having unique enum values'
|
it_behaves_like 'having unique enum values'
|
||||||
|
|
@ -85,6 +85,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
|
||||||
describe 'validation' do
|
describe 'validation' do
|
||||||
it { is_expected.to validate_presence_of(:access_level) }
|
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(: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 is not allowed to pick untagged jobs' do
|
||||||
context 'when runner does not have tags' do
|
context 'when runner does not have tags' do
|
||||||
|
|
@ -1748,7 +1749,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when creating new runner via UI' do
|
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) }
|
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) }
|
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
|
end
|
||||||
|
|
||||||
context 'when runner is created via UI' do
|
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-') }
|
it { is_expected.to start_with('glrt-') }
|
||||||
end
|
end
|
||||||
|
|
@ -1993,20 +1994,4 @@ RSpec.describe Ci::Runner, feature_category: :runner do
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
||||||
let_it_be(:creator) { create(:user) }
|
let_it_be(:creator) { create(:user) }
|
||||||
|
|
||||||
let(:created_at) { Time.current }
|
let(:created_at) { Time.current }
|
||||||
let(:token_prefix) { '' }
|
let(:token_prefix) { registration_type == :authenticated_user ? 'glrt-' : '' }
|
||||||
|
let(:registration_type) {}
|
||||||
let(:query) do
|
let(:query) do
|
||||||
%(
|
%(
|
||||||
query {
|
query {
|
||||||
|
|
@ -539,7 +540,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
||||||
|
|
||||||
let(:runner) do
|
let(:runner) do
|
||||||
create(:ci_runner, :group,
|
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
|
end
|
||||||
|
|
||||||
before_all do
|
before_all do
|
||||||
|
|
@ -570,7 +572,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
||||||
let(:user) { creator }
|
let(:user) { creator }
|
||||||
|
|
||||||
context 'with runner created in UI' do
|
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
|
context 'with runner created in last 3 hours' do
|
||||||
let(:created_at) { (3.hours - 1.second).ago }
|
let(:created_at) { (3.hours - 1.second).ago }
|
||||||
|
|
@ -600,7 +602,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with runner registered from command line' do
|
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
|
context 'with runner created in last 3 hours' do
|
||||||
let(:created_at) { (3.hours - 1.second).ago }
|
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) }
|
let(:user) { create(:admin) }
|
||||||
|
|
||||||
context 'with runner created in UI' do
|
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'
|
it_behaves_like 'a protected ephemeral_authentication_token'
|
||||||
end
|
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)
|
expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url)
|
||||||
|
|
||||||
subject
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:redirect)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ RSpec.describe AutoMergeService do
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the head piipeline succeeded' do
|
context 'when the head pipeline succeeded' do
|
||||||
let(:pipeline_status) { :success }
|
let(:pipeline_status) { :success }
|
||||||
|
|
||||||
it 'returns failed' do
|
it 'returns failed' do
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,10 @@ module Spec
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_branch(branch_name)
|
def select_branch(branch_name)
|
||||||
ref_selector = '.ref-selector'
|
|
||||||
find(ref_selector).click
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
page.within(ref_selector) do
|
click_button branch_name
|
||||||
fill_in _('Search by Git revision'), with: branch_name
|
send_keys branch_name
|
||||||
wait_for_requests
|
|
||||||
find('li', text: branch_name, match: :prefer_exact).click
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,18 @@ module Spec
|
||||||
module Helpers
|
module Helpers
|
||||||
module Features
|
module Features
|
||||||
module ReleasesHelpers
|
module ReleasesHelpers
|
||||||
|
include ListboxHelpers
|
||||||
|
|
||||||
def select_new_tag_name(tag_name)
|
def select_new_tag_name(tag_name)
|
||||||
page.within '[data-testid="tag-name-field"]' do
|
page.within '[data-testid="tag-name-field"]' do
|
||||||
find('button').click
|
find('button').click
|
||||||
|
|
||||||
wait_for_all_requests
|
wait_for_all_requests
|
||||||
|
|
||||||
find('input[aria-label="Search or create tag"]').set(tag_name)
|
find('input[aria-label="Search or create tag"]').set(tag_name)
|
||||||
|
|
||||||
wait_for_all_requests
|
wait_for_all_requests
|
||||||
|
|
||||||
click_button("Create tag #{tag_name}")
|
click_button("Create tag #{tag_name}")
|
||||||
|
click_button tag_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -39,7 +40,7 @@ module Spec
|
||||||
|
|
||||||
wait_for_all_requests
|
wait_for_all_requests
|
||||||
|
|
||||||
click_button(branch_name.to_s)
|
select_listbox_item(branch_name.to_s, exact_text: true)
|
||||||
end
|
end
|
||||||
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