Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
36170967f8
commit
44475bb86a
|
|
@ -124,6 +124,19 @@ export const getPreferredLocales = () => {
|
|||
const createDateTimeFormat = (formatOptions) =>
|
||||
Intl.DateTimeFormat(getPreferredLocales(), formatOptions);
|
||||
|
||||
/**
|
||||
* Creates an instance of Intl.ListFormat for the current locale.
|
||||
*
|
||||
* For example, for a list `['Motorcycle', 'Bus', 'Car']`:
|
||||
* - This returns `Motorcycle, Bus, and Car` for locale `en`
|
||||
* - This returns `Motorcycle, Bus oder Car` for locale `de`
|
||||
*
|
||||
* @param {Intl.ListFormatOptions} [formatOptions] - for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ListFormat
|
||||
* @returns {Intl.ListFormat}
|
||||
*/
|
||||
const createListFormat = (formatOptions) =>
|
||||
new Intl.ListFormat(getPreferredLocales(), formatOptions);
|
||||
|
||||
/**
|
||||
* Formats a number as a string using `toLocaleString`.
|
||||
* @param {number} value - number to be converted
|
||||
|
|
@ -149,5 +162,6 @@ export { getPluralFormIndex };
|
|||
export { pgettext as s__ };
|
||||
export { sprintf };
|
||||
export { createDateTimeFormat };
|
||||
export { createListFormat };
|
||||
export { formatNumber };
|
||||
export default locale;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
|
|||
import getOpenMrCountForBlobPath from '~/repository/queries/open_mr_count.query.graphql';
|
||||
import getOpenMrsForBlobPath from '~/repository/queries/open_mrs.query.graphql';
|
||||
import { nDaysBefore } from '~/lib/utils/datetime/date_calculation_utility';
|
||||
import { toYmd } from '~/analytics/shared/utils';
|
||||
import { formatDate } from '~/lib/utils/datetime/date_format_utility';
|
||||
import { logError } from '~/lib/logger';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import MergeRequestListItem from './merge_request_list_item.vue';
|
||||
|
|
@ -56,7 +56,7 @@ export default {
|
|||
},
|
||||
createdAfter() {
|
||||
const lookbackDate = nDaysBefore(new Date(), OPEN_MR_AGE_LIMIT_DAYS - 1, { utc: true });
|
||||
return toYmd(lookbackDate);
|
||||
return formatDate(lookbackDate, 'yyyy-mm-dd HH:MM:ss Z', true);
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.loading;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,213 @@
|
|||
<script>
|
||||
import { GlButton, GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
|
||||
import { debounce, intersectionBy, unionBy } from 'lodash';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { __, createListFormat, s__, sprintf } from '~/locale';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
|
||||
import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
|
||||
import { findLabelsWidget, formatLabelForListbox } from '../../utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlCollapsibleListbox,
|
||||
GlFormGroup,
|
||||
},
|
||||
inject: ['labelsManagePath'],
|
||||
props: {
|
||||
checkedItems: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
formLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
formLabelId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
selectedLabelsIds: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
labelsCache: [],
|
||||
searchLabels: [],
|
||||
searchStarted: false,
|
||||
searchTerm: '',
|
||||
selectedIds: this.selectedLabelsIds ?? [],
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
searchLabels: {
|
||||
query() {
|
||||
return this.isGroup ? groupLabelsQuery : projectLabelsQuery;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
searchTerm: this.searchTerm,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.searchStarted;
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace?.labels?.nodes ?? [];
|
||||
},
|
||||
error(error) {
|
||||
this.$emit(
|
||||
'error',
|
||||
s__('WorkItem|Something went wrong when fetching labels. Please try again.'),
|
||||
);
|
||||
Sentry.captureException(error);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
checkedItemsLabels() {
|
||||
const labels = this.checkedItems.flatMap((item) => findLabelsWidget(item)?.labels.nodes);
|
||||
return intersectionBy(labels, 'id');
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.searchLabels.loading;
|
||||
},
|
||||
listboxItems() {
|
||||
const allLabels = this.checkedItemsLabels.length
|
||||
? this.checkedItemsLabels.filter((label) =>
|
||||
label.title.toLowerCase().includes(this.searchTerm.toLowerCase()),
|
||||
)
|
||||
: this.searchLabels;
|
||||
|
||||
return this.selectedLabels.length
|
||||
? [
|
||||
{
|
||||
text: __('Selected'),
|
||||
options: this.selectedLabels.map(formatLabelForListbox),
|
||||
},
|
||||
{
|
||||
text: __('All'),
|
||||
textSrOnly: true,
|
||||
options: allLabels.map(formatLabelForListbox),
|
||||
},
|
||||
]
|
||||
: allLabels.map(formatLabelForListbox);
|
||||
},
|
||||
manageLabelText() {
|
||||
return this.isGroup ? __('Manage group labels') : __('Manage project labels');
|
||||
},
|
||||
selectedLabels() {
|
||||
return this.labelsCache.filter((label) => this.selectedIds.includes(label.id));
|
||||
},
|
||||
toggleText() {
|
||||
if (!this.selectedLabels.length) {
|
||||
return __('Select labels');
|
||||
}
|
||||
|
||||
const selectedLabelTitles = this.selectedLabels.map((label) => label.title);
|
||||
|
||||
return selectedLabelTitles.length > 3
|
||||
? sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
|
||||
labelsString: selectedLabelTitles.at(0),
|
||||
remainingLabelCount: selectedLabelTitles.length - 1,
|
||||
})
|
||||
: createListFormat().format(selectedLabelTitles);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
checkedItemsLabels(checkedItemsLabels) {
|
||||
this.updateLabelsCache(checkedItemsLabels);
|
||||
},
|
||||
searchLabels(searchLabels) {
|
||||
this.updateLabelsCache(searchLabels);
|
||||
},
|
||||
selectedLabelsIds(selectedLabelsIds) {
|
||||
this.selectedIds = selectedLabelsIds;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setSearchTermDebounced = debounce(this.setSearchTerm, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
methods: {
|
||||
clearSearch() {
|
||||
this.searchTerm = '';
|
||||
this.$refs.listbox.$refs.searchBox.clearInput?.();
|
||||
},
|
||||
handleSelect(items) {
|
||||
this.selectedIds = items;
|
||||
this.clearSearch();
|
||||
this.$emit('select', this.selectedIds);
|
||||
},
|
||||
handleShown() {
|
||||
this.searchTerm = '';
|
||||
this.searchStarted = true;
|
||||
},
|
||||
setSearchTerm(searchTerm) {
|
||||
this.searchTerm = searchTerm;
|
||||
},
|
||||
updateLabelsCache(labels) {
|
||||
// Need to store all labels we encounter so we can show "Selected" labels
|
||||
// even if they're not found in the apollo `searchLabels` list
|
||||
this.labelsCache = unionBy(this.labelsCache, labels, 'id');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-group :label="formLabel" :label-for="formLabelId">
|
||||
<gl-collapsible-listbox
|
||||
ref="listbox"
|
||||
block
|
||||
is-check-centered
|
||||
:items="listboxItems"
|
||||
multiple
|
||||
:no-results-text="s__('WorkItem|No matching results')"
|
||||
searchable
|
||||
:searching="isLoading"
|
||||
:selected="selectedIds"
|
||||
:toggle-id="formLabelId"
|
||||
:toggle-text="toggleText"
|
||||
@search="setSearchTermDebounced"
|
||||
@select="handleSelect"
|
||||
@shown="handleShown"
|
||||
>
|
||||
<template #list-item="{ item }">
|
||||
<div class="gl-flex gl-items-center gl-gap-3 gl-break-anywhere">
|
||||
<span
|
||||
:style="{ background: item.color }"
|
||||
class="gl-border gl-h-3 gl-w-5 gl-shrink-0 gl-rounded-base gl-border-white"
|
||||
></span>
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="gl-border-t-1 gl-border-t-dropdown !gl-p-2 gl-border-t-solid">
|
||||
<gl-button
|
||||
class="!gl-mt-2 !gl-justify-start"
|
||||
block
|
||||
category="tertiary"
|
||||
:href="labelsManagePath"
|
||||
>
|
||||
{{ manageLabelText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
</gl-collapsible-listbox>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<script>
|
||||
import { GlForm } from '@gitlab/ui';
|
||||
import WorkItemBulkEditLabels from './work_item_bulk_edit_labels.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlForm,
|
||||
WorkItemBulkEditLabels,
|
||||
},
|
||||
props: {
|
||||
checkedItems: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
addLabelIds: [],
|
||||
removeLabelIds: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleFormSubmitted() {
|
||||
this.$emit('bulk-update', {
|
||||
ids: this.checkedItems.map((item) => item.id),
|
||||
addLabelIds: this.addLabelIds,
|
||||
removeLabelIds: this.removeLabelIds,
|
||||
});
|
||||
this.addLabelIds = [];
|
||||
this.removeLabelIds = [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form id="work-item-list-bulk-edit" class="gl-p-5" @submit.prevent="handleFormSubmitted">
|
||||
<work-item-bulk-edit-labels
|
||||
:form-label="__('Add labels')"
|
||||
form-label-id="bulk-update-add-labels"
|
||||
:full-path="fullPath"
|
||||
:is-group="isGroup"
|
||||
:selected-labels-ids="addLabelIds"
|
||||
@select="addLabelIds = $event"
|
||||
/>
|
||||
<work-item-bulk-edit-labels
|
||||
:checked-items="checkedItems"
|
||||
:form-label="__('Remove labels')"
|
||||
form-label-id="bulk-update-remove-labels"
|
||||
:full-path="fullPath"
|
||||
:is-group="isGroup"
|
||||
:selected-labels-ids="removeLabelIds"
|
||||
@select="removeLabelIds = $event"
|
||||
/>
|
||||
</gl-form>
|
||||
</template>
|
||||
|
|
@ -15,15 +15,12 @@ import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
|
|||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
|
||||
import { findLabelsWidget, newWorkItemId, newWorkItemFullPath } from '../utils';
|
||||
|
||||
function formatLabelForListbox(label) {
|
||||
return {
|
||||
text: label.title || label.text,
|
||||
value: label.id || label.value,
|
||||
color: label.color,
|
||||
};
|
||||
}
|
||||
import {
|
||||
findLabelsWidget,
|
||||
formatLabelForListbox,
|
||||
newWorkItemId,
|
||||
newWorkItemFullPath,
|
||||
} from '../utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,12 @@ export const findHierarchyWidgetChildren = (workItem) =>
|
|||
export const findHierarchyWidgetAncestors = (workItem) =>
|
||||
findHierarchyWidget(workItem)?.ancestors?.nodes || [];
|
||||
|
||||
export const formatLabelForListbox = (label) => ({
|
||||
text: label.title || label.text,
|
||||
value: label.id || label.value,
|
||||
color: label.color,
|
||||
});
|
||||
|
||||
export const convertTypeEnumToName = (workItemTypeEnum) =>
|
||||
Object.keys(NAME_TO_ENUM_MAP).find((name) => NAME_TO_ENUM_MAP[name] === workItemTypeEnum);
|
||||
|
||||
|
|
|
|||
|
|
@ -250,24 +250,6 @@ class Packages::Package < ApplicationRecord
|
|||
def detailed_info?
|
||||
DETAILED_INFO_STATUSES.include?(status.to_sym)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method will block while another database transaction attempts to insert the same data.
|
||||
# After the lock is released by the other transaction, the uniqueness validation may fail
|
||||
# with record not unique validation error.
|
||||
|
||||
# Without this block the uniqueness validation wouldn't be able to detect duplicated
|
||||
# records as transactions can't see each other's changes.
|
||||
|
||||
# This is a temp advisory lock to prevent race conditions. We will switch to use database `upsert`
|
||||
# once we have a database unique index: https://gitlab.com/gitlab-org/gitlab/-/issues/424238#note_2187274213
|
||||
def prevent_concurrent_inserts
|
||||
lock_key = [self.class.table_name, project_id, name, version].join('-')
|
||||
lock_expression = "hashtext(#{connection.quote(lock_key)})"
|
||||
|
||||
connection.execute("SELECT pg_advisory_xact_lock(#{lock_expression})")
|
||||
end
|
||||
end
|
||||
|
||||
Packages::Package.prepend_mod
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
removal_milestone: "Pending"
|
||||
announcement_milestone: "17.9"
|
||||
breaking_change: true
|
||||
window: 1
|
||||
reporter: nagyv-gitlab
|
||||
stage: deploy
|
||||
issue_url: https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/issues/630
|
||||
|
|
|
|||
|
|
@ -28,24 +28,25 @@ and [as simple as possible](https://handbook.gitlab.com/handbook/communication/#
|
|||
|
||||
We provide [example diagrams and statements](#examples) to demonstrate correct usage of terms.
|
||||
|
||||
| Term | Definition | Scope | Discouraged synonyms |
|
||||
|------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|-------------------------------------------------|
|
||||
| Node | An individual server that runs GitLab either with a specific role or as a whole (for example a Rails application node). In a cloud context this can be a specific machine type. | GitLab | instance, server |
|
||||
| Site | One or a collection of nodes running a single GitLab application. A site can be single-node or multi-node. | GitLab | deployment, installation instance |
|
||||
| Single-node site | A specific configuration of GitLab that uses exactly one node. | GitLab | single-server, single-instance |
|
||||
| Multi-node site | A specific configuration of GitLab that uses more than one node. | GitLab | multi-server, multi-instance, high availability |
|
||||
| Primary site | A GitLab site whose data is being replicated by at least one secondary site. There can only be a single primary site. | Geo-specific | Geo deployment, Primary node |
|
||||
| Secondary site | A GitLab site that is configured to replicate the data of a primary site. There can be one or more secondary sites. | Geo-specific | Geo deployment, Secondary node |
|
||||
| Geo deployment | A collection of two or more GitLab sites with exactly one primary site being replicated by one or more secondary sites. | Geo-specific | |
|
||||
| Reference architecture | A [specified configuration of GitLab based on Requests per Second or user count](../reference_architectures/_index.md), possibly including multiple nodes and multiple sites. | GitLab | |
|
||||
| Promoting | Changing the role of a site from secondary to primary. | Geo-specific | |
|
||||
| Demoting | Changing the role of a site from primary to secondary. | Geo-specific | |
|
||||
| Failover | The entire process that shifts users from a primary Site to a secondary site. This includes promoting a secondary, but contains other parts as well. For example, scheduling maintenance. | Geo-specific | |
|
||||
| Replication | Also called "synchronization". The uni-directional process that updates a resource on a secondary site to match the resource on the primary site. | Geo-specific | |
|
||||
| Verification | The process of comparing the data that exist on a primary site to the data replicated to a secondary site. Used to ensure integrity of replicated data. | Geo-specific | |
|
||||
| Unified URL | A single external URL used for all Geo sites. Allows requests to be routed to either the primary Geo site or any secondary Geo sites. | Geo-specific | |
|
||||
| Geo proxying | A mechanism where secondary Geo sites transparently forward operations to the primary site, except for certain operations that can be handled locally by the secondary sites. | Geo-specific | |
|
||||
| Blob | Geo-related data type which can be replicated to cover various GitLab components. | Geo-specific | file |
|
||||
| Term | Definition | Scope | Discouraged synonyms |
|
||||
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|-------------------------------------------------|
|
||||
| Node | An individual server that runs GitLab either with a specific role or as a whole (for example a Rails application node). In a cloud context this can be a specific machine type. | GitLab | instance, server |
|
||||
| Site | One or a collection of nodes running a single GitLab application. A site can be single-node or multi-node. | GitLab | deployment, installation instance |
|
||||
| Single-node site | A specific configuration of GitLab that uses exactly one node. | GitLab | single-server, single-instance |
|
||||
| Multi-node site | A specific configuration of GitLab that uses more than one node. | GitLab | multi-server, multi-instance, high availability |
|
||||
| Primary site | A GitLab site whose data is being replicated by at least one secondary site. There can only be a single primary site. | Geo-specific | Geo deployment, Primary node |
|
||||
| Secondary site | A GitLab site that is configured to replicate the data of a primary site. There can be one or more secondary sites. | Geo-specific | Geo deployment, Secondary node |
|
||||
| Geo deployment | A collection of two or more GitLab sites with exactly one primary site being replicated by one or more secondary sites. | Geo-specific | |
|
||||
| Reference architecture | A [specified configuration of GitLab based on Requests per Second or user count](../reference_architectures/_index.md), possibly including multiple nodes and multiple sites. | GitLab | |
|
||||
| Promoting | Changing the role of a site from secondary to primary. | Geo-specific | |
|
||||
| Demoting | Changing the role of a site from primary to secondary. | Geo-specific | |
|
||||
| Failover | The entire process that shifts users from a primary Site to a secondary site. This includes promoting a secondary, but contains other parts as well. For example, scheduling maintenance. | Geo-specific | |
|
||||
| Replication | Also called "synchronization". The uni-directional process that updates a resource on a secondary site to match the resource on the primary site. | Geo-specific | |
|
||||
| Replication slot | The PostgreSQL replication feature that ensures a persistent connection point with the database, and tracks which WAL segments are still needed by standby servers. It can be helpful to name replication slots to match the `geo_node_name` of a site, but this is not required. | PostgreSQL | |
|
||||
| Verification | The process of comparing the data that exist on a primary site to the data replicated to a secondary site. Used to ensure integrity of replicated data. | Geo-specific | |
|
||||
| Unified URL | A single external URL used for all Geo sites. Allows requests to be routed to either the primary Geo site or any secondary Geo sites. | Geo-specific | |
|
||||
| Geo proxying | A mechanism where secondary Geo sites transparently forward operations to the primary site, except for certain operations that can be handled locally by the secondary sites. | Geo-specific | |
|
||||
| Blob | Geo-related data type which can be replicated to cover various GitLab components. | Geo-specific | file |
|
||||
|
||||
## Replicator terms
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ are [paginated](rest/_index.md#pagination).
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/project_repository_storage_moves"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/project_repository_storage_moves"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -93,7 +94,8 @@ Parameters:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/repository_storage_moves"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/1/repository_storage_moves"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -134,7 +136,8 @@ Parameters:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/project_repository_storage_moves/1"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/project_repository_storage_moves/1"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -174,7 +177,8 @@ Parameters:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/repository_storage_moves/1"
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/1/repository_storage_moves/1"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -214,9 +218,11 @@ Parameters:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" \
|
||||
--data '{"destination_storage_name":"storage2"}' \
|
||||
"https://gitlab.example.com/api/v4/projects/1/repository_storage_moves"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"destination_storage_name":"storage2"}' \
|
||||
--url "https://gitlab.example.com/api/v4/projects/1/repository_storage_moves"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -260,9 +266,11 @@ Parameters:
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" \
|
||||
--data '{"source_storage_name":"default"}' \
|
||||
"https://gitlab.example.com/api/v4/project_repository_storage_moves"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"source_storage_name":"default"}' \
|
||||
--url "https://gitlab.example.com/api/v4/project_repository_storage_moves"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
|
|
@ -110,3 +110,17 @@ are running a supported version of PyCharm:
|
|||
1. For **Compatibility**, select `PyCharm Community` or `PyCharm Professional`.
|
||||
1. For **Channels**, select your desired stability level for the GitLab plugin.
|
||||
1. For your version of PyCharm, select **Download** to download the correct GitLab plugin version, and install it.
|
||||
|
||||
## JCEF Errors
|
||||
|
||||
If you experience issues with GitLab Duo Chat related to JCEF (Java Chromium Embedded Framework), you can try these steps:
|
||||
|
||||
1. On the top bar, go to **Help > Find Action** and search for `Registry`.
|
||||
1. Find or search for `ide.browser.jcef.sandbox.enable`.
|
||||
1. Clear the checkbox to disable this setting.
|
||||
1. Close the Registry dialog.
|
||||
1. Restart your IDE.
|
||||
1. On the top bar, go to **Help > Find Action** and search for `Choose Boot Java Runtime for the IDE`.
|
||||
1. Select the boot java runtime version that's the same as your current IDE version, but with JCEF bundled:
|
||||

|
||||
1. Restart your IDE.
|
||||
|
|
|
|||
|
|
@ -333,6 +333,13 @@ Additionally, restricted access might block the standard non-overage flows:
|
|||
|
||||
## User cap for groups
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Premium, Ultimate
|
||||
- Offering: GitLab.com
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Enabled on GitLab.com](https://gitlab.com/groups/gitlab-org/-/epics/9263) in GitLab 16.3.
|
||||
|
|
|
|||
|
|
@ -566,6 +566,13 @@ Fruits
|
|||
: orange
|
||||
```
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
The rich text editor does not support inserting new description lists. To insert a new description list, use the
|
||||
plain text editor. For more information, see [issue 535956](https://gitlab.com/gitlab-org/gitlab/-/issues/535956).
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
### Task lists
|
||||
|
||||
{{< history >}}
|
||||
|
|
@ -1584,6 +1591,13 @@ $$
|
|||
|
||||

|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
The rich text editor does not support inserting new math blocks. To insert a new math block, use the
|
||||
plain text editor. For more information, see [issue 366527](https://gitlab.com/gitlab-org/gitlab/-/issues/366527).
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
## Table of contents
|
||||
|
||||
A table of contents is an unordered list that links to subheadings in the document.
|
||||
|
|
@ -2063,6 +2077,13 @@ These are used to force the Vale ReferenceLinks check to skip these examples.
|
|||
|
||||
[^footnote-42]: This text is another footnote.
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
The rich text editor does not support inserting new footnotes. To insert a new footnote, use the
|
||||
plain text editor. For more information, see [issue 365265](https://gitlab.com/gitlab-org/gitlab/-/issues/365265).
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
## Inline HTML
|
||||
|
||||
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#inline-html).
|
||||
|
|
|
|||
|
|
@ -48,9 +48,34 @@ container_scanning:
|
|||
rules:
|
||||
- if: $CONTAINER_SCANNING_DISABLED == 'true' || $CONTAINER_SCANNING_DISABLED == '1'
|
||||
when: never
|
||||
|
||||
# The following 3 blocks of rules define whether the job runs in a an *MR pipeline* or a *branch pipeline*
|
||||
# when an MR exists. If the job has additional rules to observe they should be added in the blocks 1 and 3
|
||||
# to cover both the *MR pipeline* and the *branch pipeline* workflows.
|
||||
|
||||
# 1. Run the job in an *MR pipeline* if MR pipelines for AST are enabled and there's an open merge request.
|
||||
## When FIPS mode is enabled, use the FIPS compatible image
|
||||
- if: $AST_ENABLE_MR_PIPELINES == "true" &&
|
||||
$CI_PIPELINE_SOURCE == "merge_request_event" &&
|
||||
$CI_GITLAB_FIPS_MODE == "true" &&
|
||||
$CS_ANALYZER_IMAGE !~ /-(fips|ubi)\z/
|
||||
variables:
|
||||
CS_IMAGE_SUFFIX: -fips
|
||||
## When FIPS mode is not enabled, use the regular image
|
||||
- if: $AST_ENABLE_MR_PIPELINES == "true" &&
|
||||
$CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
# 2. Don't run the job in a *branch pipeline* if *MR pipelines* for AST are enabled and there's an open merge request.
|
||||
- if: $AST_ENABLE_MR_PIPELINES == "true" &&
|
||||
$CI_OPEN_MERGE_REQUESTS
|
||||
when: never
|
||||
|
||||
# 3. Finally, run the job in a *branch pipeline* (When MR pipelines are disabled for AST, or it is enabled but no open MRs exist for the branch).
|
||||
## When FIPS mode is enabled, use the FIPS compatible image
|
||||
- if: $CI_COMMIT_BRANCH &&
|
||||
$CI_GITLAB_FIPS_MODE == "true" &&
|
||||
$CS_ANALYZER_IMAGE !~ /-(fips|ubi)\z/
|
||||
variables:
|
||||
CS_IMAGE_SUFFIX: -fips
|
||||
## When FIPS mode is not enabled, use the regular image
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@
|
|||
# List of available variables: https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-variables
|
||||
|
||||
variables:
|
||||
# Setting this variable affects all Security templates
|
||||
# (SAST, Dependency Scanning, ...)
|
||||
AST_ENABLE_MR_PIPELINES: "true"
|
||||
#
|
||||
CS_ANALYZER_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/security-products/container-scanning:7"
|
||||
CS_SCHEMA_MODEL: 15
|
||||
|
||||
|
|
@ -54,24 +58,35 @@ variables:
|
|||
- if: $CONTAINER_SCANNING_DISABLED == 'true' || $CONTAINER_SCANNING_DISABLED == '1'
|
||||
when: never
|
||||
|
||||
# Add the job to merge request pipelines if there's an open merge request.
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
|
||||
# The following 3 blocks of rules define whether the job runs in a an *MR pipeline* or a *branch pipeline*
|
||||
# when an MR exists. If the job has additional rules to observe they should be added in the blocks 1 and 3
|
||||
# to cover both the *MR pipeline* and the *branch pipeline* workflows.
|
||||
|
||||
# 1. Run the job in an *MR pipeline* if MR pipelines for AST are enabled and there's an open merge request.
|
||||
## When FIPS mode is enabled, use the FIPS compatible image
|
||||
- if: $AST_ENABLE_MR_PIPELINES == "true" &&
|
||||
$CI_PIPELINE_SOURCE == "merge_request_event" &&
|
||||
$CI_GITLAB_FIPS_MODE == "true" &&
|
||||
$CS_ANALYZER_IMAGE !~ /-(fips|ubi)\z/
|
||||
variables:
|
||||
CS_IMAGE_SUFFIX: -fips
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
## When FIPS mode is not enabled, use the regular image
|
||||
- if: $AST_ENABLE_MR_PIPELINES == "true" &&
|
||||
$CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
# Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
|
||||
- if: $CI_OPEN_MERGE_REQUESTS
|
||||
# 2. Don't run the job in a *branch pipeline* if *MR pipelines* for AST are enabled and there's an open merge request.
|
||||
- if: $AST_ENABLE_MR_PIPELINES == "true" &&
|
||||
$CI_OPEN_MERGE_REQUESTS
|
||||
when: never
|
||||
|
||||
# Add the job to branch pipelines.
|
||||
# 3. Finally, run the job in a *branch pipeline* (When MR pipelines are disabled for AST, or it is enabled but no open MRs exist for the branch).
|
||||
## When FIPS mode is enabled, use the FIPS compatible image
|
||||
- if: $CI_COMMIT_BRANCH &&
|
||||
$CI_GITLAB_FIPS_MODE == "true" &&
|
||||
$CS_ANALYZER_IMAGE !~ /-(fips|ubi)\z/
|
||||
variables:
|
||||
CS_IMAGE_SUFFIX: -fips
|
||||
## When FIPS mode is not enabled, use the regular image
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
container_scanning:
|
||||
|
|
|
|||
|
|
@ -50204,6 +50204,9 @@ msgstr ""
|
|||
msgid "Remove iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove labels"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove license"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ describe('OpenMrBadge', () => {
|
|||
blobPath: 'path/to/file.js',
|
||||
};
|
||||
|
||||
useFakeDate('2020-04-15 09:00:00 GMT+2');
|
||||
|
||||
function createComponent(
|
||||
props = {},
|
||||
mockResolver = openMRQueryResult,
|
||||
|
|
@ -81,8 +83,6 @@ describe('OpenMrBadge', () => {
|
|||
});
|
||||
|
||||
describe('computed properties', () => {
|
||||
useFakeDate();
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({});
|
||||
});
|
||||
|
|
@ -90,7 +90,7 @@ describe('OpenMrBadge', () => {
|
|||
it('computes queryVariables correctly', () => {
|
||||
expect(openMrsCountQueryHandler).toHaveBeenCalledWith({
|
||||
blobPath: 'path/to/file.js',
|
||||
createdAfter: '2020-06-07',
|
||||
createdAfter: '2020-03-17 07:00:00 UTC',
|
||||
projectPath: 'group/project',
|
||||
targetBranch: ['main'],
|
||||
});
|
||||
|
|
@ -136,7 +136,7 @@ describe('OpenMrBadge', () => {
|
|||
|
||||
expect(openMrsQueryHandler).toHaveBeenCalledWith({
|
||||
blobPath: 'path/to/file.js',
|
||||
createdAfter: '2020-06-07',
|
||||
createdAfter: '2020-03-17 07:00:00 UTC',
|
||||
projectPath: 'group/project',
|
||||
targetBranch: ['main'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,228 @@
|
|||
import { GlButton, GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { projectLabelsResponse } from 'jest/work_items/mock_data';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
|
||||
import WorkItemBulkEditLabels from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels.vue';
|
||||
import { WIDGET_TYPE_LABELS } from '~/work_items/constants';
|
||||
|
||||
jest.mock('~/sentry/sentry_browser_wrapper');
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('WorkItemBulkEditLabels component', () => {
|
||||
let wrapper;
|
||||
|
||||
const labelsManagePath = '/labels/manage/path';
|
||||
|
||||
const labels = cloneDeep(projectLabelsResponse);
|
||||
labels.data.workspace.labels.nodes.push({
|
||||
__typename: 'Label',
|
||||
id: 'gid://gitlab/Label/4',
|
||||
title: 'Label 4',
|
||||
description: 'Label 4 description',
|
||||
color: '#fff',
|
||||
textColor: '#000',
|
||||
});
|
||||
const projectLabelsQueryHandler = jest.fn().mockResolvedValue(labels);
|
||||
|
||||
const createComponent = ({ props = {}, searchQueryHandler = projectLabelsQueryHandler } = {}) => {
|
||||
wrapper = shallowMount(WorkItemBulkEditLabels, {
|
||||
apolloProvider: createMockApollo([[projectLabelsQuery, searchQueryHandler]]),
|
||||
propsData: {
|
||||
formLabel: 'Labels',
|
||||
formLabelId: 'labels-id',
|
||||
fullPath: 'group/project',
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
labelsManagePath,
|
||||
},
|
||||
stubs: {
|
||||
GlCollapsibleListbox,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
|
||||
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
const findManageLabelsButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
it('renders the form group', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findFormGroup().attributes('label')).toBe('Labels');
|
||||
});
|
||||
|
||||
it('connects the form group and the listbox', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findFormGroup().attributes('label-for')).toBe(findListbox().props('toggleId'));
|
||||
});
|
||||
|
||||
it('renders the manage labels button with correct text for project', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findManageLabelsButton().text()).toBe('Manage project labels');
|
||||
expect(findManageLabelsButton().attributes('href')).toBe(labelsManagePath);
|
||||
});
|
||||
|
||||
describe('search labels query', () => {
|
||||
it('is not called before dropdown is shown', () => {
|
||||
createComponent();
|
||||
|
||||
expect(projectLabelsQueryHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('is called when dropdown is shown', async () => {
|
||||
createComponent();
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
await nextTick();
|
||||
|
||||
expect(projectLabelsQueryHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits an error when there is an error in the call', async () => {
|
||||
createComponent({ searchQueryHandler: jest.fn().mockRejectedValue(new Error('error!')) });
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
['Something went wrong when fetching labels. Please try again.'],
|
||||
]);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('error!'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('listbox items', () => {
|
||||
describe('with no selected labels', () => {
|
||||
it('renders all labels', async () => {
|
||||
createComponent();
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('items')).toEqual([
|
||||
expect.objectContaining({ text: 'Label 1' }),
|
||||
expect.objectContaining({ text: 'Label::2' }),
|
||||
expect.objectContaining({ text: 'Label 3' }),
|
||||
expect.objectContaining({ text: 'Label 4' }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with selected labels', () => {
|
||||
it('renders a "Selected" group and an "All" group', async () => {
|
||||
createComponent();
|
||||
|
||||
findListbox().vm.$emit('select', ['gid://gitlab/Label/2']);
|
||||
findListbox().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('items')).toEqual([
|
||||
{
|
||||
text: 'Selected',
|
||||
options: [expect.objectContaining({ text: 'Label::2' })],
|
||||
},
|
||||
{
|
||||
text: 'All',
|
||||
textSrOnly: true,
|
||||
options: [
|
||||
expect.objectContaining({ text: 'Label 1' }),
|
||||
expect.objectContaining({ text: 'Label::2' }),
|
||||
expect.objectContaining({ text: 'Label 3' }),
|
||||
expect.objectContaining({ text: 'Label 4' }),
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with checked items', () => {
|
||||
it('only renders labels from the checked items', async () => {
|
||||
const checkedItems = [
|
||||
{
|
||||
widgets: [
|
||||
{
|
||||
type: WIDGET_TYPE_LABELS,
|
||||
labels: { nodes: [{ id: 'gid://gitlab/Label/1', title: 'Label 1' }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
widgets: [
|
||||
{
|
||||
type: WIDGET_TYPE_LABELS,
|
||||
labels: {
|
||||
nodes: [
|
||||
{ id: 'gid://gitlab/Label/1', title: 'Label 1' },
|
||||
{ id: 'gid://gitlab/Label/3', title: 'Label 3' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
createComponent({ props: { checkedItems } });
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('items')).toEqual([
|
||||
expect.objectContaining({ text: 'Label 1' }),
|
||||
expect.objectContaining({ text: 'Label 3' }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listbox text', () => {
|
||||
describe('with no selected labels', () => {
|
||||
it('renders "Select labels"', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findListbox().props('toggleText')).toBe('Select labels');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with fewer than 4 selected labels', () => {
|
||||
it('renders all label titles', async () => {
|
||||
createComponent({
|
||||
props: { selectedLabelsIds: ['gid://gitlab/Label/2', 'gid://gitlab/Label/3'] },
|
||||
});
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('toggleText')).toBe('Label::2 and Label 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with more than 3 selected labels', () => {
|
||||
it('renders first label title followed by the count', async () => {
|
||||
createComponent({
|
||||
props: {
|
||||
selectedLabelsIds: [
|
||||
'gid://gitlab/Label/1',
|
||||
'gid://gitlab/Label/2',
|
||||
'gid://gitlab/Label/3',
|
||||
'gid://gitlab/Label/4',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
findListbox().vm.$emit('shown');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('toggleText')).toBe('Label 1, and 3 more');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { GlForm } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import WorkItemBulkEditLabels from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels.vue';
|
||||
import WorkItemBulkEditSidebar from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue';
|
||||
|
||||
describe('WorkItemBulkEditSidebar component', () => {
|
||||
let wrapper;
|
||||
|
||||
const checkedItems = [
|
||||
{ id: 'gid://gitlab/WorkItem/1', title: 'Work Item 1' },
|
||||
{ id: 'gid://gitlab/WorkItem/2', title: 'Work Item 2' },
|
||||
];
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(WorkItemBulkEditSidebar, {
|
||||
propsData: {
|
||||
checkedItems,
|
||||
fullPath: 'group/project',
|
||||
isGroup: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findForm = () => wrapper.findComponent(GlForm);
|
||||
const findAddLabelsComponent = () => wrapper.findAllComponents(WorkItemBulkEditLabels).at(0);
|
||||
const findRemoveLabelsComponent = () => wrapper.findAllComponents(WorkItemBulkEditLabels).at(1);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('form', () => {
|
||||
it('renders', () => {
|
||||
expect(findForm().attributes('id')).toBe('work-item-list-bulk-edit');
|
||||
});
|
||||
|
||||
it('emits "bulk-update" event when submitted', () => {
|
||||
const addLabelIds = ['gid://gitlab/Label/1'];
|
||||
const removeLabelIds = ['gid://gitlab/Label/2'];
|
||||
|
||||
findAddLabelsComponent().vm.$emit('select', addLabelIds);
|
||||
findRemoveLabelsComponent().vm.$emit('select', removeLabelIds);
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
|
||||
expect(wrapper.emitted('bulk-update')).toEqual([
|
||||
[
|
||||
{
|
||||
ids: checkedItems.map((item) => item.id),
|
||||
addLabelIds,
|
||||
removeLabelIds,
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(findAddLabelsComponent().props('selectedLabelsIds')).toEqual([]);
|
||||
expect(findRemoveLabelsComponent().props('selectedLabelsIds')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"Add labels" component', () => {
|
||||
it('renders', () => {
|
||||
expect(findAddLabelsComponent().props()).toMatchObject({
|
||||
formLabel: 'Add labels',
|
||||
formLabelId: 'bulk-update-add-labels',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates labels to add when "Add labels" component emits "select" event', async () => {
|
||||
const labelIds = ['gid://gitlab/Label/1', 'gid://gitlab/Label/2'];
|
||||
|
||||
findAddLabelsComponent().vm.$emit('select', labelIds);
|
||||
await nextTick();
|
||||
|
||||
expect(findAddLabelsComponent().props('selectedLabelsIds')).toEqual(labelIds);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"Remove labels" component', () => {
|
||||
it('renders', () => {
|
||||
expect(findRemoveLabelsComponent().props()).toMatchObject({
|
||||
formLabel: 'Remove labels',
|
||||
formLabelId: 'bulk-update-remove-labels',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates labels to remove when "Remove labels" component emits "select" event', async () => {
|
||||
const labelIds = ['gid://gitlab/Label/1', 'gid://gitlab/Label/2'];
|
||||
|
||||
findRemoveLabelsComponent().vm.$emit('select', labelIds);
|
||||
await nextTick();
|
||||
|
||||
expect(findRemoveLabelsComponent().props('selectedLabelsIds')).toEqual(labelIds);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
# These shared_contexts and shared_examples are used to test the # CI/CD templates
|
||||
# powering the Application Security Testing (AST) features.
|
||||
# There is a lot of repitition across these templates and the setup for these
|
||||
# specs is expensive.
|
||||
#
|
||||
# Usually, each template will have its behavior tested in these 3 different pipelines types:
|
||||
# - default branch pipeline
|
||||
# - feature branch pipeline
|
||||
# - MR pipeline
|
||||
#
|
||||
# Additionally, some templates have CI jobs using rules:exists which involves setting up
|
||||
# a project with a repository that contains specific files. To improve speed and
|
||||
# efficiency, the setup steps are extracted into shared_context that use let_it_be and
|
||||
# before(:context). This ensures to create the project only once per scenario.
|
||||
# Though these contexts assume a particular usage which reduces flexibility.
|
||||
# Please check existing specs for examples.
|
||||
|
||||
RSpec.shared_context 'with CI variables' do |variables|
|
||||
before do
|
||||
variables.each do |(key, value)|
|
||||
create(:ci_variable, project: project, key: key, value: value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_context 'with default branch pipeline setup' do
|
||||
let_it_be(:pipeline_branch) { default_branch }
|
||||
let_it_be(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) }
|
||||
end
|
||||
|
||||
RSpec.shared_context 'with feature branch pipeline setup' do
|
||||
let_it_be(:pipeline_branch) { feature_branch }
|
||||
let_it_be(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) }
|
||||
|
||||
before(:context) do
|
||||
project.repository.create_file(
|
||||
project.creator,
|
||||
'branch.patch',
|
||||
'',
|
||||
message: "Add file to feature branch",
|
||||
branch_name: pipeline_branch,
|
||||
# Ensure new branch includes expected project files from default branch.
|
||||
start_branch_name: default_branch)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_context 'with MR pipeline setup' do
|
||||
let_it_be(:pipeline_branch) { 'mr_branch' }
|
||||
let_it_be(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
|
||||
|
||||
let(:pipeline) { service.execute(merge_request).payload }
|
||||
|
||||
let_it_be(:merge_request) do
|
||||
# Ensure MR has at least one commit otherwise MR pipeline won't be triggered.
|
||||
# This also seems to be required to happen before the MR creation.
|
||||
project.repository.create_file(
|
||||
project.creator,
|
||||
'MR.patch',
|
||||
'',
|
||||
message: "Add patch to MR branch",
|
||||
branch_name: pipeline_branch,
|
||||
# Ensure new branch includes expected project files from default branch.
|
||||
start_branch_name: default_branch)
|
||||
|
||||
create(:merge_request,
|
||||
source_project: project,
|
||||
source_branch: pipeline_branch,
|
||||
target_project: project,
|
||||
target_branch: default_branch)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'has expected jobs' do |jobs|
|
||||
it 'includes jobs', if: jobs.any? do
|
||||
expect(pipeline.builds.pluck(:name)).to match_array(jobs)
|
||||
# TODO: Failing for DAST related templates with error:
|
||||
# "Insufficient permissions for dast_configuration keyword"
|
||||
# expect(pipeline.errors.full_messages).to be_empty unless ignore_errors
|
||||
end
|
||||
|
||||
it 'includes no jobs', if: jobs.empty? do
|
||||
expect(pipeline.builds.pluck(:name)).to be_empty
|
||||
expect(pipeline.errors.full_messages).to match_array(
|
||||
[sanitize_message(Ci::Pipeline.rules_failure_message)])
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'has FIPS compatible jobs' do |variable, jobs|
|
||||
context 'when CI_GITLAB_FIPS_MODE=false', fips_mode: false do
|
||||
jobs.each do |job|
|
||||
it "sets #{variable} to '' for job #{job}" do
|
||||
build = pipeline.builds.find_by(name: job)
|
||||
expect(String(build.variables.to_hash[variable])).to eql('')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CI_GITLAB_FIPS_MODE=true', :fips_mode do
|
||||
jobs.each do |job|
|
||||
it "sets #{variable} to '-fips' for job #{job}" do
|
||||
build = pipeline.builds.find_by(name: job)
|
||||
expect(String(build.variables.to_hash[variable])).to eql('-fips')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'has jobs that can be disabled' do |key, disabled_values, jobs|
|
||||
disabled_values.each do |disabled_value|
|
||||
context "when #{key} is set to '#{disabled_value}'" do
|
||||
before do
|
||||
create(:ci_variable, project: project, key: key, value: disabled_value)
|
||||
end
|
||||
|
||||
include_examples 'has expected jobs', []
|
||||
end
|
||||
end
|
||||
|
||||
# This ensures we don't accidentally disable jobs when user sets the variable to 'false'.
|
||||
context "when #{key} is set to 'false'" do
|
||||
before do
|
||||
create(:ci_variable, project: project, key: key, value: 'false')
|
||||
end
|
||||
|
||||
include_examples 'has expected jobs', jobs
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue