diff --git a/app/assets/javascripts/content_editor/components/wrappers/image.vue b/app/assets/javascripts/content_editor/components/wrappers/image.vue
index a8d3ba4af1f..1d8e7cef8dc 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/image.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/image.vue
@@ -25,10 +25,17 @@ export default {
required: false,
default: false,
},
+ updateAttributes: {
+ type: Function,
+ required: true,
+ default: () => {},
+ },
},
data() {
return {
dragData: {},
+ width: this.node.attrs.width,
+ height: this.node.attrs.height,
};
},
computed: {
@@ -51,8 +58,8 @@ export default {
handle,
startX: event.screenX,
startY: event.screenY,
- width: this.$refs.image.width,
- height: this.$refs.image.height,
+ width: Number(this.width) || this.$refs.image.width,
+ height: Number(this.height) || this.$refs.image.height,
};
},
onDrag(event) {
@@ -63,8 +70,8 @@ export default {
const newWidth = handle.includes('w') ? width - deltaX : width + deltaX;
const newHeight = (height / width) * newWidth;
- this.$refs.image.setAttribute('width', newWidth);
- this.$refs.image.setAttribute('height', newHeight);
+ this.width = Math.max(newWidth, 0);
+ this.height = Math.max(newHeight, 0);
},
onDragEnd() {
const { handle } = this.dragData;
@@ -72,15 +79,12 @@ export default {
this.dragData = {};
- this.editor
- .chain()
- .focus()
- .updateAttributes(this.node.type.name, {
- width: this.$refs.image.width,
- height: this.$refs.image.height,
- })
- .setNodeSelection(this.getPos())
- .run();
+ const { width, height } = this.$refs.image;
+
+ this.width = width;
+ this.height = height;
+
+ this.updateAttributes({ width, height });
},
},
resizeHandles: ['ne', 'nw', 'se', 'sw'],
@@ -108,8 +112,8 @@ export default {
:src="node.attrs.src"
:alt="node.attrs.alt"
:title="node.attrs.title"
- :width="node.attrs.width || 'auto'"
- :height="node.attrs.height || 'auto'"
+ :width="width || 'auto'"
+ :height="height || 'auto'"
:class="{ 'ProseMirror-selectednode': selected }"
/>
diff --git a/app/assets/javascripts/lib/utils/chart_utils.js b/app/assets/javascripts/lib/utils/chart_utils.js
index 520d7f627f6..7da3bab0a4b 100644
--- a/app/assets/javascripts/lib/utils/chart_utils.js
+++ b/app/assets/javascripts/lib/utils/chart_utils.js
@@ -1,6 +1,3 @@
-import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
-import { __ } from '~/locale';
-
const commonTooltips = () => ({
mode: 'x',
intersect: false,
@@ -101,38 +98,3 @@ export const firstAndLastY = (data) => {
return [firstY, lastY];
};
-
-const toolboxIconSvgPath = async (name) => {
- return `path://${await getSvgIconPathContent(name)}`;
-};
-
-export const getToolboxOptions = async () => {
- const promises = ['marquee-selection', 'redo', 'repeat', 'download'].map(toolboxIconSvgPath);
-
- try {
- const [marqueeSelectionPath, redoPath, repeatPath, downloadPath] = await Promise.all(promises);
-
- return {
- toolbox: {
- feature: {
- dataZoom: {
- icon: { zoom: marqueeSelectionPath, back: redoPath },
- },
- restore: {
- icon: repeatPath,
- },
- saveAsImage: {
- icon: downloadPath,
- },
- },
- },
- };
- } catch (e) {
- if (process.env.NODE_ENV !== 'production') {
- // eslint-disable-next-line no-console
- console.warn(__('SVG could not be rendered correctly: '), e);
- }
-
- return {};
- }
-};
diff --git a/app/assets/javascripts/lib/utils/icon_utils.js b/app/assets/javascripts/lib/utils/icon_utils.js
deleted file mode 100644
index 58274092cf8..00000000000
--- a/app/assets/javascripts/lib/utils/icon_utils.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { memoize } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
-
-/**
- * Resolves to a DOM that contains GitLab icons
- * in svg format. Memoized to avoid duplicate requests
- */
-const getSvgDom = memoize(() =>
- axios
- .get(gon.sprite_icons)
- .then(({ data: svgs }) => new DOMParser().parseFromString(svgs, 'text/xml'))
- .catch((e) => {
- getSvgDom.cache.clear();
-
- throw e;
- }),
-);
-
-/**
- * Clears the memoized SVG content.
- *
- * You probably don't need to invoke this function unless
- * sprite_icons are updated.
- */
-export const clearSvgIconPathContentCache = () => {
- getSvgDom.cache.clear();
-};
-
-/**
- * Retrieve SVG icon path content from gitlab/svg sprite icons.
- *
- * Content loaded is cached.
- *
- * @param {String} name - Icon name
- * @returns A promise that resolves to the svg path
- */
-export const getSvgIconPathContent = (name) =>
- getSvgDom()
- .then((doc) => {
- return doc.querySelector(`#${name} path`).getAttribute('d');
- })
- .catch(() => null);
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
index 8a3c5e68c3d..6d57b584fd6 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
@@ -1,7 +1,7 @@
-
- {{ $options.i18n.DEPENDENCY_PROXY_HEADER }}
- {{ $options.i18n.DEPENDENCY_PROXY_DESCRIPTION }}
-
-
-
-
-
-
-
- {{
- content
- }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {{
+ content
+ }}
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue
index 22528ddb026..5936f9557a1 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue
@@ -15,7 +15,7 @@ import updateNamespacePackageSettings from '~/packages_and_registries/settings/g
import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update';
import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import ForwardingSettings from '~/packages_and_registries/settings/group/components/forwarding_settings.vue';
export default {
@@ -31,7 +31,7 @@ export default {
GlButton,
GlLink,
GlSprintf,
- SettingsBlock,
+ SettingsSection,
},
mixins: [glFeatureFlagsMixin()],
inject: ['groupPath'],
@@ -165,8 +165,7 @@ export default {
-
- {{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_HEADER }}
+
{{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_DESCRIPTION }}
@@ -177,30 +176,29 @@ export default {
-
-
-
-
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
index 96df1ce8826..9abe7039d6d 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
@@ -14,7 +14,7 @@ import {
import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update';
import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import ExceptionsInput from '~/packages_and_registries/settings/group/components/exceptions_input.vue';
export default {
@@ -43,7 +43,7 @@ export default {
},
],
components: {
- SettingsBlock,
+ SettingsSection,
GlTableLite,
GlToggle,
ExceptionsInput,
@@ -166,48 +166,44 @@ export default {
-
- {{ $options.i18n.PACKAGE_SETTINGS_HEADER }}
-
-
- {{ $options.i18n.PACKAGE_SETTINGS_DESCRIPTION }}
-
-
-
-
-
-
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
index 1dd88d69d30..030637be529 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
@@ -13,11 +13,11 @@ import {
UNAVAILABLE_ADMIN_FEATURE_TEXT,
} from '~/packages_and_registries/settings/project/constants';
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
export default {
components: {
- SettingsBlock,
+ SettingsSection,
GlAlert,
GlSprintf,
GlLink,
@@ -85,8 +85,10 @@ export default {
-
- {{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}
+
@@ -96,39 +98,38 @@ export default {
-
-
-
- {{ $options.i18n.CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION }}
-
-
- {{ cleanupRulesButtonText }}
-
-
-
-
- {{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }}
-
-
- {{ content }}
-
-
-
-
-
-
-
+
+
+ {{ $options.i18n.CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION }}
+
+
+ {{ cleanupRulesButtonText }}
+
+
+
+
+ {{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }}
+
+
+
+ {{ content }}
+
+
+
+
+
+
-
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue
index d7870e121df..5ec2508e2ac 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rule_form.vue
@@ -123,77 +123,75 @@ export default {
-
+
+
+
+
+
+ {{ s__('ContainerRegistry|Add rule') }}
+ {{ __('Cancel') }}
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue
index 9a1212fec9f..7c49c17d764 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_protection_rules.vue
@@ -13,7 +13,7 @@ import {
} from '@gitlab/ui';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import protectionRulesQuery from '~/packages_and_registries/settings/project/graphql/queries/get_container_protection_rules.query.graphql';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import ContainerProtectionRuleForm from '~/packages_and_registries/settings/project/components/container_protection_rule_form.vue';
import deleteContainerProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/delete_container_protection_rule.mutation.graphql';
import updateContainerRegistryProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_registry_protection_rule.mutation.graphql';
@@ -37,7 +37,7 @@ export default {
GlLoadingIcon,
GlModal,
GlTable,
- SettingsBlock,
+ SettingsSection,
GlSprintf,
},
directives: {
@@ -144,9 +144,11 @@ export default {
methods: {
showProtectionRuleForm() {
this.protectionRuleFormVisibility = true;
+ this.$refs.containerCrud.showForm();
},
hideProtectionRuleForm() {
this.protectionRuleFormVisibility = false;
+ this.$refs.containerCrud.hideForm();
},
refetchProtectionRules() {
this.$apollo.queries.protectionRulesQueryPayload.refetch();
@@ -280,15 +282,12 @@ export default {
-
- {{ $options.i18n.settingBlockTitle }}
-
-
- {{ $options.i18n.settingBlockDescription }}
-
-
+
-
+
-
+
+
-
- {{ alertErrorMessage }}
-
+
+ {{ alertErrorMessage }}
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -397,5 +395,5 @@ export default {
{{ $options.i18n.protectionRuleDeletionConfirmModal.descriptionConsequence }}
-
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue
index 2f4bc35e5f7..1b792c901ec 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue
@@ -6,13 +6,13 @@ import {
PACKAGES_CLEANUP_POLICY_DESCRIPTION,
} from '~/packages_and_registries/settings/project/constants';
import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import PackagesCleanupPolicyForm from './packages_cleanup_policy_form.vue';
export default {
components: {
- SettingsBlock,
+ SettingsSection,
GlAlert,
GlSprintf,
PackagesCleanupPolicyForm,
@@ -47,22 +47,20 @@ export default {
-
- {{ $options.i18n.PACKAGES_CLEANUP_POLICY_TITLE }}
+
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
index f79eee8d884..b846b5e535a 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rule_form.vue
@@ -123,79 +123,77 @@ export default {
-
+
+
+
+
+
+ {{ s__('PackageRegistry|Add rule') }}
+ {{ __('Cancel') }}
+
+
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
index af4ed3eef2f..d659d51623d 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
@@ -16,7 +16,7 @@ import packagesProtectionRuleQuery from '~/packages_and_registries/settings/proj
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
import deletePackagesProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/delete_packages_protection_rule.mutation.graphql';
import updatePackagesProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_protection_rule.mutation.graphql';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import PackagesProtectionRuleForm from '~/packages_and_registries/settings/project/components/packages_protection_rule_form.vue';
import { s__, __ } from '~/locale';
@@ -27,7 +27,7 @@ const I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH = s__('PackageRegistry|Minimum access l
export default {
components: {
CrudComponent,
- SettingsBlock,
+ SettingsSection,
GlButton,
GlAlert,
GlTable,
@@ -134,9 +134,11 @@ export default {
methods: {
showProtectionRuleForm() {
this.protectionRuleFormVisibility = true;
+ this.$refs.packagesCrud.showForm();
},
hideProtectionRuleForm() {
this.protectionRuleFormVisibility = false;
+ this.$refs.packagesCrud.hideForm();
},
refetchProtectionRules() {
this.$apollo.queries.packageProtectionRulesQueryPayload.refetch();
@@ -259,15 +261,12 @@ export default {
-
- {{ $options.i18n.settingBlockTitle }}
-
-
- {{ $options.i18n.settingBlockDescription }}
-
-
+
-
+
-
+
+
+
+
-
-
-
+
+
@@ -364,5 +363,5 @@ export default {
{{ $options.i18n.protectionRuleDeletionConfirmModal.descriptionConsequence }}
-
+
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue b/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue
deleted file mode 100644
index 609d457eb74..00000000000
--- a/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
diff --git a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
index 9b78c4a2bfb..6b66b2c625f 100644
--- a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
+++ b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
@@ -4,7 +4,7 @@ import { GlForm, GlButton, GlFormGroup } from '@gitlab/ui';
import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import { isUserBusy, computedClearStatusAfterValue } from '~/set_status_modal/utils';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
@@ -18,7 +18,7 @@ export default {
GlForm,
GlFormGroup,
GlButton,
- SettingsBlock,
+ SettingsSection,
SetStatusForm,
TimezoneDropdown,
},
@@ -150,9 +150,11 @@ export default {
-
- {{ $options.i18n.setStatusTitle }}
- {{ $options.i18n.setStatusDescription }}
+
-
-
- {{ $options.i18n.setTimezoneTitle }}
- {{ $options.i18n.setTimezoneDescription }}
+
+
-
+
diff --git a/app/assets/javascripts/projects/explore/components/filtered_search_and_sort.vue b/app/assets/javascripts/projects/explore/components/filtered_search_and_sort.vue
index f88f6bfcd55..5f89b6f687f 100644
--- a/app/assets/javascripts/projects/explore/components/filtered_search_and_sort.vue
+++ b/app/assets/javascripts/projects/explore/components/filtered_search_and_sort.vue
@@ -6,6 +6,7 @@ import { RECENT_SEARCHES_STORAGE_KEY_PROJECTS } from '~/filtered_search/recent_s
import { queryToObject, objectToQuery, visitUrl } from '~/lib/utils/url_utility';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { ACCESS_LEVEL_OWNER_INTEGER } from '~/access_level/constants';
+import { InternalEvents } from '~/tracking';
import {
SORT_OPTIONS,
SORT_DIRECTION_ASC,
@@ -15,6 +16,8 @@ import {
FILTERED_SEARCH_NAMESPACE,
} from '../constants';
+const trackingMixin = InternalEvents.mixin();
+
export default {
name: 'ProjectsExploreFilteredSearchAndSort',
filteredSearch: {
@@ -25,6 +28,7 @@ export default {
components: {
FilteredSearchAndSort,
},
+ mixins: [trackingMixin],
inject: ['initialSort', 'programmingLanguages', 'starredExploreProjectsPath', 'exploreRootPath'],
computed: {
filteredSearchTokens() {
@@ -110,6 +114,10 @@ export default {
isAscending ? SORT_DIRECTION_ASC : SORT_DIRECTION_DESC
}`;
+ this.trackEvent('use_sort_projects_explore', {
+ label: sort,
+ });
+
this.visitUrlWithQueryObject({
...this.queryAsObjectWithoutPagination,
sort,
@@ -118,6 +126,10 @@ export default {
onSortByChange(sortBy) {
const sort = `${sortBy}_${this.isAscending ? SORT_DIRECTION_ASC : SORT_DIRECTION_DESC}`;
+ this.trackEvent('use_sort_projects_explore', {
+ label: sort,
+ });
+
this.visitUrlWithQueryObject({ ...this.queryAsObjectWithoutPagination, sort });
},
onFilter(filtersQuery) {
@@ -131,6 +143,27 @@ export default {
queryObject.archived = this.queryAsObject.archived;
}
+ const trackingProperty = Object.entries(filtersQuery).reduce(
+ (accumulator, [tokenType, optionValue]) => {
+ if (tokenType === FILTERED_SEARCH_TERM_KEY) {
+ return {
+ ...accumulator,
+ search: optionValue,
+ };
+ }
+
+ const token = this.filteredSearchTokens.find(({ type }) => type === tokenType);
+ const option = token.options.find(({ value }) => value === optionValue[0]);
+
+ return { ...accumulator, [token.title.toLowerCase()]: option.title };
+ },
+ {},
+ );
+
+ this.trackEvent('use_filter_bar_projects_explore', {
+ label: JSON.stringify(trackingProperty),
+ });
+
this.visitUrlWithQueryObject(queryObject);
},
},
diff --git a/app/assets/javascripts/vue_shared/components/settings/settings_section.vue b/app/assets/javascripts/vue_shared/components/settings/settings_section.vue
index 9512c9081f0..e27e3361447 100644
--- a/app/assets/javascripts/vue_shared/components/settings/settings_section.vue
+++ b/app/assets/javascripts/vue_shared/components/settings/settings_section.vue
@@ -3,7 +3,8 @@ export default {
props: {
heading: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
description: {
type: String,
diff --git a/app/assets/javascripts/work_items/components/work_item_development/work_item_development.vue b/app/assets/javascripts/work_items/components/work_item_development/work_item_development.vue
index 6f1244c0d5f..7e1c8791ef4 100644
--- a/app/assets/javascripts/work_items/components/work_item_development/work_item_development.vue
+++ b/app/assets/javascripts/work_items/components/work_item_development/work_item_development.vue
@@ -4,7 +4,7 @@ import { GlLoadingIcon, GlIcon, GlButton, GlTooltipDirective, GlModalDirective }
import { s__, __ } from '~/locale';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import { WIDGET_TYPE_DEVELOPMENT } from '~/work_items/constants';
+import { sprintfWorkItem, WIDGET_TYPE_DEVELOPMENT, STATE_OPEN } from '~/work_items/constants';
import WorkItemDevelopmentRelationshipList from './work_item_development_relationship_list.vue';
@@ -35,7 +35,7 @@ export default {
},
},
apollo: {
- workItemDevelopment: {
+ workItem: {
query: workItemByIidQuery,
variables() {
return {
@@ -44,11 +44,7 @@ export default {
};
},
update(data) {
- return (
- data.workspace?.workItem?.widgets?.find(
- (widget) => widget.type === WIDGET_TYPE_DEVELOPMENT,
- ) || {}
- );
+ return data.workspace?.workItem || {};
},
skip() {
return !this.workItemIid;
@@ -65,8 +61,20 @@ export default {
};
},
computed: {
+ workItemState() {
+ return this.workItem?.state;
+ },
+ workItemTypeName() {
+ return this.workItem?.workItemType?.name;
+ },
+ workItemDevelopment() {
+ return this.workItem?.widgets?.find(({ type }) => type === WIDGET_TYPE_DEVELOPMENT);
+ },
isLoading() {
- return this.$apollo.queries.workItemDevelopment.loading;
+ return this.$apollo.queries.workItem.loading;
+ },
+ willAutoCloseByMergeRequest() {
+ return this.workItemDevelopment?.willAutoCloseByMergeRequest;
},
linkedMergeRequests() {
return this.workItemDevelopment?.closingMergeRequests?.nodes || [];
@@ -74,6 +82,25 @@ export default {
isEmptyRelatedWorkItems() {
return !this.error && this.linkedMergeRequests.length === 0;
},
+ showAutoCloseInformation() {
+ return (
+ this.linkedMergeRequests.length > 0 && this.willAutoCloseByMergeRequest && !this.isLoading
+ );
+ },
+ openStateText() {
+ return this.linkedMergeRequests.length > 1
+ ? sprintfWorkItem(this.$options.i18n.openStateText, this.workItemTypeName)
+ : sprintfWorkItem(
+ this.$options.i18n.openStateWithOneMergeRequestText,
+ this.workItemTypeName,
+ );
+ },
+ closedStateText() {
+ return sprintfWorkItem(this.$options.i18n.closedStateText, this.workItemTypeName);
+ },
+ tooltipText() {
+ return this.workItemState === STATE_OPEN ? this.openStateText : this.closedStateText;
+ },
},
createMRModalId: 'create-merge-request-modal',
i18n: {
@@ -81,14 +108,37 @@ export default {
fetchError: s__('WorkItem|Something went wrong when fetching items. Please refresh this page.'),
createMergeRequest: __('Create merge request'),
createBranch: __('Create branch'),
+ openStateWithOneMergeRequestText: s__(
+ 'WorkItem|This %{workItemType} will be closed when the following is merged.',
+ ),
+ openStateText: s__(
+ 'WorkItem|This %{workItemType} will be closed when any of the following is merged.',
+ ),
+ closedStateText: s__(
+ 'WorkItem|The %{workItemType} was closed automatically when a branch was merged.',
+ ),
},
};
-
+
{{ $options.i18n.development }}
+
+
+
Settings > Metrics and profiling > Metrics - Grafana'
diff --git a/app/services/ci/catalog/resources/versions/build_components_service.rb b/app/services/ci/catalog/resources/versions/build_components_service.rb
new file mode 100644
index 00000000000..6e967a82003
--- /dev/null
+++ b/app/services/ci/catalog/resources/versions/build_components_service.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ module Versions
+ # This service is called from the Versions::CreateService and
+ # responsible for building components for a release version.
+ class BuildComponentsService
+ MAX_COMPONENTS = Ci::Catalog::ComponentsProject::COMPONENTS_LIMIT
+
+ def initialize(release, version)
+ @release = release
+ @version = version
+
+ @project = release.project
+ @components_project = Ci::Catalog::ComponentsProject.new(project)
+ @errors = []
+ end
+
+ def execute
+ components = build_components_from_fetched_data
+
+ if errors.empty?
+ ServiceResponse.success(payload: components)
+ else
+ ServiceResponse.error(message: errors.flatten.first(10).join(', '))
+ end
+ end
+
+ private
+
+ attr_reader :release, :version, :project, :components_project, :errors
+
+ def build_components_from_fetched_data
+ component_paths = components_project.fetch_component_paths(release.sha, limit: MAX_COMPONENTS + 1)
+
+ check_number_of_components(component_paths.size)
+ return if errors.present?
+
+ build_components_from_paths(component_paths)
+ end
+
+ def build_components_from_paths(component_paths)
+ paths_with_oids = component_paths.map { |path| [release.sha, path] }
+ blobs = project.repository.blobs_at(paths_with_oids)
+
+ blobs.map do |blob|
+ metadata = extract_metadata(blob)
+ build_catalog_resource_component(metadata)
+ end
+ rescue ::Gitlab::Config::Loader::FormatError => e
+ error(e)
+ end
+
+ def extract_metadata(blob)
+ component_name = components_project.extract_component_name(blob.path)
+
+ {
+ name: component_name,
+ spec: components_project.extract_spec(blob.data)
+ }
+ end
+
+ def check_number_of_components(size)
+ return if size <= MAX_COMPONENTS
+
+ error("Release cannot contain more than #{MAX_COMPONENTS} components")
+ end
+
+ def build_catalog_resource_component(metadata)
+ return if errors.present?
+
+ component = Ci::Catalog::Resources::Component.new(
+ name: metadata[:name],
+ project: version.project,
+ spec: metadata[:spec],
+ version: version,
+ catalog_resource: version.catalog_resource,
+ created_at: Time.current
+ )
+
+ return component if component.valid?
+
+ error("Build component error: #{component.errors.full_messages.join(', ')}")
+ end
+
+ def error(message)
+ errors << message
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/catalog/resources/versions/create_service.rb b/app/services/ci/catalog/resources/versions/create_service.rb
index 7fc99e5fd58..f189acba5ad 100644
--- a/app/services/ci/catalog/resources/versions/create_service.rb
+++ b/app/services/ci/catalog/resources/versions/create_service.rb
@@ -6,17 +6,15 @@ module Ci
module Versions
class CreateService
def initialize(release)
- @project = release.project
@release = release
+ @project = release.project
@errors = []
- @version = nil
- @components_project = Ci::Catalog::ComponentsProject.new(project)
end
def execute
- build_catalog_resource_version
- fetch_and_build_components
- publish_catalog_resource!
+ version = build_catalog_resource_version
+ build_components(version)
+ publish(version)
if errors.empty?
ServiceResponse.success
@@ -27,12 +25,12 @@ module Ci
private
- attr_reader :project, :errors, :release, :components_project
+ attr_reader :project, :errors, :release
def build_catalog_resource_version
return error('Project is not a catalog resource') unless project.catalog_resource
- @version = Ci::Catalog::Resources::Version.new(
+ Ci::Catalog::Resources::Version.new(
release: release,
catalog_resource: project.catalog_resource,
project: project,
@@ -40,62 +38,24 @@ module Ci
)
end
- def fetch_and_build_components
+ def build_components(version)
return if errors.present?
- max_components = Ci::Catalog::ComponentsProject::COMPONENTS_LIMIT
- component_paths = components_project.fetch_component_paths(release.sha, limit: max_components + 1)
+ response = BuildComponentsService.new(release, version).execute
- if component_paths.size > max_components
- return error("Release cannot contain more than #{max_components} components")
+ if response.success?
+ version.components = response.payload
+ else
+ error(response.message)
end
-
- build_components(component_paths)
end
- def build_components(component_paths)
- paths_with_oids = component_paths.map { |path| [release.sha, path] }
- blobs = project.repository.blobs_at(paths_with_oids)
-
- blobs.each do |blob|
- metadata = extract_metadata(blob)
- build_catalog_resource_component(metadata)
- end
- rescue ::Gitlab::Config::Loader::FormatError => e
- error(e)
- end
-
- def extract_metadata(blob)
- component_name = components_project.extract_component_name(blob.path)
-
- {
- name: component_name,
- spec: components_project.extract_spec(blob.data)
- }
- end
-
- def build_catalog_resource_component(metadata)
- return if errors.present?
-
- component = @version.components.build(
- name: metadata[:name],
- project: @version.project,
- spec: metadata[:spec],
- catalog_resource: @version.catalog_resource,
- created_at: Time.current
- )
-
- return if component.valid?
-
- error("Build component error: #{component.errors.full_messages.join(', ')}")
- end
-
- def publish_catalog_resource!
+ def publish(version)
return if errors.present?
::Ci::Catalog::Resources::Version.transaction do
BulkInsertableAssociations.with_bulk_insert do
- @version.save!
+ version.save!
end
project.catalog_resource.publish!
diff --git a/app/views/explore/projects/_head.html.haml b/app/views/explore/projects/_head.html.haml
index c1d37965cd6..1699a3d5873 100644
--- a/app/views/explore/projects/_head.html.haml
+++ b/app/views/explore/projects/_head.html.haml
@@ -7,5 +7,5 @@
%h1.page-title.gl-font-size-h-display= page_title
.page-title-controls
- if current_user&.can_create_project?
- = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm) do
+ = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { event_tracking: 'click_new_project_projects_explore' } }) do
= _("New project")
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
index 4e667636769..c6acde8958c 100644
--- a/app/views/explore/projects/_nav.html.haml
+++ b/app/views/explore/projects/_nav.html.haml
@@ -1,10 +1,10 @@
.gl-flex
= gl_tabs_nav({ class: 'gl-display-flex gl-flex-grow-1 gl-border-none'}) do
- = gl_tab_link_to _('Most starred'), starred_explore_projects_path, { item_active: current_page?(starred_explore_projects_path) || current_page?(explore_root_path) }
- = gl_tab_link_to _('Trending'), trending_explore_projects_path
- = gl_tab_link_to _('Active'), explore_projects_path, { item_active: current_page?(explore_projects_path) && !params['archived'] }
- = gl_tab_link_to _('Inactive'), explore_projects_path({ archived: 'only' }), { item_active: current_page?(explore_projects_path) && params['archived'] == 'only' }
- = gl_tab_link_to _('All'), explore_projects_path({ archived: true }), { item_active: current_page?(explore_projects_path) && params['archived'] == 'true' }
+ = gl_tab_link_to _('Most starred'), starred_explore_projects_path, { item_active: current_page?(starred_explore_projects_path) || current_page?(explore_root_path), data: { event_tracking: 'click_tab_projects_explore', event_label: 'Most starred' } }
+ = gl_tab_link_to _('Trending'), trending_explore_projects_path, { data: { event_tracking: 'click_tab_projects_explore', event_label: 'Trending' } }
+ = gl_tab_link_to _('Active'), explore_projects_path, { item_active: current_page?(explore_projects_path) && !params['archived'], data: { event_tracking: 'click_tab_projects_explore', event_label: 'Active' } }
+ = gl_tab_link_to _('Inactive'), explore_projects_path({ archived: 'only' }), { item_active: current_page?(explore_projects_path) && params['archived'] == 'only', data: { event_tracking: 'click_tab_projects_explore', event_label: 'Inactive' } }
+ = gl_tab_link_to _('All'), explore_projects_path({ archived: true }), { item_active: current_page?(explore_projects_path) && params['archived'] == 'true', data: { event_tracking: 'click_tab_projects_explore', event_label: 'All' } }
#js-projects-explore-filtered-search-and-sort.gl-py-5.gl-border-t.gl-border-b{ data: { app_data: projects_explore_filtered_search_and_sort_app_data } }
-# This element takes up space while Vue is rendering to avoid page jump
diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml
index a55dfb110f0..973da3e9e34 100644
--- a/app/views/explore/projects/_projects.html.haml
+++ b/app/views/explore/projects/_projects.html.haml
@@ -2,4 +2,4 @@
.nothing-here-block
%h5= _('Enter at least three characters to search')
- else
- = render 'shared/projects/list', projects: projects, user: current_user, explore_page: true, pipeline_status: Feature.enabled?(:explore_pipeline_status, type: :ops)
+ = render 'shared/projects/list', projects: projects, user: current_user, explore_page: true, pipeline_status: Feature.enabled?(:explore_pipeline_status, type: :ops), event_tracking: 'use_pagination_projects_explore'
diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml
index 8b29cb1988f..127543a235f 100644
--- a/app/views/kaminari/gitlab/_without_count.html.haml
+++ b/app/views/kaminari/gitlab/_without_count.html.haml
@@ -2,11 +2,11 @@
%ul.pagination.justify-content-center
- if previous_path
%li.page-item.prev
- = link_to previous_path, rel: 'prev', class: 'page-link' do
+ = link_to previous_path, rel: 'prev', class: 'page-link', data: paginate_event_tracking_data_attributes(event_tracking: event_tracking, event_label: 'prev') do
= sprite_icon('chevron-lg-left', size: 8)
= s_('Pagination|Prev')
- if next_path
%li.page-item.next
- = link_to next_path, rel: 'next', class: 'page-link' do
+ = link_to next_path, rel: 'next', class: 'page-link', data: paginate_event_tracking_data_attributes(event_tracking: event_tracking, event_label: 'next') do
= s_('Pagination|Next')
= sprite_icon('chevron-lg-right', size: 8)
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index f9c42c50b5e..966f5f0156c 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -52,7 +52,7 @@
- @branches.each do |branch|
= render "projects/branches/branch", branch: branch, merged: @merged_branch_names.include?(branch.name), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any?
- if Feature.enabled?(:branch_list_keyset_pagination, @project)
- = render('kaminari/gitlab/without_count', previous_path: @prev_path, next_path: @next_path)
+ = render('kaminari/gitlab/without_count', previous_path: @prev_path, next_path: @next_path, event_tracking: nil)
- else
= paginate @branches, theme: 'gitlab'
- else
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 1941e688fb2..ba616c45f89 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -11,6 +11,7 @@
- skip_pagination = false unless local_assigns[:skip_pagination] == true
- compact_mode = false unless local_assigns[:compact_mode] == true
- card_mode = local_assigns[:card_mode] == true
+- event_tracking = local_assigns[:event_tracking]
- css_classes = "#{'compact' if compact_mode} #{'explore' if explore_projects_tab?}"
- contributed_projects_current_user_empty_message_header = s_('UserProfile|Explore public groups to find projects to contribute to')
- contributed_projects_visitor_empty_message = s_('UserProfile|This user hasn\'t contributed to any projects')
@@ -53,7 +54,7 @@
forks: able_to_see_forks_count?(project, user), show_last_commit_as_description: show_last_commit_as_description,
user: user, merge_requests: able_to_see_merge_requests?(project, user), issues: able_to_see_issues?(project, user),
pipeline_status: pipeline_status, compact_mode: compact_mode
- = paginate_collection(projects, remote: remote) unless skip_pagination
+ = paginate_collection(projects, remote: remote, event_tracking: event_tracking) unless skip_pagination
- else
- if @contributed_projects
= render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: own_projects_illustration_path,
diff --git a/config/events/click_new_project_projects_explore.yml b/config/events/click_new_project_projects_explore.yml
new file mode 100644
index 00000000000..d485212fb5f
--- /dev/null
+++ b/config/events/click_new_project_projects_explore.yml
@@ -0,0 +1,16 @@
+---
+description: Click "New project" button on projects explore
+internal_events: true
+action: click_new_project_projects_explore
+identifiers:
+- user
+product_group: tenant_scale
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/events/click_tab_projects_explore.yml b/config/events/click_tab_projects_explore.yml
new file mode 100644
index 00000000000..374a1da0041
--- /dev/null
+++ b/config/events/click_tab_projects_explore.yml
@@ -0,0 +1,19 @@
+---
+description: Click on a tab on projects explore
+internal_events: true
+action: click_tab_projects_explore
+identifiers:
+- user
+additional_properties:
+ label:
+ description: Tab name
+product_group: tenant_scale
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/events/use_filter_bar_projects_explore.yml b/config/events/use_filter_bar_projects_explore.yml
new file mode 100644
index 00000000000..d85d74c61dd
--- /dev/null
+++ b/config/events/use_filter_bar_projects_explore.yml
@@ -0,0 +1,19 @@
+---
+description: User uses filter bar
+internal_events: true
+action: use_filter_bar_projects_explore
+identifiers:
+- user
+additional_properties:
+ label:
+ description: JSON object of filter parameters
+product_group: tenant_scale
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/events/use_pagination_projects_explore.yml b/config/events/use_pagination_projects_explore.yml
new file mode 100644
index 00000000000..e07b035de97
--- /dev/null
+++ b/config/events/use_pagination_projects_explore.yml
@@ -0,0 +1,19 @@
+---
+description: Use pagination on projects explore
+internal_events: true
+action: use_pagination_projects_explore
+identifiers:
+- user
+additional_properties:
+ label:
+ description: Pagination button (next or prev)
+product_group: tenant_scale
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/events/use_sort_projects_explore.yml b/config/events/use_sort_projects_explore.yml
new file mode 100644
index 00000000000..1f3e2871ee6
--- /dev/null
+++ b/config/events/use_sort_projects_explore.yml
@@ -0,0 +1,19 @@
+---
+description: User uses sort on projects explore
+internal_events: true
+action: use_sort_projects_explore
+identifiers:
+- user
+additional_properties:
+ label:
+ description: Selected sort option
+product_group: tenant_scale
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_click_new_project_projects_explore_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_click_new_project_projects_explore_monthly.yml
new file mode 100644
index 00000000000..c7b76036a17
--- /dev/null
+++ b/config/metrics/counts_28d/count_distinct_user_id_from_click_new_project_projects_explore_monthly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_click_new_project_projects_explore_monthly
+description: Monthly count of unique users that click "New project" button on projects explore
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: click_new_project_projects_explore
+ unique: user.id
diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_click_tab_projects_explore_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_click_tab_projects_explore_monthly.yml
new file mode 100644
index 00000000000..69f4f9abcad
--- /dev/null
+++ b/config/metrics/counts_28d/count_distinct_user_id_from_click_tab_projects_explore_monthly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_click_tab_projects_explore_monthly
+description: Monthly count of unique users click on tab on projects explore
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: click_tab_projects_explore
+ unique: user.id
diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_use_filter_bar_projects_explore_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_use_filter_bar_projects_explore_monthly.yml
new file mode 100644
index 00000000000..d96bc30679c
--- /dev/null
+++ b/config/metrics/counts_28d/count_distinct_user_id_from_use_filter_bar_projects_explore_monthly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_use_filter_bar_projects_explore_monthly
+description: Monthly count of unique users using explore projects search bar
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: use_filter_bar_projects_explore
+ unique: user.id
diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_use_pagination_projects_explore_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_use_pagination_projects_explore_monthly.yml
new file mode 100644
index 00000000000..c4f1b482761
--- /dev/null
+++ b/config/metrics/counts_28d/count_distinct_user_id_from_use_pagination_projects_explore_monthly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_use_pagination_projects_explore_monthly
+description: Monthly count of unique users that use pagination on projects explore
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: use_pagination_projects_explore
+ unique: user.id
diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_use_sort_projects_explore_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_use_sort_projects_explore_monthly.yml
new file mode 100644
index 00000000000..beaa7e3994a
--- /dev/null
+++ b/config/metrics/counts_28d/count_distinct_user_id_from_use_sort_projects_explore_monthly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_use_sort_projects_explore_monthly
+description: Monthly count of unique users using projects explore sort
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: use_sort_projects_explore
+ unique: user.id
diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_click_new_project_projects_explore_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_click_new_project_projects_explore_weekly.yml
new file mode 100644
index 00000000000..ad66250684d
--- /dev/null
+++ b/config/metrics/counts_7d/count_distinct_user_id_from_click_new_project_projects_explore_weekly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_click_new_project_projects_explore_weekly
+description: Weekly count of unique users that click "New project" button on projects explore
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: click_new_project_projects_explore
+ unique: user.id
diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_click_tab_projects_explore_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_click_tab_projects_explore_weekly.yml
new file mode 100644
index 00000000000..5593fbe1b54
--- /dev/null
+++ b/config/metrics/counts_7d/count_distinct_user_id_from_click_tab_projects_explore_weekly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_click_tab_projects_explore_weekly
+description: Weekly count of unique users click on tab on projects explore
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: click_tab_projects_explore
+ unique: user.id
diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_use_filter_bar_projects_explore_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_use_filter_bar_projects_explore_weekly.yml
new file mode 100644
index 00000000000..0507a9eeecf
--- /dev/null
+++ b/config/metrics/counts_7d/count_distinct_user_id_from_use_filter_bar_projects_explore_weekly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_use_filter_bar_projects_explore_weekly
+description: Weekly count of unique users using explore projects search bar
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: use_filter_bar_projects_explore
+ unique: user.id
diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_use_pagination_projects_explore_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_use_pagination_projects_explore_weekly.yml
new file mode 100644
index 00000000000..e380afe6409
--- /dev/null
+++ b/config/metrics/counts_7d/count_distinct_user_id_from_use_pagination_projects_explore_weekly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_use_pagination_projects_explore_weekly
+description: Weekly count of unique users that use pagination on projects explore
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: use_pagination_projects_explore
+ unique: user.id
diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_use_sort_projects_explore_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_use_sort_projects_explore_weekly.yml
new file mode 100644
index 00000000000..44e14cb6f75
--- /dev/null
+++ b/config/metrics/counts_7d/count_distinct_user_id_from_use_sort_projects_explore_weekly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_use_sort_projects_explore_weekly
+description: Weekly count of unique users using projects explore sort
+product_group: tenant_scale
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.3'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158282
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: use_sort_projects_explore
+ unique: user.id
diff --git a/db/docs/approval_policy_rule_project_links.yml b/db/docs/approval_policy_rule_project_links.yml
new file mode 100644
index 00000000000..276a34be094
--- /dev/null
+++ b/db/docs/approval_policy_rule_project_links.yml
@@ -0,0 +1,12 @@
+---
+table_name: approval_policy_rule_project_links
+classes:
+- Security::ApprovalPolicyRuleProjectLink
+feature_categories:
+- security_policy_management
+description: Project/ApprovalPolicyRule join table.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158441
+milestone: '17.3'
+gitlab_schema: gitlab_main_cell
+sharding_key:
+ project_id: projects
\ No newline at end of file
diff --git a/db/docs/batched_background_migrations/backfill_epic_issues_namespace_id.yml b/db/docs/batched_background_migrations/backfill_epic_issues_namespace_id.yml
new file mode 100644
index 00000000000..3b42a90b7af
--- /dev/null
+++ b/db/docs/batched_background_migrations/backfill_epic_issues_namespace_id.yml
@@ -0,0 +1,9 @@
+---
+migration_job_name: BackfillEpicIssuesNamespaceId
+description: Backfills sharding key `epic_issues.namespace_id` from `issues`.
+feature_category: portfolio_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156640
+milestone: '17.3'
+queued_migration_version: 20240618123929
+finalize_after: '2024-09-22'
+finalized_by: # version of the migration that finalized this BBM
diff --git a/db/docs/epic_issues.yml b/db/docs/epic_issues.yml
index 42de3d7d57a..c4b599d1c67 100644
--- a/db/docs/epic_issues.yml
+++ b/db/docs/epic_issues.yml
@@ -19,3 +19,4 @@ desired_sharding_key:
table: issues
sharding_key: namespace_id
belongs_to: issue
+desired_sharding_key_migration_job_name: BackfillEpicIssuesNamespaceId
diff --git a/db/migrate/20240618123925_add_namespace_id_to_epic_issues.rb b/db/migrate/20240618123925_add_namespace_id_to_epic_issues.rb
new file mode 100644
index 00000000000..adde20a0911
--- /dev/null
+++ b/db/migrate/20240618123925_add_namespace_id_to_epic_issues.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddNamespaceIdToEpicIssues < Gitlab::Database::Migration[2.2]
+ milestone '17.3'
+
+ def change
+ add_column :epic_issues, :namespace_id, :bigint
+ end
+end
diff --git a/db/migrate/20240705083121_create_approval_policy_rule_project_links.rb b/db/migrate/20240705083121_create_approval_policy_rule_project_links.rb
new file mode 100644
index 00000000000..f915948fed5
--- /dev/null
+++ b/db/migrate/20240705083121_create_approval_policy_rule_project_links.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class CreateApprovalPolicyRuleProjectLinks < Gitlab::Database::Migration[2.2]
+ milestone '17.3'
+
+ INDEX_NAME = 'index_approval_policy_rule_on_project_and_rule'
+
+ def up
+ create_table :approval_policy_rule_project_links do |t|
+ t.bigint :project_id, null: false, index: true
+ t.bigint :approval_policy_rule_id, null: false
+
+ t.index [:approval_policy_rule_id, :project_id], unique: true, name: INDEX_NAME
+ end
+ end
+
+ def down
+ drop_table :approval_policy_rule_project_links
+ end
+end
diff --git a/db/migrate/20240705083319_add_project_id_fk_to_approval_policy_rule_project_links.rb b/db/migrate/20240705083319_add_project_id_fk_to_approval_policy_rule_project_links.rb
new file mode 100644
index 00000000000..91e72816d67
--- /dev/null
+++ b/db/migrate/20240705083319_add_project_id_fk_to_approval_policy_rule_project_links.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddProjectIdFkToApprovalPolicyRuleProjectLinks < Gitlab::Database::Migration[2.2]
+ milestone '17.3'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_policy_rule_project_links, :projects, column: :project_id
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_policy_rule_project_links, column: :project_id
+ end
+ end
+end
diff --git a/db/migrate/20240705083349_add_rule_id_fk_to_approval_policy_rule_project_links.rb b/db/migrate/20240705083349_add_rule_id_fk_to_approval_policy_rule_project_links.rb
new file mode 100644
index 00000000000..db34fd1b1f9
--- /dev/null
+++ b/db/migrate/20240705083349_add_rule_id_fk_to_approval_policy_rule_project_links.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddRuleIdFkToApprovalPolicyRuleProjectLinks < Gitlab::Database::Migration[2.2]
+ milestone '17.3'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_policy_rule_project_links, :approval_policy_rules,
+ column: :approval_policy_rule_id
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :approval_policy_rule_project_links, column: :approval_policy_rule_id
+ end
+ end
+end
diff --git a/db/post_migrate/20240618123926_index_epic_issues_on_namespace_id.rb b/db/post_migrate/20240618123926_index_epic_issues_on_namespace_id.rb
new file mode 100644
index 00000000000..cd055b03554
--- /dev/null
+++ b/db/post_migrate/20240618123926_index_epic_issues_on_namespace_id.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class IndexEpicIssuesOnNamespaceId < Gitlab::Database::Migration[2.2]
+ milestone '17.3'
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_epic_issues_on_namespace_id'
+
+ def up
+ add_concurrent_index :epic_issues, :namespace_id, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :epic_issues, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20240618123927_add_epic_issues_namespace_id_fk.rb b/db/post_migrate/20240618123927_add_epic_issues_namespace_id_fk.rb
new file mode 100644
index 00000000000..e81bd02f623
--- /dev/null
+++ b/db/post_migrate/20240618123927_add_epic_issues_namespace_id_fk.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddEpicIssuesNamespaceIdFk < Gitlab::Database::Migration[2.2]
+ milestone '17.3'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :epic_issues, :namespaces, column: :namespace_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :epic_issues, column: :namespace_id
+ end
+ end
+end
diff --git a/db/post_migrate/20240618123928_add_epic_issues_namespace_id_trigger.rb b/db/post_migrate/20240618123928_add_epic_issues_namespace_id_trigger.rb
new file mode 100644
index 00000000000..0870188c61b
--- /dev/null
+++ b/db/post_migrate/20240618123928_add_epic_issues_namespace_id_trigger.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddEpicIssuesNamespaceIdTrigger < Gitlab::Database::Migration[2.2]
+ milestone '17.3'
+
+ def up
+ install_sharding_key_assignment_trigger(
+ table: :epic_issues,
+ sharding_key: :namespace_id,
+ parent_table: :issues,
+ parent_sharding_key: :namespace_id,
+ foreign_key: :issue_id
+ )
+ end
+
+ def down
+ remove_sharding_key_assignment_trigger(
+ table: :epic_issues,
+ sharding_key: :namespace_id,
+ parent_table: :issues,
+ parent_sharding_key: :namespace_id,
+ foreign_key: :issue_id
+ )
+ end
+end
diff --git a/db/post_migrate/20240618123929_queue_backfill_epic_issues_namespace_id.rb b/db/post_migrate/20240618123929_queue_backfill_epic_issues_namespace_id.rb
new file mode 100644
index 00000000000..29bfadc8df7
--- /dev/null
+++ b/db/post_migrate/20240618123929_queue_backfill_epic_issues_namespace_id.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class QueueBackfillEpicIssuesNamespaceId < Gitlab::Database::Migration[2.2]
+ milestone '17.3'
+ restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
+
+ MIGRATION = "BackfillEpicIssuesNamespaceId"
+ DELAY_INTERVAL = 2.minutes
+ BATCH_SIZE = 1000
+ SUB_BATCH_SIZE = 100
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :epic_issues,
+ :id,
+ :namespace_id,
+ :issues,
+ :namespace_id,
+ :issue_id,
+ job_interval: DELAY_INTERVAL,
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(
+ MIGRATION,
+ :epic_issues,
+ :id,
+ [
+ :namespace_id,
+ :issues,
+ :namespace_id,
+ :issue_id
+ ]
+ )
+ end
+end
diff --git a/db/schema_migrations/20240618123925 b/db/schema_migrations/20240618123925
new file mode 100644
index 00000000000..d3307c35a58
--- /dev/null
+++ b/db/schema_migrations/20240618123925
@@ -0,0 +1 @@
+5c4f06513e121d11ec7e9a58719f8afd89aa8e1138f389c9ed9051e70d38690c
\ No newline at end of file
diff --git a/db/schema_migrations/20240618123926 b/db/schema_migrations/20240618123926
new file mode 100644
index 00000000000..ed1e6ecd3f6
--- /dev/null
+++ b/db/schema_migrations/20240618123926
@@ -0,0 +1 @@
+3e36a93340a321ab8806e60909b29dcf5be511525343925c5cf03ae76d97d214
\ No newline at end of file
diff --git a/db/schema_migrations/20240618123927 b/db/schema_migrations/20240618123927
new file mode 100644
index 00000000000..80f35e26af2
--- /dev/null
+++ b/db/schema_migrations/20240618123927
@@ -0,0 +1 @@
+936f20fb1ba535b2eaff62b233d4e5fedda222268472699bd6c486099b3d4623
\ No newline at end of file
diff --git a/db/schema_migrations/20240618123928 b/db/schema_migrations/20240618123928
new file mode 100644
index 00000000000..bb5a76cd7aa
--- /dev/null
+++ b/db/schema_migrations/20240618123928
@@ -0,0 +1 @@
+6c2cc4a0226782be360e7288703eb93b7395e26db1eb816503ec0ad188150c9c
\ No newline at end of file
diff --git a/db/schema_migrations/20240618123929 b/db/schema_migrations/20240618123929
new file mode 100644
index 00000000000..bfd13282170
--- /dev/null
+++ b/db/schema_migrations/20240618123929
@@ -0,0 +1 @@
+07e54ae3656922da72af33ae09aafcb60d37edf7b7a6dc075eb2746efd4facd7
\ No newline at end of file
diff --git a/db/schema_migrations/20240705083121 b/db/schema_migrations/20240705083121
new file mode 100644
index 00000000000..9366d4bd675
--- /dev/null
+++ b/db/schema_migrations/20240705083121
@@ -0,0 +1 @@
+4c19a5f70977c3e8e334e043f4ab7bd26bcffec355793b7a38fb5ca5c36cb7ee
\ No newline at end of file
diff --git a/db/schema_migrations/20240705083319 b/db/schema_migrations/20240705083319
new file mode 100644
index 00000000000..12d451eafde
--- /dev/null
+++ b/db/schema_migrations/20240705083319
@@ -0,0 +1 @@
+9a1d39ba92f817293f584f37201db2bd014787561b07c07e6e250901daa35d9e
\ No newline at end of file
diff --git a/db/schema_migrations/20240705083349 b/db/schema_migrations/20240705083349
new file mode 100644
index 00000000000..f14f282879e
--- /dev/null
+++ b/db/schema_migrations/20240705083349
@@ -0,0 +1 @@
+93db69f816de1eaeee094a3ba8cd2f25b03baaedcb508e5e6d0c7220e79ed1c0
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 00c02f1f02a..4e763cc2f6d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1122,6 +1122,22 @@ RETURN NEW;
END
$$;
+CREATE FUNCTION trigger_46ebe375f632() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+IF NEW."namespace_id" IS NULL THEN
+ SELECT "namespace_id"
+ INTO NEW."namespace_id"
+ FROM "issues"
+ WHERE "issues"."id" = NEW."issue_id";
+END IF;
+
+RETURN NEW;
+
+END
+$$;
+
CREATE FUNCTION trigger_49862b4b3035() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -6036,6 +6052,21 @@ CREATE SEQUENCE approval_merge_request_rules_users_id_seq
ALTER SEQUENCE approval_merge_request_rules_users_id_seq OWNED BY approval_merge_request_rules_users.id;
+CREATE TABLE approval_policy_rule_project_links (
+ id bigint NOT NULL,
+ project_id bigint NOT NULL,
+ approval_policy_rule_id bigint NOT NULL
+);
+
+CREATE SEQUENCE approval_policy_rule_project_links_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE approval_policy_rule_project_links_id_seq OWNED BY approval_policy_rule_project_links.id;
+
CREATE TABLE approval_policy_rules (
id bigint NOT NULL,
security_policy_id bigint NOT NULL,
@@ -10226,7 +10257,8 @@ CREATE TABLE epic_issues (
id integer NOT NULL,
epic_id integer NOT NULL,
issue_id integer NOT NULL,
- relative_position integer
+ relative_position integer,
+ namespace_id bigint
);
CREATE SEQUENCE epic_issues_id_seq
@@ -20551,6 +20583,8 @@ ALTER TABLE ONLY approval_merge_request_rules_groups ALTER COLUMN id SET DEFAULT
ALTER TABLE ONLY approval_merge_request_rules_users ALTER COLUMN id SET DEFAULT nextval('approval_merge_request_rules_users_id_seq'::regclass);
+ALTER TABLE ONLY approval_policy_rule_project_links ALTER COLUMN id SET DEFAULT nextval('approval_policy_rule_project_links_id_seq'::regclass);
+
ALTER TABLE ONLY approval_policy_rules ALTER COLUMN id SET DEFAULT nextval('approval_policy_rules_id_seq'::regclass);
ALTER TABLE ONLY approval_project_rules ALTER COLUMN id SET DEFAULT nextval('approval_project_rules_id_seq'::regclass);
@@ -22358,6 +22392,9 @@ ALTER TABLE ONLY approval_merge_request_rules
ALTER TABLE ONLY approval_merge_request_rules_users
ADD CONSTRAINT approval_merge_request_rules_users_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY approval_policy_rule_project_links
+ ADD CONSTRAINT approval_policy_rule_project_links_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY approval_policy_rules
ADD CONSTRAINT approval_policy_rules_pkey PRIMARY KEY (id);
@@ -26192,6 +26229,10 @@ CREATE UNIQUE INDEX index_approval_merge_request_rules_users_1 ON approval_merge
CREATE INDEX index_approval_merge_request_rules_users_2 ON approval_merge_request_rules_users USING btree (user_id);
+CREATE UNIQUE INDEX index_approval_policy_rule_on_project_and_rule ON approval_policy_rule_project_links USING btree (approval_policy_rule_id, project_id);
+
+CREATE INDEX index_approval_policy_rule_project_links_on_project_id ON approval_policy_rule_project_links USING btree (project_id);
+
CREATE INDEX index_approval_policy_rules_on_policy_management_project_id ON approval_policy_rules USING btree (security_policy_management_project_id);
CREATE UNIQUE INDEX index_approval_policy_rules_on_unique_policy_rule_index ON approval_policy_rules USING btree (security_policy_id, rule_index);
@@ -27248,6 +27289,8 @@ CREATE INDEX index_epic_issues_on_epic_id_and_issue_id ON epic_issues USING btre
CREATE UNIQUE INDEX index_epic_issues_on_issue_id ON epic_issues USING btree (issue_id);
+CREATE INDEX index_epic_issues_on_namespace_id ON epic_issues USING btree (namespace_id);
+
CREATE INDEX index_epic_metrics ON epic_metrics USING btree (epic_id);
CREATE INDEX index_epic_user_mentions_on_group_id ON epic_user_mentions USING btree (group_id);
@@ -31804,6 +31847,8 @@ CREATE TRIGGER trigger_43484cb41aca BEFORE INSERT OR UPDATE ON wiki_repository_s
CREATE TRIGGER trigger_44558add1625 BEFORE INSERT OR UPDATE ON merge_request_assignees FOR EACH ROW EXECUTE FUNCTION trigger_44558add1625();
+CREATE TRIGGER trigger_46ebe375f632 BEFORE INSERT OR UPDATE ON epic_issues FOR EACH ROW EXECUTE FUNCTION trigger_46ebe375f632();
+
CREATE TRIGGER trigger_49862b4b3035 BEFORE INSERT OR UPDATE ON approval_group_rules_protected_branches FOR EACH ROW EXECUTE FUNCTION trigger_49862b4b3035();
CREATE TRIGGER trigger_49e070da6320 BEFORE INSERT OR UPDATE ON packages_dependency_links FOR EACH ROW EXECUTE FUNCTION trigger_49e070da6320();
@@ -32124,6 +32169,9 @@ ALTER TABLE ONLY analytics_devops_adoption_segments
ALTER TABLE ONLY project_statistics
ADD CONSTRAINT fk_198ad46fdc FOREIGN KEY (root_namespace_id) REFERENCES namespaces(id) ON DELETE SET NULL;
+ALTER TABLE ONLY approval_policy_rule_project_links
+ ADD CONSTRAINT fk_1c78796d52 FOREIGN KEY (approval_policy_rule_id) REFERENCES approval_policy_rules(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY issue_links
ADD CONSTRAINT fk_1cce06b868 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@@ -32712,6 +32760,9 @@ ALTER TABLE ONLY ci_build_trace_chunks
ALTER TABLE ONLY catalog_resource_components
ADD CONSTRAINT fk_89fd1a3e33 FOREIGN KEY (version_id) REFERENCES catalog_resource_versions(id) ON DELETE CASCADE;
+ALTER TABLE ONLY epic_issues
+ ADD CONSTRAINT fk_8a0fdc0d65 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY protected_branch_merge_access_levels
ADD CONSTRAINT fk_8a3072ccb3 FOREIGN KEY (protected_branch_id) REFERENCES protected_branches(id) ON DELETE CASCADE;
@@ -32808,6 +32859,9 @@ ALTER TABLE ONLY protected_environments
ALTER TABLE ONLY alert_management_alerts
ADD CONSTRAINT fk_9e49e5c2b7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY approval_policy_rule_project_links
+ ADD CONSTRAINT fk_9ed5cf0600 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY protected_branch_push_access_levels
ADD CONSTRAINT fk_9ffc86a3d9 FOREIGN KEY (protected_branch_id) REFERENCES protected_branches(id) ON DELETE CASCADE;
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 853786a6711..5e8b946c14d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -14609,6 +14609,29 @@ The edge type for [`PipelineExecutionPolicy`](#pipelineexecutionpolicy).
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`PipelineExecutionPolicy`](#pipelineexecutionpolicy) | The item at the end of the edge. |
+#### `PipelineManualVariableConnection`
+
+The connection type for [`PipelineManualVariable`](#pipelinemanualvariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `edges` | [`[PipelineManualVariableEdge]`](#pipelinemanualvariableedge) | A list of edges. |
+| `nodes` | [`[PipelineManualVariable]`](#pipelinemanualvariable) | A list of nodes. |
+| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `PipelineManualVariableEdge`
+
+The edge type for [`PipelineManualVariable`](#pipelinemanualvariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| `node` | [`PipelineManualVariable`](#pipelinemanualvariable) | The item at the end of the edge. |
+
#### `PipelineScheduleConnection`
The connection type for [`PipelineSchedule`](#pipelineschedule).
@@ -27604,7 +27627,7 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
| `iid` | [`String!`](#string) | Internal ID of the pipeline. |
| `jobArtifacts` | [`[CiJobArtifact!]`](#cijobartifact) | Job artifacts of the pipeline. |
| `latest` | [`Boolean!`](#boolean) | If the pipeline is the latest one or not. |
-| `manualVariables` | [`CiManualVariableConnection`](#cimanualvariableconnection) | CI/CD variables added to a manual pipeline. (see [Connections](#connections)) |
+| `manualVariables` | [`PipelineManualVariableConnection`](#pipelinemanualvariableconnection) | CI/CD variables added to a manual pipeline. (see [Connections](#connections)) |
| `mergeRequest` | [`MergeRequest`](#mergerequest) | The MR which the Pipeline is attached to. |
| `mergeRequestEventType` | [`PipelineMergeRequestEventType`](#pipelinemergerequesteventtype) | Event type of the pipeline associated with a merge request. |
| `name` | [`String`](#string) | Name of the pipeline. |
@@ -27824,6 +27847,18 @@ Represents the pipeline execution policy.
| `updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. |
| `yaml` | [`String!`](#string) | YAML definition of the policy. |
+### `PipelineManualVariable`
+
+CI/CD variables added to a manual pipeline.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `id` | [`ID!`](#id) | ID of the variable. |
+| `key` | [`String`](#string) | Name of the variable. |
+| `value` | [`String`](#string) | Value of the variable. |
+
### `PipelineMessage`
#### Fields
diff --git a/doc/tutorials/observability/img/java_configuration_menu.png b/doc/tutorials/observability/img/java_configuration_menu.png
new file mode 100644
index 00000000000..8f301aa9054
Binary files /dev/null and b/doc/tutorials/observability/img/java_configuration_menu.png differ
diff --git a/doc/tutorials/observability/img/java_edit_configuration.png b/doc/tutorials/observability/img/java_edit_configuration.png
new file mode 100644
index 00000000000..4b067af302e
Binary files /dev/null and b/doc/tutorials/observability/img/java_edit_configuration.png differ
diff --git a/doc/tutorials/observability/img/java_start_application.png b/doc/tutorials/observability/img/java_start_application.png
new file mode 100644
index 00000000000..0f060e96cb4
Binary files /dev/null and b/doc/tutorials/observability/img/java_start_application.png differ
diff --git a/doc/tutorials/observability/img/maven_changes.png b/doc/tutorials/observability/img/maven_changes.png
new file mode 100644
index 00000000000..7f094a70af7
Binary files /dev/null and b/doc/tutorials/observability/img/maven_changes.png differ
diff --git a/doc/tutorials/observability/observabillty_java_tutorial.md b/doc/tutorials/observability/observabillty_java_tutorial.md
new file mode 100644
index 00000000000..6a067e4dc9a
--- /dev/null
+++ b/doc/tutorials/observability/observabillty_java_tutorial.md
@@ -0,0 +1,127 @@
+---
+stage: Monitor
+group: Observability
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Tutorial: Use GitLab Observability with a Java Spring application
+
+DETAILS:
+**Tier:** Ultimate
+**Offering:** GitLab.com
+**Status:** Beta
+
+> - Observability [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124966) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `observability_features`. Disabled by default.
+
+FLAG:
+The availability of this feature is controlled by a feature flag.
+For more information, see the history.
+This feature is available for testing, but not ready for production use.
+
+In this tutorial, you'll learn how to create, configure, instrument, and monitor a Java Spring application using GitLab Observability features.
+
+## Before you begin
+
+To follow along this tutorial, you must have:
+
+- A GitLab Ultimate subscription for GitLab.com
+- A local installation of Ruby on Rails
+- Basic knowledge of Git, Java Spring, and the core concepts of [OpenTelemetry](https://opentelemetry.io/)
+
+## Create a GitLab project
+
+First, create a GitLab project and a corresponding access token.
+
+1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New project/repository**.
+1. Select **Create from template**.
+1. Select **Spring** and then **Use template**.
+1. Enter the project details.
+ - In the **Project name** field, enter a name such as `test-spring-o11y`
+1. Select **Create project**.
+1. In the `test-sprint-o11y` project, on the left sidebar, select **Settings > Access tokens**.
+1. Create a new access token with the Owner role and the `read_api` and `write_observability` scopes. Store the token value somewhere safe—you'll need it later.
+
+## Run the application
+
+Next, we'll run the application to ensure that it works.
+
+1. After cloning the project from GitLab, open it in IntelliJ (or your preferred IDE).
+1. Open `src/main/java/com.example.demo/DemoApplication` and run the application:
+ 
+1. After initialization, the application should be available at `http://localhost:8000`. Test it out, then in the IDE select the **Stop** button.
+
+## Add the OpenTelemetry dependencies
+
+Use auto-instrumentation to instrument the application:
+
+1. In the `pom.xml` file, add the required dependencies:
+
+ ```xml
+
+ io.opentelemetry
+ opentelemetry-api
+
+
+ io.opentelemetry
+ opentelemetry-sdk-extension-autoconfigure
+
+
+ io.opentelemetry
+ opentelemetry-sdk-extension-autoconfigure-spi
+
+ ```
+
+ ```xml
+
+
+
+ io.opentelemetry
+ opentelemetry-bom
+ 1.40.0
+ pom
+ import
+
+
+
+ ```
+
+1. Update dependencies by selecting **Update Maven Changes**:
+
+ 
+
+1. Download the OpenTelemetry java agent file from the OpenTelemetry repository.
+
+ ```shell
+ curl --location --http1.0 "https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar"
+ ```
+
+## Define environment variables
+
+The OpenTelemetry autoconfigure libraries read their configuration from environment variables.
+
+1. From the top-right menu, select **Edit Configurations...**:
+
+ 
+
+1. In the configuration menu, select the icon in the **Environment Variables** field.
+
+ 
+
+1. Add the following set of environment variables, replacing `{{PATH_TO_JAVA_AGENT}}`, `{{NAMESPACE_ID}}`, `{{PROJECT_ID}}`, `{{PROJECT_ACCESS_TOKEN}}` and `{{SERVICE_NAME}}` with the correct values.
+ - `JAVA_TOOL_OPTIONS=-javaagent:{{PATH_TO_JAVA_AGENT}}/opentelemetry-javaagent.jar`
+ - `OTEL_EXPORTER_OTLP_ENDPOINT=https://observe.gitlab.com/v3/{{NAMESPACE_ID}}/{{PROJECT_ID}}/ingest`
+ - `OTEL_EXPORTER_OTLP_HEADERS=PRIVATE-TOKEN\={{PROJECT_ACCESS_TOKEN}}`
+ - `OTEL_LOGS_EXPORTER=otlp`
+ - `OTEL_METRIC_EXPORT_INTERVAL=15000`
+ - `OTEL_METRICS_EXPORTER=otlp`
+ - `OTEL_SERVICE_NAME=example-java-application`
+ - `OTEL_TRACES_EXPORTER=otlp`
+
+1. Restart the application and reload the page at `http://localhost:8000` a few times.
+
+## View the information in GitLab
+
+To view the exported information from your test project:
+
+1. On the left sidebar, select **Search or go to** and find your project.
+1. Select **Monitor**, then either **Logs**, **Metrics**, or **Traces**.
diff --git a/doc/user/analytics/dora_metrics.md b/doc/user/analytics/dora_metrics.md
index 7f947100fb6..66189531a14 100644
--- a/doc/user/analytics/dora_metrics.md
+++ b/doc/user/analytics/dora_metrics.md
@@ -42,6 +42,19 @@ Deployment frequency is the frequency of successful deployments to production ov
Software leaders can use the deployment frequency metric to understand how often the team successfully deploys software to production, and how quickly the teams can respond to customers' requests or new market opportunities.
High deployment frequency means you can get feedback sooner and iterate faster to deliver improvements and features.
+### Deployment frequency forecasting
+
+DETAILS:
+**Tier:** Ultimate
+**Offering:** GitLab.com, Self-managed, GitLab Dedicated
+**Status:** Experiment
+
+Deployment frequency forecasting (formerly named Value stream forecasting) uses a statistical forecasting model to predict productivity metrics and identify anomalies across the software development lifecycle.
+This information can help you improve planning and decision-making for your product and teams.
+
+
+Watch an overview of [Value stream forecasting](https://www.youtube.com/watch?v=6u8_8QQ5pEQ&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED).
+
### How deployment frequency is calculated
In GitLab, deployment frequency is measured by the average number of deployments per day to a given environment, based on the deployment's end time (its `finished_at` property).
diff --git a/doc/user/gitlab_duo/index.md b/doc/user/gitlab_duo/index.md
index cd6e9373d68..7a67ee45ceb 100644
--- a/doc/user/gitlab_duo/index.md
+++ b/doc/user/gitlab_duo/index.md
@@ -227,18 +227,6 @@ DETAILS:
- LLM: Vertex AI Codey [`codechat-bison`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/code-chat)
- [View documentation](../analytics/analytics_dashboards.md#generate-a-custom-visualization-with-gitlab-duo).
-### Value stream forecasting
-
-DETAILS:
-**Tier:** GitLab.com and Self-managed: For a limited time, Ultimate. In the future, [GitLab Duo Enterprise](../../subscriptions/subscription-add-ons.md).
GitLab Dedicated: GitLab Duo Enterprise.
-**Offering:** GitLab.com, Self-managed, GitLab Dedicated
-**Status:** Experiment
-
-- Helps you improve planning and decision-making by predicting productivity metrics and identifying anomalies across your software development lifecycle.
-- LLM: Statistical forecasting
-- [Watch overview](https://www.youtube.com/watch?v=6u8_8QQ5pEQ&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
-- [View documentation](experiments.md#forecast-deployment-frequency-with-value-stream-forecasting).
-
## Disable GitLab Duo features for specific groups or projects or an entire instance
Disable GitLab Duo features by [following these instructions](turn_on_off.md).
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index e842f193fbe..65f2985bd92 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -469,14 +469,15 @@ To learn what happens when you sort by priority or label priority, see
DETAILS:
**Tier:** Free, Premium, Ultimate
-**Offering:** GitLab.com
+**Offering:** Self-managed
**Status:** Beta
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/408676) in GitLab 16.3 [with a flag](../../administration/feature_flags.md) named `enforce_locked_labels_on_merge`. This feature is [beta](../../policy/experiment-beta-support.md).
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/408676) in GitLab 16.3 [with a flag](../../administration/feature_flags.md) named `enforce_locked_labels_on_merge`. This feature is [beta](../../policy/experiment-beta-support.md). Disabled by default.
FLAG:
-On self-managed GitLab and GitLab Dedicated, this feature is not available.
-On GitLab.com, this feature is available but can be configured by GitLab.com administrators only. To make it available per group or per project, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `enforce_locked_labels_on_merge`.
+The availability of this feature is controlled by a feature flag.
+For more information, see the history.
+This feature is available for testing, but not ready for production use.
To comply with certain auditing requirements, you can set a label to be locked.
When a merge request with locked labels gets merged, nobody can remove them from the MR.
diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md
index 8af931d664d..c91ea083ca4 100644
--- a/doc/user/project/settings/project_access_tokens.md
+++ b/doc/user/project/settings/project_access_tokens.md
@@ -11,6 +11,9 @@ info: "To determine the technical writer assigned to the Stage/Group associated
Project access tokens are similar to passwords, except you can [limit access to resources](#scopes-for-a-project-access-token),
select a limited role, and provide an expiry date.
+NOTE:
+Actual access to a project is controlled by a combination of [roles and permissions](../../permissions.md), and the [token scopes](#scopes-for-a-project-access-token).
+
Use a project access token to authenticate:
- With the [GitLab API](../../../api/rest/index.md#personalprojectgroup-access-tokens).
diff --git a/lib/gitlab/background_migration/backfill_epic_issues_namespace_id.rb b/lib/gitlab/background_migration/backfill_epic_issues_namespace_id.rb
new file mode 100644
index 00000000000..64bd654c323
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_epic_issues_namespace_id.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ class BackfillEpicIssuesNamespaceId < BackfillDesiredShardingKeyJob
+ operation_name :backfill_epic_issues_namespace_id
+ feature_category :portfolio_management
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb
index e056f8511e5..74a52ecacac 100644
--- a/lib/gitlab/ci/config/entry/processable.rb
+++ b/lib/gitlab/ci/config/entry/processable.rb
@@ -27,6 +27,7 @@ module Gitlab
validates :name, length: { maximum: 255 }
validates :config, mutually_exclusive_keys: %i[script trigger]
+ validates :config, mutually_exclusive_keys: %i[run trigger]
validates :config, disallowed_keys: {
in: %i[only except start_in],
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 9684776d274..26ff169f6fc 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -1072,6 +1072,7 @@ excluded_attributes:
epic_issue:
- :epic_id
- :issue_id
+ - :namespace_id
system_note_metadata:
- :description_version_id
- :note_id
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7aaf8624ef1..4df51ec6599 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19515,6 +19515,9 @@ msgstr ""
msgid "DuoProTrial|Code refactorization"
msgstr ""
+msgid "DuoProTrial|Congratulations, your free GitLab Duo Pro trial is activated and will expire on %{exp_date}. The new license might take a minute to show on the page. To give members access to new GitLab Duo Pro features, %{assign_link_start}assign them%{assign_link_end} to GitLab Duo Pro seats."
+msgstr ""
+
msgid "DuoProTrial|Day %{daysUsed}/%{duration}"
msgstr ""
@@ -19566,9 +19569,6 @@ msgstr ""
msgid "DuoProTrial|You do not have access to the GitLab Duo Pro trial"
msgstr ""
-msgid "DuoProTrial|You have successfully created a trial subscription for GitLab Duo Pro. It will expire on %{exp_date}.%{new_line}To get started, enable the GitLab Duo Pro add-on for team members on this page by turning on the toggle for each team member. The subscription may take a minute to sync, so refresh the page if it's not visible yet."
-msgstr ""
-
msgid "DuoProTrial|You no longer have access to GitLab Duo Pro features"
msgstr ""
@@ -46538,9 +46538,6 @@ msgstr ""
msgid "SSL verification"
msgstr ""
-msgid "SVG could not be rendered correctly: "
-msgstr ""
-
msgid "Sat"
msgstr ""
@@ -60691,6 +60688,9 @@ msgstr ""
msgid "WorkItem|Test case"
msgstr ""
+msgid "WorkItem|The %{workItemType} was closed automatically when a branch was merged."
+msgstr ""
+
msgid "WorkItem|The current key result"
msgstr ""
@@ -60709,6 +60709,12 @@ msgstr ""
msgid "WorkItem|This %{workItemType} is currently blocked by the following items:"
msgstr ""
+msgid "WorkItem|This %{workItemType} will be closed when any of the following is merged."
+msgstr ""
+
+msgid "WorkItem|This %{workItemType} will be closed when the following is merged."
+msgstr ""
+
msgid "WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it."
msgstr ""
diff --git a/spec/frontend/content_editor/components/wrappers/image_spec.js b/spec/frontend/content_editor/components/wrappers/image_spec.js
index e594d1c9075..b5ff8fcd5e4 100644
--- a/spec/frontend/content_editor/components/wrappers/image_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/image_spec.js
@@ -1,7 +1,7 @@
import { NodeViewWrapper } from '@tiptap/vue-2';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ImageWrapper from '~/content_editor/components/wrappers/image.vue';
-import { createTestEditor, mockChainedCommands } from '../../test_utils';
+import { createTestEditor } from '../../test_utils';
import '~/content_editor/services/upload_helpers';
jest.mock('~/content_editor/services/upload_helpers', () => ({
@@ -13,14 +13,17 @@ jest.mock('~/content_editor/services/upload_helpers', () => ({
describe('content/components/wrappers/image_spec', () => {
let wrapper;
let tiptapEditor;
+ let updateAttributes;
const createWrapper = (node = {}) => {
tiptapEditor = createTestEditor();
+ updateAttributes = jest.fn();
wrapper = shallowMountExtended(ImageWrapper, {
propsData: {
editor: tiptapEditor,
node,
getPos: jest.fn().mockReturnValue(12),
+ updateAttributes,
},
});
};
@@ -117,19 +120,9 @@ describe('content/components/wrappers/image_spec', () => {
});
it('updates prosemirror doc state on mouse release with final size', () => {
- const commands = mockChainedCommands(tiptapEditor, [
- 'focus',
- 'updateAttributes',
- 'setNodeSelection',
- 'run',
- ]);
-
document.dispatchEvent(new MouseEvent('mouseup'));
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.updateAttributes).toHaveBeenCalledWith('image', tiptapNodeAttributes);
- expect(commands.setNodeSelection).toHaveBeenCalledWith(12);
- expect(commands.run).toHaveBeenCalled();
+ expect(updateAttributes).toHaveBeenCalledWith(tiptapNodeAttributes);
});
});
});
diff --git a/spec/frontend/lib/utils/chart_utils_spec.js b/spec/frontend/lib/utils/chart_utils_spec.js
index 3b34b0ef672..65bb68c5017 100644
--- a/spec/frontend/lib/utils/chart_utils_spec.js
+++ b/spec/frontend/lib/utils/chart_utils_spec.js
@@ -1,8 +1,4 @@
-import { firstAndLastY, getToolboxOptions } from '~/lib/utils/chart_utils';
-import { __ } from '~/locale';
-import * as iconUtils from '~/lib/utils/icon_utils';
-
-jest.mock('~/lib/utils/icon_utils');
+import { firstAndLastY } from '~/lib/utils/chart_utils';
describe('Chart utils', () => {
describe('firstAndLastY', () => {
@@ -16,53 +12,4 @@ describe('Chart utils', () => {
expect(firstAndLastY(data)).toEqual([1, 3]);
});
});
-
- describe('getToolboxOptions', () => {
- describe('when icons are successfully fetched', () => {
- beforeEach(() => {
- iconUtils.getSvgIconPathContent.mockImplementation((name) =>
- Promise.resolve(`${name}-svg-path-mock`),
- );
- });
-
- it('returns toolbox config', async () => {
- await expect(getToolboxOptions()).resolves.toEqual({
- toolbox: {
- feature: {
- dataZoom: {
- icon: {
- zoom: 'path://marquee-selection-svg-path-mock',
- back: 'path://redo-svg-path-mock',
- },
- },
- restore: {
- icon: 'path://repeat-svg-path-mock',
- },
- saveAsImage: {
- icon: 'path://download-svg-path-mock',
- },
- },
- },
- });
- });
- });
-
- describe('when icons are not successfully fetched', () => {
- const error = new Error();
-
- beforeEach(() => {
- iconUtils.getSvgIconPathContent.mockRejectedValue(error);
- jest.spyOn(console, 'warn').mockImplementation();
- });
-
- it('returns empty object and calls `console.warn`', async () => {
- await expect(getToolboxOptions()).resolves.toEqual({});
- // eslint-disable-next-line no-console
- expect(console.warn).toHaveBeenCalledWith(
- __('SVG could not be rendered correctly: '),
- error,
- );
- });
- });
- });
});
diff --git a/spec/frontend/lib/utils/icon_utils_spec.js b/spec/frontend/lib/utils/icon_utils_spec.js
deleted file mode 100644
index 59839862504..00000000000
--- a/spec/frontend/lib/utils/icon_utils_spec.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import { clearSvgIconPathContentCache, getSvgIconPathContent } from '~/lib/utils/icon_utils';
-
-describe('Icon utils', () => {
- describe('getSvgIconPathContent', () => {
- let spriteIcons;
- let axiosMock;
- const mockName = 'mockIconName';
- const mockPath = 'mockPath';
- const mockIcons = ``;
-
- beforeAll(() => {
- spriteIcons = gon.sprite_icons;
- gon.sprite_icons = 'mockSpriteIconsEndpoint';
- });
-
- afterAll(() => {
- gon.sprite_icons = spriteIcons;
- });
-
- beforeEach(() => {
- axiosMock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- axiosMock.restore();
- clearSvgIconPathContentCache();
- });
-
- describe('when the icons can be loaded', () => {
- beforeEach(() => {
- axiosMock.onGet(gon.sprite_icons).reply(HTTP_STATUS_OK, mockIcons);
- });
-
- it('extracts svg icon path content from sprite icons', () => {
- return getSvgIconPathContent(mockName).then((path) => {
- expect(path).toBe(mockPath);
- });
- });
-
- it('returns null if icon path content does not exist', () => {
- return getSvgIconPathContent('missing-icon').then((path) => {
- expect(path).toBe(null);
- });
- });
- });
-
- describe('when the icons cannot be loaded on the first 2 tries', () => {
- beforeEach(() => {
- axiosMock
- .onGet(gon.sprite_icons)
- .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR)
- .onGet(gon.sprite_icons)
- .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR)
- .onGet(gon.sprite_icons)
- .reply(HTTP_STATUS_OK, mockIcons);
- });
-
- it('returns null', () => {
- return getSvgIconPathContent(mockName).then((path) => {
- expect(path).toBe(null);
- });
- });
-
- it('extracts svg icon path content, after 2 attempts', () => {
- return getSvgIconPathContent(mockName)
- .then((path1) => {
- expect(path1).toBe(null);
- return getSvgIconPathContent(mockName);
- })
- .then((path2) => {
- expect(path2).toBe(null);
- return getSvgIconPathContent(mockName);
- })
- .then((path3) => {
- expect(path3).toBe(mockPath);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
index e029fa63c4c..6040fc7f2b0 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
@@ -14,7 +14,7 @@ import {
import updateDependencyProxySettings from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql';
import updateDependencyProxyImageTtlGroupPolicy from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql';
import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import {
updateGroupDependencyProxySettingsOptimisticResponse,
updateDependencyProxyImageTtlGroupPolicyOptimisticResponse,
@@ -68,7 +68,6 @@ describe('DependencyProxySettings', () => {
stubs: {
GlSprintf,
GlToggle,
- SettingsBlock,
},
});
};
@@ -82,7 +81,7 @@ describe('DependencyProxySettings', () => {
.mockResolvedValue(dependencyProxyUpdateTllPolicyMutationMock());
});
- const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findSettingsSection = () => wrapper.findComponent(SettingsSection);
const findEnableProxyToggle = () => wrapper.findByTestId('dependency-proxy-setting-toggle');
const findEnableTtlPoliciesToggle = () =>
wrapper.findByTestId('dependency-proxy-ttl-policies-toggle');
@@ -98,17 +97,17 @@ describe('DependencyProxySettings', () => {
});
};
- it('renders a settings block', () => {
+ it('renders a settings section', () => {
mountComponent();
- expect(findSettingsBlock().exists()).toBe(true);
+ expect(findSettingsSection().exists()).toBe(true);
});
it('has the correct header text and description', () => {
mountComponent();
- expect(wrapper.text()).toContain(DEPENDENCY_PROXY_HEADER);
- expect(wrapper.text()).toContain(DEPENDENCY_PROXY_DESCRIPTION);
+ expect(findSettingsSection().props('heading')).toContain(DEPENDENCY_PROXY_HEADER);
+ expect(findSettingsSection().props('description')).toContain(DEPENDENCY_PROXY_DESCRIPTION);
});
describe('enable toggle', () => {
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 91b225df779..0451eeba067 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -14,7 +14,7 @@ import {
import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
import {
packageSettings,
@@ -50,14 +50,10 @@ describe('Packages Settings', () => {
propsData: {
packageSettings,
},
- stubs: {
- SettingsBlock,
- },
});
};
- const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
- const findDescription = () => wrapper.findByTestId('description');
+ const findSettingsSection = () => wrapper.findComponent(SettingsSection);
const findMavenSettings = () => wrapper.findByTestId('maven-settings');
const findGenericSettings = () => wrapper.findByTestId('generic-settings');
const findNugetSettings = () => wrapper.findByTestId('nuget-settings');
@@ -97,19 +93,21 @@ describe('Packages Settings', () => {
it('renders a settings block', () => {
mountComponent();
- expect(findSettingsBlock().exists()).toBe(true);
+ expect(findSettingsSection().exists()).toBe(true);
});
it('has the correct header text', () => {
mountComponent();
- expect(wrapper.text()).toContain(PACKAGE_SETTINGS_HEADER);
+ expect(findSettingsSection().props('heading')).toContain(PACKAGE_SETTINGS_HEADER);
});
it('has the correct description text', () => {
mountComponent();
- expect(findDescription().text()).toMatchInterpolatedText(PACKAGE_SETTINGS_DESCRIPTION);
+ expect(findSettingsSection().props('description')).toMatchInterpolatedText(
+ PACKAGE_SETTINGS_DESCRIPTION,
+ );
});
describe('maven settings', () => {
diff --git a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
index 16934e4cf28..9f598bc2a5e 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
@@ -13,7 +13,7 @@ import {
import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
import {
packageSettings,
@@ -62,12 +62,12 @@ describe('Packages Forwarding Settings', () => {
},
stubs: {
GlSprintf,
- SettingsBlock,
+ SettingsSection,
},
});
};
- const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findSettingsBlock = () => wrapper.findComponent(SettingsSection);
const findForm = () => wrapper.find('form');
const findSubmitButton = () => findForm().findComponent(GlButton);
const findDescription = () => wrapper.findByTestId('description');
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
index 224ff0be2bd..95eecaea8e8 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
@@ -14,7 +14,7 @@ import {
UNAVAILABLE_USER_FEATURE_TEXT,
} from '~/packages_and_registries/settings/project/constants';
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import {
expirationPolicyPayload,
@@ -39,13 +39,12 @@ describe('Container expiration policy project settings', () => {
const findDescription = () => wrapper.findByTestId('description');
const findButton = () => wrapper.findByTestId('rules-button');
const findAlert = () => wrapper.findComponent(GlAlert);
- const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findSettingsBlock = () => wrapper.findComponent(SettingsSection);
const mountComponent = (provide = defaultProvidedValues, config) => {
wrapper = shallowMountExtended(component, {
stubs: {
GlSprintf,
- SettingsBlock,
},
provide,
...config,
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js
index ed9a96a2d2b..8a9c09717e5 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_protection_rules_spec.js
@@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ContainerProtectionRuleForm from '~/packages_and_registries/settings/project/components/container_protection_rule_form.vue';
import ContainerProtectionRules from '~/packages_and_registries/settings/project/components/container_protection_rules.vue';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import ContainerProtectionRuleQuery from '~/packages_and_registries/settings/project/graphql/queries/get_container_protection_rules.query.graphql';
import deleteContainerProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/delete_container_protection_rule.mutation.graphql';
import updateContainerProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_registry_protection_rule.mutation.graphql';
@@ -30,7 +30,7 @@ describe('Container protection rules project settings', () => {
const $toast = { show: jest.fn() };
- const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findSettingsBlock = () => wrapper.findComponent(SettingsSection);
const findTable = () =>
extendedWrapper(wrapper.findByRole('table', { name: /protected containers/i }));
const findTableBody = () => extendedWrapper(findTable().findAllByRole('rowgroup').at(1));
@@ -46,7 +46,7 @@ describe('Container protection rules project settings', () => {
const mountComponent = (mountFn = mountExtended, provide = defaultProvidedValues, config) => {
wrapper = mountFn(ContainerProtectionRules, {
stubs: {
- SettingsBlock,
+ SettingsSection,
GlModal: true,
},
mocks: {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
index 89d0f119e55..b39004e0e1d 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
@@ -8,7 +8,7 @@ import component from '~/packages_and_registries/settings/project/components/pac
import PackagesCleanupPolicyForm from '~/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue';
import { FETCH_SETTINGS_ERROR_MESSAGE } from '~/packages_and_registries/settings/project/constants';
import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import { packagesCleanupPolicyPayload, packagesCleanupPolicyData } from '../mock_data';
@@ -24,13 +24,12 @@ describe('Packages cleanup policy project settings', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findFormComponent = () => wrapper.findComponent(PackagesCleanupPolicyForm);
- const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findSettingsBlock = () => wrapper.findComponent(SettingsSection);
const mountComponent = (provide = defaultProvidedValues, config) => {
wrapper = shallowMount(component, {
stubs: {
GlSprintf,
- SettingsBlock,
},
provide,
...config,
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
index 3e9e00eda5d..007f7042a7b 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
@@ -7,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { getBinding } from 'helpers/vue_mock_directive';
import PackagesProtectionRules from '~/packages_and_registries/settings/project/components/packages_protection_rules.vue';
import PackagesProtectionRuleForm from '~/packages_and_registries/settings/project/components/packages_protection_rule_form.vue';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import packagesProtectionRuleQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql';
import deletePackagesProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/delete_packages_protection_rule.mutation.graphql';
import updatePackagesProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_protection_rule.mutation.graphql';
@@ -30,7 +30,7 @@ describe('Packages protection rules project settings', () => {
const $toast = { show: jest.fn() };
- const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findSettingsBlock = () => wrapper.findComponent(SettingsSection);
const findTable = () =>
extendedWrapper(wrapper.findByRole('table', { name: /protected packages/i }));
const findTableBody = () => extendedWrapper(findTable().findAllByRole('rowgroup').at(1));
@@ -46,7 +46,7 @@ describe('Packages protection rules project settings', () => {
const mountComponent = (mountFn = mountExtended, provide = defaultProvidedValues, config) => {
wrapper = mountFn(PackagesProtectionRules, {
stubs: {
- SettingsBlock,
+ SettingsSection,
GlModal: true,
},
mocks: {
diff --git a/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js b/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js
deleted file mode 100644
index 664a821c275..00000000000
--- a/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
-
-describe('SettingsBlock', () => {
- let wrapper;
-
- const mountComponent = (propsData) => {
- wrapper = shallowMountExtended(SettingsBlock, {
- propsData,
- slots: {
- title: '',
- description: '',
- default: '',
- },
- });
- };
-
- const findDefaultSlot = () => wrapper.findByTestId('default-slot');
- const findTitleSlot = () => wrapper.findByTestId('title-slot');
- const findDescriptionSlot = () => wrapper.findByTestId('description-slot');
-
- it('has a default slot', () => {
- mountComponent();
-
- expect(findDefaultSlot().exists()).toBe(true);
- });
-
- it('has a title slot', () => {
- mountComponent();
-
- expect(findTitleSlot().exists()).toBe(true);
- });
-
- it('has a description slot', () => {
- mountComponent();
-
- expect(findDescriptionSlot().exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/projects/explore/components/filtered_search_and_sort_spec.js b/spec/frontend/projects/explore/components/filtered_search_and_sort_spec.js
index 49fe509967f..3a2dd9aa0df 100644
--- a/spec/frontend/projects/explore/components/filtered_search_and_sort_spec.js
+++ b/spec/frontend/projects/explore/components/filtered_search_and_sort_spec.js
@@ -16,6 +16,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { visitUrl } from '~/lib/utils/url_utility';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
+import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -101,6 +102,7 @@ describe('ProjectsExploreFilteredSearchAndSort', () => {
});
describe('when filtered search bar is submitted', () => {
+ const { bindInternalEventDocument } = useMockInternalEventsTracking();
const searchTerm = 'foo bar';
beforeEach(() => {
@@ -117,9 +119,23 @@ describe('ProjectsExploreFilteredSearchAndSort', () => {
`?${FILTERED_SEARCH_TERM_KEY}=foo%20bar&language=5&sort=${SORT_OPTION_CREATED.value}_${SORT_DIRECTION_ASC}&archived=only`,
);
});
+
+ it('tracks event', () => {
+ const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
+
+ expect(trackEventSpy).toHaveBeenCalledWith(
+ 'use_filter_bar_projects_explore',
+ {
+ label: JSON.stringify({ search: searchTerm, language: 'CSS' }),
+ },
+ undefined,
+ );
+ });
});
describe('when sort item is changed', () => {
+ const { bindInternalEventDocument } = useMockInternalEventsTracking();
+
beforeEach(() => {
createComponent();
@@ -131,9 +147,23 @@ describe('ProjectsExploreFilteredSearchAndSort', () => {
`?archived=only&${FILTERED_SEARCH_TERM_KEY}=foo&sort=${SORT_OPTION_UPDATED.value}_${SORT_DIRECTION_ASC}`,
);
});
+
+ it('tracks event', () => {
+ const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
+
+ expect(trackEventSpy).toHaveBeenCalledWith(
+ 'use_sort_projects_explore',
+ {
+ label: `${SORT_OPTION_UPDATED.value}_${SORT_DIRECTION_ASC}`,
+ },
+ undefined,
+ );
+ });
});
describe('when sort direction is changed', () => {
+ const { bindInternalEventDocument } = useMockInternalEventsTracking();
+
beforeEach(() => {
createComponent();
@@ -145,6 +175,18 @@ describe('ProjectsExploreFilteredSearchAndSort', () => {
`?archived=only&${FILTERED_SEARCH_TERM_KEY}=foo&sort=${SORT_OPTION_CREATED.value}_${SORT_DIRECTION_DESC}`,
);
});
+
+ it('tracks event', () => {
+ const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
+
+ expect(trackEventSpy).toHaveBeenCalledWith(
+ 'use_sort_projects_explore',
+ {
+ label: `${SORT_OPTION_CREATED.value}_${SORT_DIRECTION_DESC}`,
+ },
+ undefined,
+ );
+ });
});
describe('when on the "Most starred" tab', () => {
diff --git a/spec/frontend/work_items/components/work_item_development/work_item_development_spec.js b/spec/frontend/work_items/components/work_item_development/work_item_development_spec.js
index 5fed8ef2a3a..3392b4b2a80 100644
--- a/spec/frontend/work_items/components/work_item_development/work_item_development_spec.js
+++ b/spec/frontend/work_items/components/work_item_development/work_item_development_spec.js
@@ -6,10 +6,12 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective } from 'helpers/vue_mock_directive';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
+import { STATE_CLOSED, STATE_OPEN } from '~/work_items/constants';
import {
workItemResponseFactory,
workItemDevelopmentFragmentResponse,
+ workItemDevelopmentNodes,
} from 'jest/work_items/mock_data';
import WorkItemDevelopment from '~/work_items/components/work_item_development/work_item_development.vue';
@@ -22,6 +24,15 @@ describe('WorkItemDevelopment CE', () => {
let mockApollo;
const workItem = workItemResponseFactory({ developmentWidgetPresent: true });
+ const workItemWithOneMR = workItemResponseFactory({
+ developmentWidgetPresent: true,
+ developmentItems: workItemDevelopmentFragmentResponse([workItemDevelopmentNodes[0]], true),
+ });
+ const workItemWithMRList = workItemResponseFactory({
+ developmentWidgetPresent: true,
+ developmentItems: workItemDevelopmentFragmentResponse(workItemDevelopmentNodes, true),
+ });
+
const projectWorkItemResponseWithMRList = {
data: {
workspace: {
@@ -31,11 +42,50 @@ describe('WorkItemDevelopment CE', () => {
},
},
};
+
+ const closedWorkItemWithAutoCloseFlagEnabled = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ workItem: {
+ ...workItemWithMRList.data.workItem,
+ state: STATE_CLOSED,
+ },
+ },
+ },
+ };
+
+ const openWorkItemWithAutoCloseFlagEnabledAndOneMR = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ workItem: workItemWithOneMR.data.workItem,
+ },
+ },
+ };
+
+ const openWorkItemWithAutoCloseFlagEnabledAndMRList = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ workItem: workItemWithMRList.data.workItem,
+ },
+ },
+ };
+
const successQueryHandler = jest.fn().mockResolvedValue(projectWorkItemResponseWithMRList);
const workItemWithEmptyMRList = workItemResponseFactory({
developmentWidgetPresent: true,
developmentItems: workItemDevelopmentFragmentResponse([]),
});
+ const workItemWithAutoCloseFlagEnabled = workItemResponseFactory({
+ developmentWidgetPresent: true,
+ developmentItems: workItemDevelopmentFragmentResponse(workItemDevelopmentNodes, true),
+ });
+
const successQueryHandlerWithEmptyMRList = jest.fn().mockResolvedValue({
data: {
workspace: {
@@ -46,6 +96,26 @@ describe('WorkItemDevelopment CE', () => {
},
});
+ const successQueryHandlerWorkItemWithAutoCloseFlagEnabled = jest.fn().mockResolvedValue({
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ workItem: workItemWithAutoCloseFlagEnabled.data.workItem,
+ },
+ },
+ });
+
+ const successQueryHandlerWithOneMR = jest
+ .fn()
+ .mockResolvedValue(openWorkItemWithAutoCloseFlagEnabledAndOneMR);
+ const successQueryHandlerWithMRList = jest
+ .fn()
+ .mockResolvedValue(openWorkItemWithAutoCloseFlagEnabledAndMRList);
+ const successQueryHandlerWithClosedWorkItem = jest
+ .fn()
+ .mockResolvedValue(closedWorkItemWithAutoCloseFlagEnabled);
+
const createComponent = ({
isGroup = false,
canUpdate = true,
@@ -78,6 +148,7 @@ describe('WorkItemDevelopment CE', () => {
const findAddMoreIcon = () => wrapper.findComponent(GlIcon);
const findCreateMRButton = () => wrapper.findByTestId('create-mr-button');
const findCreateBranchButton = () => wrapper.findByTestId('create-branch-button');
+ const findMoreInformation = () => wrapper.findByTestId('more-information');
const findRelationshipList = () => wrapper.findComponent(WorkItemDevelopmentRelationshipList);
describe('Default', () => {
@@ -134,5 +205,40 @@ describe('WorkItemDevelopment CE', () => {
expect(findRelationshipList().exists()).toBe(true);
});
});
+
+ it('when auto close flag is disabled, should not show the "i" indicator', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findMoreInformation().exists()).toBe(false);
+ });
+
+ it('when auto close flag is enabled, should show the "i" indicator', async () => {
+ createComponent({
+ workItemQueryHandler: successQueryHandlerWorkItemWithAutoCloseFlagEnabled,
+ });
+
+ await waitForPromises();
+
+ expect(findMoreInformation().exists()).toBe(true);
+ });
+
+ it.each`
+ queryHandler | message | workItemState | linkedMRsNumber
+ ${successQueryHandlerWithOneMR} | ${'This task will be closed when the following is merged.'} | ${STATE_OPEN} | ${1}
+ ${successQueryHandlerWithMRList} | ${'This task will be closed when any of the following is merged.'} | ${STATE_OPEN} | ${workItemDevelopmentNodes.length}
+ ${successQueryHandlerWithClosedWorkItem} | ${'The task was closed automatically when a branch was merged.'} | ${STATE_CLOSED} | ${workItemDevelopmentNodes.length}
+ `(
+ 'when the workItemState is `$workItemState` and number of linked MRs is `$linkedMRsNumber` shows message `$message`',
+ async ({ queryHandler, message }) => {
+ createComponent({
+ workItemQueryHandler: queryHandler,
+ });
+
+ await waitForPromises();
+
+ expect(findMoreInformation().attributes('aria-label')).toBe(message);
+ },
+ );
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index c73311770b1..a4f279c0cc4 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -871,9 +871,13 @@ export const workItemDevelopmentNodes = [
},
];
-export const workItemDevelopmentFragmentResponse = (nodes = workItemDevelopmentNodes) => {
+export const workItemDevelopmentFragmentResponse = (
+ nodes = workItemDevelopmentNodes,
+ willAutoCloseByMergeRequest = false,
+) => {
return {
type: 'DEVELOPMENT',
+ willAutoCloseByMergeRequest,
closingMergeRequests: {
nodes,
__typename: 'WorkItemClosingMergeRequestConnection',
diff --git a/spec/graphql/types/ci/pipeline_manual_variable_type_spec.rb b/spec/graphql/types/ci/pipeline_manual_variable_type_spec.rb
new file mode 100644
index 00000000000..fe72a7c4601
--- /dev/null
+++ b/spec/graphql/types/ci/pipeline_manual_variable_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::PipelineManualVariableType, feature_category: :continuous_integration do
+ specify { expect(described_class.graphql_name).to eq('PipelineManualVariable') }
+
+ it 'contains attributes related to a variable' do
+ expected_fields = %w[id key value]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index e3d52dac458..0e52e2889fa 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -33,6 +33,7 @@ RSpec.describe Types::Ci::PipelineType, feature_category: :continuous_integratio
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'KEY_1', value: 'VALUE_1') }
let(:query) do
%(
{
@@ -44,6 +45,7 @@ RSpec.describe Types::Ci::PipelineType, feature_category: :continuous_integratio
startedAt
manualVariables {
nodes {
+ id
key
value
}
@@ -60,14 +62,16 @@ RSpec.describe Types::Ci::PipelineType, feature_category: :continuous_integratio
before do
project.add_role(user, user_access_level) # rubocop:disable RSpec/BeforeAllRoleAssignment -- need dynamic settings `user_access_level`
- create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
end
context 'when user has access to read variables' do
let(:user_access_level) { :owner }
it 'returns the manual variables' do
- expect(manual_variables).to match_array([{ 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1' }])
+ expect(manual_variables.size).to eq(1)
+ expect(manual_variables.first['key']).to eq(variable.key)
+ expect(manual_variables.first['value']).to eq(variable.value)
+ expect(manual_variables.first.keys).to match_array(%w[id key value])
end
end
@@ -75,7 +79,10 @@ RSpec.describe Types::Ci::PipelineType, feature_category: :continuous_integratio
let(:user_access_level) { :developer }
it 'returns the manual variables with nil values' do
- expect(manual_variables).to match_array([{ 'key' => 'TRIGGER_KEY_1', 'value' => nil }])
+ expect(manual_variables.size).to eq(1)
+ expect(manual_variables.first['key']).to eq(variable.key)
+ expect(manual_variables.first['value']).to eq(nil)
+ expect(manual_variables.first.keys).to match_array(%w[id key value])
end
end
end
diff --git a/spec/helpers/pagination_helper_spec.rb b/spec/helpers/pagination_helper_spec.rb
index 4cd6b6f3922..257379780ff 100644
--- a/spec/helpers/pagination_helper_spec.rb
+++ b/spec/helpers/pagination_helper_spec.rb
@@ -10,16 +10,40 @@ RSpec.describe PaginationHelper do
without_count = collection.without_count
expect(helper).to receive(:paginate_without_count)
- .with(without_count)
+ .with(without_count, event_tracking: 'foo_bar')
.and_call_original
- helper.paginate_collection(without_count)
+ helper.paginate_collection(without_count, event_tracking: 'foo_bar')
end
it 'paginates a collection using a COUNT' do
- expect(helper).to receive(:paginate_with_count).and_call_original
+ expect(helper).to receive(:paginate_with_count)
+ .with(collection, remote: nil, total_pages: nil)
+ .and_call_original
- helper.paginate_collection(collection)
+ helper.paginate_collection(collection, event_tracking: 'foo_bar')
+ end
+ end
+
+ describe '#paginate_event_tracking_data_attributes' do
+ context 'when event_tracking argument is nil' do
+ it 'returns an empty object' do
+ expect(helper.paginate_event_tracking_data_attributes).to eq({})
+ end
+ end
+
+ context 'when event tracking argument is set' do
+ it 'returns event tracking data attributes' do
+ expect(
+ helper.paginate_event_tracking_data_attributes(
+ event_tracking: 'foo_bar',
+ event_label: 'baz'
+ )
+ ).to eq({
+ event_tracking: 'foo_bar',
+ event_label: 'baz'
+ })
+ end
end
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_epic_issues_namespace_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_epic_issues_namespace_id_spec.rb
new file mode 100644
index 00000000000..90cfc4d51c5
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_epic_issues_namespace_id_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillEpicIssuesNamespaceId,
+ feature_category: :portfolio_management,
+ schema: 20240618123925 do
+ include_examples 'desired sharding key backfill job' do
+ let(:batch_table) { :epic_issues }
+ let(:backfill_column) { :namespace_id }
+ let(:backfill_via_table) { :issues }
+ let(:backfill_via_column) { :namespace_id }
+ let(:backfill_via_foreign_key) { :issue_id }
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index 3b250686a49..8c942d440fe 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -133,6 +133,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeli
end
end
+ context 'when run: and trigger: are used together' do
+ let(:config) do
+ {
+ run: [{ name: 'step1', step: 'some reference' }],
+ trigger: 'test-group/test-project'
+ }
+ end
+
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include(/these keys cannot be used together: run, trigger/)
+ end
+ end
+
context 'when only: is used with rules:' do
let(:config) { { only: ['merge_requests'], rules: [{ if: '$THIS' }] } }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index f838b4cbd02..6cd46c7f69b 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -878,6 +878,10 @@ project:
- security_policy_project_linked_namespaces
- relation_import_trackers
- saved_replies
+- security_policy_project_links
+- security_policies
+- approval_policy_rule_project_links
+- approval_policy_rules
award_emoji:
- awardable
- user
@@ -1134,6 +1138,7 @@ approval_rules:
- approval_project_rules_users
- approval_project_rules_protected_branches
- scan_result_policy_read
+ - approval_policy_rules
approval_project_rules_users:
- user
- approval_project_rule
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 445682f68b5..f1728085c01 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -1012,6 +1012,7 @@ Epic:
EpicIssue:
- id
- relative_position
+ - namespace_id
SystemNoteMetadata:
- id
- commit_count
diff --git a/spec/migrations/20240618123929_queue_backfill_epic_issues_namespace_id_spec.rb b/spec/migrations/20240618123929_queue_backfill_epic_issues_namespace_id_spec.rb
new file mode 100644
index 00000000000..b909283efa7
--- /dev/null
+++ b/spec/migrations/20240618123929_queue_backfill_epic_issues_namespace_id_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillEpicIssuesNamespaceId, feature_category: :portfolio_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :epic_issues,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE,
+ gitlab_schema: :gitlab_main_cell,
+ job_arguments: [
+ :namespace_id,
+ :issues,
+ :namespace_id,
+ :issue_id
+ ]
+ )
+ }
+ end
+ end
+end
diff --git a/spec/services/ci/catalog/resources/versions/build_components_service_spec.rb b/spec/services/ci/catalog/resources/versions/build_components_service_spec.rb
new file mode 100644
index 00000000000..697825174fa
--- /dev/null
+++ b/spec/services/ci/catalog/resources/versions/build_components_service_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::Versions::BuildComponentsService, feature_category: :pipeline_composition do
+ describe '#execute' do
+ let(:files) do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1",
+ 'templates/dast/template.yml' => 'image: alpine_2',
+ 'templates/blank-yaml.yml' => '',
+ 'templates/dast/sub-folder/template.yml' => 'image: alpine_3',
+ 'templates/template.yml' => "spec:\n inputs:\n environment:\n---\nimage: alpine_6",
+ 'tests/test.yml' => 'image: alpine_7',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ let(:project) do
+ create(
+ :project, :custom_repo,
+ description: 'Simple and Complex components',
+ files: files
+ )
+ end
+
+ let(:release) { create(:release, tag: '1.2.0', project: project, sha: project.repository.root_ref_sha) }
+ let!(:catalog_resource) { create(:ci_catalog_resource, project: project) }
+ let(:version) { create(:ci_catalog_resource_version, release: release, catalog_resource: catalog_resource) }
+
+ subject(:execute) { described_class.new(release, version).execute }
+
+ it 'builds components for a release version' do
+ expect(execute).to be_success
+
+ components = execute.payload
+
+ expect(components.size).to eq(4)
+ expect(components.map(&:name)).to contain_exactly('blank-yaml', 'dast', 'secret-detection', 'template')
+ expect(components.map(&:spec)).to contain_exactly(
+ {},
+ {},
+ { 'inputs' => { 'website' => nil } },
+ { 'inputs' => { 'environment' => nil } }
+ )
+ end
+
+ context 'when there are more than 30 components' do
+ let(:files) do
+ num_components = 31
+ components = (0..num_components).map { |i| "templates/secret#{i}.yml" }
+ components << 'README.md'
+
+ components.index_with { |_file| '' }
+ end
+
+ it 'raises an error' do
+ response = execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Release cannot contain more than 30 components')
+ end
+ end
+
+ context 'with invalid data' do
+ let_it_be(:files) do
+ {
+ 'templates/secret-detection.yml' => 'some: invalid: syntax',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'returns an error' do
+ response = execute
+
+ expect(response).to be_error
+ expect(response.message).to include('mapping values are not allowed in this context')
+ end
+ end
+
+ context 'when one or more components are invalid' do
+ let_it_be(:files) do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n - website\n---\nimage: alpine_1",
+ 'README.md' => 'Read me'
+ }
+ end
+
+ it 'returns an error' do
+ response = execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Spec must be a valid json schema')
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/catalog/resources/versions/create_service_spec.rb b/spec/services/ci/catalog/resources/versions/create_service_spec.rb
index 98606298d99..eff593d83d3 100644
--- a/spec/services/ci/catalog/resources/versions/create_service_spec.rb
+++ b/spec/services/ci/catalog/resources/versions/create_service_spec.rb
@@ -27,12 +27,13 @@ RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category
let(:release) { create(:release, tag: '1.2.0', project: project, sha: project.repository.root_ref_sha) }
let!(:catalog_resource) { create(:ci_catalog_resource, project: project) }
- context 'when the project is not a catalog resource' do
- it 'does not create a version' do
- project = create(:project, :repository)
- release = create(:release, tag: '1.2.1', project: project, sha: project.repository.root_ref_sha)
+ subject(:execute) { described_class.new(release).execute }
- response = described_class.new(release).execute
+ context 'when the project is not a catalog resource' do
+ let(:release) { create(:release, tag: '1.2.1') }
+
+ it 'does not create a version' do
+ response = execute
expect(response).to be_error
expect(response.message).to include('Project is not a catalog resource')
@@ -40,8 +41,8 @@ RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category
end
context 'when the catalog resource has different types of components and a release' do
- it 'creates a version for the release' do
- response = described_class.new(release).execute
+ it 'creates a version for the release and marks the catalog resource as published' do
+ response = execute
expect(response).to be_success
@@ -51,58 +52,18 @@ RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category
expect(version.semver.to_s).to eq(release.tag)
expect(version.catalog_resource).to eq(catalog_resource)
expect(version.catalog_resource.project).to eq(project)
- end
-
- it 'marks the catalog resource as published' do
- described_class.new(release).execute
-
expect(catalog_resource.reload.state).to eq('published')
end
- context 'when there are at max 30 components' do
- let(:files) do
- num_components = 30
- components = (0...num_components).map { |i| "templates/secret#{i}.yml" }
- components << 'README.md'
-
- components.index_with { |_file| '' }
- end
-
- it 'creates the components' do
- response = described_class.new(release).execute
-
- expect(response).to be_success
- expect(project.ci_components.count).to eq(30)
- end
- end
-
- context 'when there are more than 30 components' do
- let(:files) do
- num_components = 31
- components = (0..num_components).map { |i| "templates/secret#{i}.yml" }
- components << 'README.md'
-
- components.index_with { |_file| '' }
- end
-
- it 'raises an error' do
- response = described_class.new(release).execute
-
- expect(response).to be_error
- expect(response.message).to include('Release cannot contain more than 30 components')
- expect(project.ci_components.count).to eq(0)
- end
- end
-
it 'bulk inserts all the components' do
expect(Ci::Catalog::Resources::Component).to receive(:bulk_insert!).and_call_original
- described_class.new(release).execute
+ execute
end
it 'creates components for the catalog resource' do
expect(project.ci_components.count).to eq(0)
- response = described_class.new(release).execute
+ response = execute
expect(response).to be_success
@@ -132,6 +93,41 @@ RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category
end
end
+ context 'when there are at max 30 components' do
+ let(:files) do
+ num_components = 30
+ components = (0...num_components).map { |i| "templates/secret#{i}.yml" }
+ components << 'README.md'
+
+ components.index_with { |_file| '' }
+ end
+
+ it 'creates the components' do
+ response = execute
+
+ expect(response).to be_success
+ expect(project.ci_components.count).to eq(30)
+ end
+ end
+
+ context 'when there are more than 30 components' do
+ let(:files) do
+ num_components = 31
+ components = (0..num_components).map { |i| "templates/secret#{i}.yml" }
+ components << 'README.md'
+
+ components.index_with { |_file| '' }
+ end
+
+ it 'raises an error' do
+ response = execute
+
+ expect(response).to be_error
+ expect(response.message).to include('Release cannot contain more than 30 components')
+ expect(project.ci_components.count).to eq(0)
+ end
+ end
+
context 'with invalid data' do
let_it_be(:files) do
{
@@ -141,7 +137,7 @@ RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category
end
it 'returns an error' do
- response = described_class.new(release).execute
+ response = execute
expect(response).to be_error
expect(response.message).to include('mapping values are not allowed in this context')
@@ -157,7 +153,7 @@ RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category
end
it 'returns an error' do
- response = described_class.new(release).execute
+ response = execute
expect(response).to be_error
expect(response.message).to include('Spec must be a valid json schema')
diff --git a/yarn.lock b/yarn.lock
index 9d8b5446686..f3770ee032e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13015,16 +13015,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -13076,7 +13067,7 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -13090,13 +13081,6 @@ strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -14783,7 +14767,7 @@ worker-loader@^3.0.8:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -14801,15 +14785,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"