Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-05-22 15:18:20 +00:00
parent a075011d5b
commit 13e210a5d6
146 changed files with 3457 additions and 1786 deletions

View File

@ -978,14 +978,18 @@ Gitlab/BoundedContexts:
- 'app/models/board_project_recent_visit.rb'
- 'app/models/bulk_import.rb'
- 'app/models/bulk_imports/batch_tracker.rb'
- 'app/models/bulk_imports/configuration.rb'
- 'app/models/bulk_imports/entity.rb'
- 'app/models/bulk_imports/export.rb'
- 'app/models/bulk_imports/export_batch.rb'
- 'app/models/bulk_imports/export_status.rb'
- 'app/models/bulk_imports/export_upload.rb'
- 'app/models/bulk_imports/failure.rb'
- 'app/models/bulk_imports/file_transfer.rb'
- 'app/models/bulk_imports/file_transfer/base_config.rb'
- 'app/models/bulk_imports/file_transfer/group_config.rb'
- 'app/models/bulk_imports/file_transfer/project_config.rb'
- 'app/models/bulk_imports/tracker.rb'
- 'app/models/chat_name.rb'
- 'app/models/chat_team.rb'
- 'app/models/ci_platform_metric.rb'

View File

@ -1861,7 +1861,6 @@ Style/InlineDisableAnnotation:
- 'ee/spec/controllers/concerns/gitlab_subscriptions/seat_count_alert_spec.rb'
- 'ee/spec/controllers/concerns/routable_actions_spec.rb'
- 'ee/spec/controllers/projects/settings/merge_requests_controller_spec.rb'
- 'ee/spec/elastic/migrate/20230503064300_backfill_project_permissions_in_blobs_using_permutations_spec.rb'
- 'ee/spec/factories/package_metadata/pm_licenses.rb'
- 'ee/spec/factories/security_scans.rb'
- 'ee/spec/features/dashboards/todos_spec.rb'

View File

@ -39,12 +39,9 @@ export default {
<filtered-search-bar />
<gl-empty-state v-if="abuseReports.length == 0" :title="s__('AbuseReports|No reports found')" />
<abuse-report-row
v-for="(report, index) in abuseReports"
v-else
:key="index"
:report="report"
/>
<ul v-else class="gl-pl-0">
<abuse-report-row v-for="(report, index) in abuseReports" :key="index" :report="report" />
</ul>
<gl-pagination
v-if="showPagination"

View File

@ -8,7 +8,7 @@ import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { formType } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isMetaKey } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { updateHistory, visitUrl } from '~/lib/utils/url_utility';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__, __ } from '~/locale';
@ -243,7 +243,7 @@ export default {
async switchBoardKeyEvent(boardId, e) {
if (isMetaKey(e)) {
e.stopPropagation();
window.open(`${this.boardBaseUrl}/${boardId}`, '_blank');
visitUrl(`${this.boardBaseUrl}/${boardId}`, true);
}
},
switchBoardGroup(value) {

View File

@ -160,6 +160,8 @@ export default {
pinnedFileStatus: '',
codequalityData: {},
sastData: {},
keydownTime: undefined,
listenersAttached: false,
};
},
apollo: {
@ -234,6 +236,7 @@ export default {
'showWhitespace',
'targetBranchName',
'branchName',
'showTreeList',
]),
...mapGetters('diffs', [
'whichCollapsedTypes',
@ -303,6 +306,9 @@ export default {
resourceId() {
return convertToGraphQLId('MergeRequest', this.getNoteableData.id);
},
renderFileTree() {
return this.renderDiffFiles && this.showTreeList;
},
},
watch: {
commit(newCommit, oldCommit) {
@ -337,6 +343,7 @@ export default {
this.adjustView();
this.subscribeToVirtualScrollingEvents();
},
renderFileTree: 'adjustView',
isLoading: 'adjustView',
},
mounted() {
@ -432,6 +439,7 @@ export default {
'setFileByFile',
'disableVirtualScroller',
'fetchPinnedFile',
'toggleTreeList',
]),
...mapActions('findingsDrawer', ['setDrawer']),
closeDrawer() {
@ -607,6 +615,8 @@ export default {
}
},
setEventListeners() {
if (this.listenersAttached) return;
Mousetrap.bind(keysFor(MR_PREVIOUS_FILE_IN_DIFF), () => this.jumpToFile(-1));
Mousetrap.bind(keysFor(MR_NEXT_FILE_IN_DIFF), () => this.jumpToFile(+1));
@ -619,32 +629,36 @@ export default {
);
}
let keydownTime;
Mousetrap.bind(['mod+f', 'mod+g'], () => {
keydownTime = new Date().getTime();
this.keydownTime = new Date().getTime();
});
window.addEventListener('blur', () => {
if (keydownTime) {
const delta = new Date().getTime() - keydownTime;
window.addEventListener('blur', this.handleBrowserFindActivation);
// To make sure the user is using the find function we need to wait for blur
// and max 1000ms to be sure it the search box is filtered
if (delta >= 0 && delta < 1000) {
this.disableVirtualScroller();
api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
api.trackRedisCounterEvent('diff_searches');
}
}
});
this.listenersAttached = true;
},
removeEventListeners() {
Mousetrap.unbind(keysFor(MR_PREVIOUS_FILE_IN_DIFF));
Mousetrap.unbind(keysFor(MR_NEXT_FILE_IN_DIFF));
Mousetrap.unbind(keysFor(MR_COMMITS_NEXT_COMMIT));
Mousetrap.unbind(keysFor(MR_COMMITS_PREVIOUS_COMMIT));
Mousetrap.unbind(['ctrl+f', 'command+f']);
Mousetrap.unbind(['ctrl+f', 'command+f', 'mod+f', 'mod+g']);
window.removeEventListener('blur', this.handleBrowserFindActivation);
this.listenersAttached = false;
},
handleBrowserFindActivation() {
if (!this.keydownTime) return;
const delta = new Date().getTime() - this.keydownTime;
// To make sure the user is using the find function we need to wait for blur
// and max 1000ms to be sure it the search box is filtered
if (delta >= 0 && delta < 1000) {
this.disableVirtualScroller();
api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
api.trackRedisCounterEvent('diff_searches');
}
},
jumpToFile(step) {
const targetIndex = this.currentDiffIndex + step;
@ -709,6 +723,10 @@ export default {
this.trackEvent(types[event.name]);
}
},
fileTreeToggled() {
this.toggleTreeList();
this.adjustView();
},
},
howToMergeDocsPath: helpPagePath('user/project/merge_requests/merge_request_troubleshooting.md', {
anchor: 'check-out-merge-requests-locally-through-the-head-ref',
@ -738,7 +756,7 @@ export default {
:data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex gl-mt-2"
>
<diffs-file-tree :render-diff-files="renderDiffFiles" @toggled="adjustView" />
<diffs-file-tree :visible="renderFileTree" @toggled="fileTreeToggled" />
<div class="col-12 col-md-auto diff-files-holder">
<commit-widget v-if="commit" :commit="commit" :collapsible="false" />
<gl-alert

View File

@ -1,6 +1,4 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
import { Mousetrap } from '~/lib/mousetrap';
import { keysFor, MR_TOGGLE_FILE_BROWSER } from '~/behaviors/shortcuts/keybindings';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
@ -18,7 +16,7 @@ export default {
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: window.innerWidth / 2,
props: {
renderDiffFiles: {
visible: {
type: Boolean,
required: true,
},
@ -32,33 +30,29 @@ export default {
};
},
computed: {
...mapState('diffs', ['showTreeList']),
renderFileTree() {
return this.renderDiffFiles && this.showTreeList;
},
hideFileStats() {
return this.treeWidth <= TREE_HIDE_STATS_WIDTH;
},
},
watch: {
renderFileTree() {
this.$emit('toggled');
},
},
mounted() {
Mousetrap.bind(keysFor(MR_TOGGLE_FILE_BROWSER), this.toggleTreeList);
Mousetrap.bind(keysFor(MR_TOGGLE_FILE_BROWSER), this.toggle);
},
beforeDestroy() {
Mousetrap.unbind(keysFor(MR_TOGGLE_FILE_BROWSER), this.toggleTreeList);
Mousetrap.unbind(keysFor(MR_TOGGLE_FILE_BROWSER), this.toggle);
},
methods: {
...mapActions('diffs', ['cacheTreeListWidth', 'toggleTreeList']),
toggle() {
this.$emit('toggled');
},
cacheTreeListWidth(size) {
localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
},
},
};
</script>
<template>
<div v-if="renderFileTree" :style="{ width: `${treeWidth}px` }" class="diff-tree-list gl-px-5">
<div v-if="visible" :style="{ width: `${treeWidth}px` }" class="diff-tree-list gl-px-5">
<panel-resizer
:size.sync="treeWidth"
:start-size="treeWidth"

View File

@ -21,7 +21,6 @@ import {
DIFF_VIEW_COOKIE_NAME,
MR_TREE_SHOW_KEY,
TREE_LIST_STORAGE_KEY,
TREE_LIST_WIDTH_STORAGE_KEY,
OLD_LINE_KEY,
NEW_LINE_KEY,
TYPE_KEY,
@ -759,10 +758,6 @@ export const toggleFileFinder = ({ commit }, visible) => {
commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible);
};
export const cacheTreeListWidth = (_, size) => {
localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
};
export const receiveFullDiffError = ({ commit }, filePath) => {
commit(types.RECEIVE_FULL_DIFF_ERROR, filePath);
createAlert({

View File

@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { createAlert } from '~/alert';
import { getCookie, isMetaClick, parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
import { parseUrlPathname } from '~/lib/utils/url_utility';
import { parseUrlPathname, visitUrl } from '~/lib/utils/url_utility';
import createEventHub from '~/helpers/event_hub_factory';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
@ -282,7 +282,7 @@ export default class MergeRequestTabs {
if (isMetaClick(e)) {
const targetLink = e.currentTarget.getAttribute('href');
window.open(targetLink, '_blank');
visitUrl(targetLink, true);
} else if (action) {
const href = e.currentTarget.getAttribute('href');
this.tabShown(action, href);

View File

@ -2,6 +2,7 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import Raphael from './raphael';
@ -238,7 +239,7 @@ export default class BranchGraph {
opacity: 0,
cursor: 'pointer',
})
.click(() => window.open(options.commit_url.replace('%s', commit.id), '_blank'))
.click(() => visitUrl(options.commit_url.replace('%s', commit.id), true))
.hover(
function () {
this.tooltip = r.commitTooltip(x + 5, y, commit);

View File

@ -16,6 +16,16 @@ export default {
required: false,
default: null,
},
maxDateRange: {
type: Number,
required: false,
default: null,
},
dateOptions: {
type: Array,
required: false,
default: () => TIME_RANGE_OPTIONS,
},
},
data() {
return {
@ -28,7 +38,7 @@ export default {
},
computed: {
dateRangeOptions() {
return TIME_RANGE_OPTIONS.map((option) => {
return this.dateOptions.map((option) => {
const dateRange = periodToDate(option.value);
return {
value: option.value,
@ -95,6 +105,7 @@ export default {
:default-start-date="dateRange.startDate"
:default-end-date="dateRange.endDate"
:default-max-date="defaultMaxDate"
:max-date-range="maxDateRange"
@input="onCustomRangeSelected"
/>
</div>

View File

@ -26,7 +26,7 @@ export default {
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column">
<ul class="gl-pl-0">
<image-list-row
v-for="(listItem, index) in images"
:key="index"
@ -35,5 +35,5 @@ export default {
:expiration-policy="expirationPolicy"
@delete="$emit('delete', $event)"
/>
</div>
</ul>
</template>

View File

@ -53,14 +53,14 @@ export default {
<manifests-empty-state v-else-if="manifests.length === 0" />
<div v-else data-testid="main-area">
<div class="gl-display-flex gl-flex-direction-column">
<ul class="gl-pl-0">
<manifest-row
v-for="(manifest, index) in manifests"
:key="index"
:dependency-proxy-image-prefix="dependencyProxyImagePrefix"
:manifest="manifest"
/>
</div>
</ul>
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
v-bind="pagination"

View File

@ -188,17 +188,19 @@ export default {
</template>
<template v-else-if="hasVersions">
<package-list-row
v-for="v in packageEntity.versions"
:key="v.id"
:package-entity="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
name: packageEntity.name,
...v,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:package-link="v.id.toString()"
:disable-delete="true"
:show-package-type="false"
/>
<ul class="gl-pl-0">
<package-list-row
v-for="v in packageEntity.versions"
:key="v.id"
:package-entity="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
name: packageEntity.name,
...v,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:package-link="v.id.toString()"
:disable-delete="true"
:show-package-type="false"
/>
</ul>
</template>
<p v-else class="gl-mt-3" data-testid="no-versions-message">

View File

@ -75,7 +75,7 @@ export default {
</div>
<template v-else>
<div data-testid="packages-table">
<ul data-testid="packages-table" class="gl-pl-0">
<packages-list-row
v-for="packageEntity in list"
:key="packageEntity.id"
@ -84,7 +84,7 @@ export default {
:is-group="isGroupPage"
@packageToDelete="setItemToBeDeleted"
/>
</div>
</ul>
<gl-pagination
v-model="currentPage"

View File

@ -117,14 +117,14 @@ export default {
</gl-button>
</div>
<div v-for="(item, index) in items" :key="index">
<ul v-for="(item, index) in items" :key="index" class="gl-pl-0">
<slot
:select-item="selectItem"
:is-selected="isSelected"
:item="item"
:first="!hiddenDelete && index === 0"
></slot>
</div>
</ul>
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination

View File

@ -77,3 +77,5 @@ export const REQUIRED_ICON_CLASS = 'gl-fill-green-500';
export const NOT_REQUIRED_ICON_CLASS = 'gl-text-red-500';
export const DELETE_RULE_MODAL_ID = 'delete-branch-rule-modal';
export const projectUsersOptions = { push_code: true, active: true };

View File

@ -22,11 +22,12 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import branchRulesQuery from 'ee_else_ce/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
import { createAlert } from '~/alert';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import editBranchRuleMutation from 'ee_else_ce/projects/settings/branch_rules/mutations/edit_branch_rule.mutation.graphql';
import deleteBranchRuleMutation from '../../mutations/branch_rule_delete.mutation.graphql';
import editBranchRuleMutation from '../../mutations/edit_branch_rule.mutation.graphql';
import { getAccessLevels } from '../../../utils';
import BranchRuleModal from '../../../components/branch_rule_modal.vue';
import Protection from './protection.vue';
import RuleDrawer from './rule_drawer.vue';
import {
I18N,
ALL_BRANCHES_WILDCARD,
@ -61,6 +62,7 @@ export default {
GlModal,
GlButton,
BranchRuleModal,
RuleDrawer,
},
mixins: [glFeatureFlagsMixin()],
inject: {
@ -115,6 +117,8 @@ export default {
statusChecks: [],
branchRule: {},
matchingBranchesCount: null,
isAllowedToMergeDrawerOpen: false,
isRuleUpdating: false,
};
},
computed: {
@ -214,18 +218,39 @@ export default {
});
});
},
editBranchRule({ name }) {
openAllowedToMergeDrawer() {
this.isAllowedToMergeDrawerOpen = true;
},
closeAllowedToMergeDrawer() {
this.isAllowedToMergeDrawerOpen = false;
},
editBranchRule({ name = this.branchRule.name, branchProtection = null, toastMessage = '' }) {
this.isRuleUpdating = true;
this.$apollo
.mutate({
mutation: editBranchRuleMutation,
variables: {
id: this.branchRule.id,
name,
input: {
id: this.branchRule.id,
name,
...(branchProtection && { branchProtection }),
},
},
})
.then(visitUrl(setUrlParams({ branch: name })))
.then(() => {
const isRedirectNeeded = !branchProtection;
if (isRedirectNeeded) {
visitUrl(setUrlParams({ branch: name }));
} else {
this.closeAllowedToMergeDrawer();
this.$toast.show(toastMessage);
}
})
.catch(() => {
createAlert({ message: this.$options.i18n.updateBranchRuleError });
})
.finally(() => {
this.isRuleUpdating = false;
});
},
},
@ -260,7 +285,7 @@ export default {
<gl-button
v-if="glFeatures.editBranchRules && !isPredefinedRule"
v-gl-modal="$options.editModalId"
data-testid="edit-rule-button"
data-testid="edit-rule-name-button"
size="small"
>{{ $options.i18n.edit }}</gl-button
>
@ -283,7 +308,6 @@ export default {
</gl-link>
</template>
</gl-sprintf>
<!-- Allowed to push -->
<protection
class="gl-mt-3"
@ -309,6 +333,23 @@ export default {
:empty-state-copy="$options.i18n.allowedToMergeEmptyState"
is-edit-available
data-testid="allowed-to-merge-content"
@edit="openAllowedToMergeDrawer"
/>
<rule-drawer
:is-open="isAllowedToMergeDrawerOpen"
:roles="mergeAccessLevels.roles"
:users="mergeAccessLevels.users"
:groups="mergeAccessLevels.groups"
:is-loading="isRuleUpdating"
:title="s__('BranchRules|Edit allowed to merge')"
@editRule="
editBranchRule({
branchProtection: { mergeAccessLevels: $event },
toastMessage: s__('BranchRules|Allowed to merge updated'),
})
"
@close="closeAllowedToMergeDrawer"
/>
<!-- Force push -->

View File

@ -100,7 +100,7 @@ export default {
<gl-button
v-if="glFeatures.editBranchRules && isEditAvailable"
size="small"
data-testid="edit-button"
data-testid="edit-rule-button"
@click="$emit('edit')"
>{{ __('Edit') }}</gl-button
>

View File

@ -0,0 +1,117 @@
<script>
import { GlDrawer, GlButton, GlFormGroup } from '@gitlab/ui';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { projectUsersOptions } from './constants';
export default {
DRAWER_Z_INDEX,
projectUsersOptions,
components: {
GlDrawer,
GlButton,
GlFormGroup,
ItemsSelector: () =>
import('ee_component/projects/settings/branch_rules/components/view/items_selector.vue'),
},
props: {
isOpen: {
type: Boolean,
required: true,
},
users: {
type: Array,
required: true,
},
groups: {
type: Array,
required: true,
},
roles: {
type: Array,
required: true,
},
title: {
type: String,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
},
data() {
return {
updatedGroups: [],
updatedUsers: [],
isRuleUpdated: false,
};
},
computed: {
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
},
methods: {
handleRuleDataUpdate(namespace, items) {
this.isRuleUpdated = true;
this[namespace] = items;
},
formatItemsData(items, keyName, type) {
return items.map((item) => ({ [keyName]: convertToGraphQLId(type, item.id) }));
},
getRuleEditData() {
return [
...this.formatItemsData(this.updatedUsers, 'userId', 'User'), // eslint-disable-line @gitlab/require-i18n-strings
];
},
formatItemsIds(items) {
return items.map((item) => ({ ...item, id: getIdFromGraphQLId(item.id) }));
},
editRule() {
this.$emit('editRule', this.getRuleEditData());
},
},
};
</script>
<template>
<gl-drawer
:header-height="getDrawerHeaderHeight"
:z-index="$options.DRAWER_Z_INDEX"
:open="isOpen"
@ok="editRule()"
v-on="$listeners"
>
<template #title>
<h2 class="gl-font-size-h2 gl-mt-0">{{ title }}</h2>
</template>
<template #header>
<gl-button
variant="confirm"
:disabled="!isRuleUpdated"
:loading="isLoading"
data-testid="save-allowed-to-merge"
@click="editRule()"
>
{{ __('Save changes') }}
</gl-button>
<gl-button variant="confirm" category="secondary" @click="$emit('close')">
{{ __('Cancel') }}
</gl-button>
</template>
<template #default>
<gl-form-group class="gl-border-none">
<items-selector
type="users"
:items="formatItemsIds(users)"
is-project-only-namespace
:users-options="$options.projectUsersOptions"
@change="handleRuleDataUpdate('updatedUsers', $event)"
/>
</gl-form-group>
</template>
</gl-drawer>
</template>

View File

@ -1,9 +1,24 @@
mutation editBrachRule($id: ProjectsBranchRuleID!, $name: String!) {
branchRuleUpdate(input: { id: $id, name: $name }) {
mutation editBrachRule($input: BranchRuleUpdateInput!) {
branchRuleUpdate(input: $input) {
errors
branchRule {
id
name
branchProtection {
allowForcePush
pushAccessLevels {
nodes {
accessLevel
accessLevelDescription
}
}
mergeAccessLevels {
nodes {
accessLevel
accessLevelDescription
}
}
}
}
}
}

View File

@ -5,6 +5,10 @@ export default {
components: {
GlFormTextarea,
},
model: {
prop: 'value',
event: 'input',
},
props: {
value: {
type: String,
@ -19,11 +23,6 @@ export default {
required: true,
},
},
data() {
return {
messageText: this.value,
};
},
};
</script>
@ -40,13 +39,13 @@ export default {
</div>
<gl-form-textarea
:id="inputId"
v-model="messageText"
:value="value"
class="form-control js-gfm-input gl-mb-3 commit-message-edit !gl-font-monospace"
dir="auto"
required="required"
rows="7"
max-rows="32"
@input="$emit('input', messageText)"
@input="(val) => $emit('input', val)"
/>
</div>
</li>

View File

@ -605,7 +605,7 @@ export default {
<ul class="border-top commits-list flex-list gl-list-none gl-p-0 gl-pt-4">
<commit-edit
v-if="shouldShowSquashEdit"
:value="squashCommitMessage"
v-model="squashCommitMessage"
:label="__('Squash commit message')"
input-id="squash-message-edit"
class="gl-m-0! gl-p-0!"
@ -617,7 +617,7 @@ export default {
</commit-edit>
<commit-edit
v-if="shouldShowMergeEdit"
:value="commitMessage"
v-model="commitMessage"
:label="__('Merge commit message')"
input-id="merge-message-edit"
class="gl-m-0! gl-p-0!"

View File

@ -45,6 +45,16 @@ export default {
required: false,
default: false,
},
usersQueryOptions: {
type: Object,
required: false,
default: () => ({}),
},
isProjectOnlyNamespace: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -59,6 +69,9 @@ export default {
config() {
return CONFIG[this.type];
},
showNamespaceDropdown() {
return this.config.showNamespaceDropdown && !this.isProjectOnlyNamespace;
},
namespaceDropdownText() {
return parseBoolean(this.isProjectNamespace)
? this.$options.i18n.projectGroups
@ -105,7 +118,7 @@ export default {
}
},
async fetchUsersBySearchTerm(search) {
const users = await Api.projectUsers(this.projectPath, search);
const users = await Api.projectUsers(this.projectPath, search, this.usersQueryOptions);
return users?.map((user) => ({
text: user.name,
@ -228,10 +241,11 @@ export default {
</gl-collapsible-listbox>
<gl-collapsible-listbox
v-if="config.showNamespaceDropdown"
v-if="showNamespaceDropdown"
v-model="isProjectNamespace"
:toggle-text="namespaceDropdownText"
:items="$options.namespaceOptions"
data-testid="namespace-dropdown"
@select="handleSelectNamespace"
/>
</div>

View File

@ -0,0 +1,181 @@
<script>
import { GlButton, GlForm, GlFormGroup, GlFormInput, GlModal } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
import { __, s__, sprintf } from '~/locale';
import { isSafeURL } from '~/lib/utils/url_utility';
export default {
components: {
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlModal,
},
props: {
edit: {
type: Boolean,
required: false,
default: false,
},
imageFiles: {
type: [Array, FileList],
required: false,
default: () => [],
},
imageId: {
type: Number,
required: false,
default: null,
},
filename: {
type: String,
required: false,
default: '',
},
visible: {
type: Boolean,
required: true,
},
url: {
type: String,
required: false,
default: '',
},
urlText: {
type: String,
required: false,
default: '',
},
},
data() {
return {
modalUrl: this.url,
modalUrlText: this.urlText,
};
},
computed: {
...mapState(['isUploadingImage']),
title() {
return this.edit
? sprintf(this.$options.i18n.editTitle, { filename: this.filename })
: this.$options.i18n.uploadTitle;
},
isUrlValid() {
return this.modalUrl === '' || isSafeURL(this.modalUrl);
},
},
methods: {
...mapActions(['uploadImage', 'updateImage']),
clear() {
this.modalUrl = this.url;
this.modalUrlText = this.urlText;
this.$emit('hidden');
},
async submit() {
if (!this.isUrlValid) {
this.$refs.urlInput.$el.focus();
return;
}
if (this.edit) {
await this.updateImage({
imageId: this.imageId,
url: this.modalUrl,
urlText: this.modalUrlText,
});
this.clear();
return;
}
await this.uploadImage({
files: this.imageFiles,
url: this.modalUrl,
urlText: this.modalUrlText,
});
this.clear();
},
},
i18n: {
cancel: __('Cancel'),
description: s__(
"Incidents|Add text or a link to display with your image. If you don't add either, the file name displays instead.",
),
invalidUrlMessage: __('Invalid URL'),
textInputLabel: __('Text (optional)'),
urlInputLabel: __('Link (optional)'),
urlInputDescription: s__('Incidents|Must start with http:// or https://'),
editTitle: s__('Incident|Editing %{filename}'),
uploadTitle: s__('Incidents|Add image details'),
update: __('Update'),
upload: __('Upload'),
},
};
</script>
<template>
<gl-modal
modal-id="metric-image-details-modal"
size="sm"
:title="title"
:visible="visible"
@hidden="clear"
>
<p v-if="!edit" data-testid="metric-image-details-modal-description">
{{ $options.i18n.description }}
</p>
<gl-form
id="metric-image-details-modal-form"
data-testid="metric-image-details-modal-form"
@submit.prevent="submit"
>
<gl-form-group
:label="$options.i18n.textInputLabel"
label-for="metric-image-details-modal-text-input"
>
<gl-form-input
id="metric-image-details-modal-text-input"
v-model="modalUrlText"
data-testid="metric-image-details-modal-text-input"
/>
</gl-form-group>
<gl-form-group
data-testid="metric-image-details-url-form-group"
:label="$options.i18n.urlInputLabel"
label-for="metric-image-details-modal-url-input"
:description="$options.i18n.urlInputDescription"
:invalid-feedback="$options.i18n.invalidUrlMessage"
:state="isUrlValid"
>
<gl-form-input
id="metric-image-details-modal-url-input"
ref="urlInput"
v-model="modalUrl"
data-testid="metric-image-details-modal-url-input"
:state="isUrlValid"
lazy
/>
</gl-form-group>
</gl-form>
<template #modal-footer>
<gl-button category="primary" variant="default" @click="clear">
{{ $options.i18n.cancel }}
</gl-button>
<gl-button
form="metric-image-details-modal-form"
:loading="isUploadingImage"
category="primary"
variant="confirm"
type="submit"
>
{{ edit ? $options.i18n.update : $options.i18n.upload }}
</gl-button>
</template>
</gl-modal>
</template>

View File

@ -1,99 +1,48 @@
<script>
import { GlFormGroup, GlFormInput, GlLoadingIcon, GlModal } from '@gitlab/ui';
import { GlLoadingIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import { s__ } from '~/locale';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import MetricImagesTable from '~/vue_shared/components/metric_images/metric_images_table.vue';
import MetricImageDetailsModal from './metric_image_details_modal.vue';
export default {
components: {
GlFormGroup,
GlFormInput,
GlLoadingIcon,
GlModal,
MetricImagesTable,
MetricImageDetailsModal,
UploadDropzone,
},
inject: ['canUpdate', 'projectId', 'iid'],
data() {
return {
currentFiles: [],
isModalUrlValid: true,
modalVisible: false,
modalUrl: '',
modalUrlText: '',
};
},
computed: {
...mapState(['metricImages', 'isLoadingMetricImages', 'isUploadingImage']),
actionPrimaryProps() {
return {
text: this.$options.i18n.modalUpload,
attributes: {
loading: this.isUploadingImage,
disabled: this.isUploadingImage || !this.isModalUrlValid,
category: 'primary',
variant: 'confirm',
},
};
},
...mapState(['metricImages', 'isLoadingMetricImages']),
},
mounted() {
this.setInitialData({ modelIid: this.iid, projectId: this.projectId });
this.fetchImages();
},
methods: {
...mapActions(['fetchImages', 'uploadImage', 'setInitialData']),
...mapActions(['fetchImages', 'setInitialData']),
clearInputs() {
this.modalVisible = false;
this.modalUrl = '';
this.modalUrlText = '';
this.currentFile = false;
this.currentFiles = [];
},
openMetricDialog(files) {
this.modalVisible = true;
this.currentFiles = files;
},
async onUpload() {
if (!this.isModalUrlValid) return;
try {
await this.uploadImage({
files: this.currentFiles,
url: this.modalUrl,
urlText: this.modalUrlText,
});
// Error case handled within action
} finally {
this.clearInputs();
}
},
onModalUrlInput(value) {
if (value === '') this.validateModalUrl();
},
validateModalUrl() {
try {
this.isModalUrlValid = Boolean(new URL(this.modalUrl));
} catch (err) {
this.isModalUrlValid = false;
}
},
},
i18n: {
modalUpload: __('Upload'),
modalCancel: __('Cancel'),
modalTitle: s__('Incidents|Add image details'),
modalDescription: s__(
"Incidents|Add text or a link to display with your image. If you don't add either, the file name displays instead.",
),
dropDescription: s__(
'Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident',
),
textInputLabel: __('Text (optional)'),
urlInputLabel: __('Link (optional)'),
urlInputDescription: s__('Incidents|Must start with http:// or https://'),
invalidUrlMessage: __('Invalid URL'),
},
};
</script>
@ -103,40 +52,11 @@ export default {
<div v-if="isLoadingMetricImages">
<gl-loading-icon class="gl-p-5" size="sm" />
</div>
<gl-modal
modal-id="upload-metric-modal"
size="sm"
:action-primary="actionPrimaryProps"
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:title="$options.i18n.modalTitle"
<metric-image-details-modal
:image-files="currentFiles"
:visible="modalVisible"
@hidden="clearInputs"
@primary.prevent="onUpload"
>
<p>{{ $options.i18n.modalDescription }}</p>
<gl-form-group :label="$options.i18n.textInputLabel" label-for="upload-text-input">
<gl-form-input id="upload-text-input" v-model="modalUrlText" />
</gl-form-group>
<gl-form-group
id="upload-url-group"
:label="$options.i18n.urlInputLabel"
label-for="upload-url-input"
:description="$options.i18n.urlInputDescription"
:invalid-feedback="$options.i18n.invalidUrlMessage"
:state="isModalUrlValid"
>
<gl-form-input
id="upload-url-input"
v-model="modalUrl"
:state="isModalUrlValid"
@blur="validateModalUrl"
@input="onModalUrlInput"
/>
</gl-form-group>
</gl-modal>
/>
<metric-images-table v-for="metric in metricImages" :key="metric.id" v-bind="metric" />
<upload-dropzone
v-if="canUpdate"

View File

@ -1,8 +1,6 @@
<script>
import {
GlButton,
GlFormGroup,
GlFormInput,
GlCard,
GlIcon,
GlLink,
@ -13,6 +11,7 @@ import {
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import MetricImageDetailsModal from './metric_image_details_modal.vue';
export default {
i18n: {
@ -20,20 +19,18 @@ export default {
modalDescription: s__('Incident|Are you sure you wish to delete this image?'),
modalCancel: __('Cancel'),
modalTitle: s__('Incident|Deleting %{filename}'),
editModalUpdate: __('Update'),
editModalTitle: s__('Incident|Editing %{filename}'),
editIconTitle: s__('Incident|Edit image text or link'),
deleteIconTitle: s__('Incident|Delete image'),
editButtonLabel: __('Edit'),
},
components: {
GlButton,
GlFormGroup,
GlFormInput,
GlCard,
GlIcon,
GlLink,
GlModal,
GlSprintf,
MetricImageDetailsModal,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -67,11 +64,8 @@ export default {
return {
isCollapsed: false,
isDeleting: false,
isUpdating: false,
modalVisible: false,
editModalVisible: false,
modalUrl: this.url,
modalUrlText: this.urlText,
};
},
computed: {
@ -86,17 +80,6 @@ export default {
},
};
},
updateActionPrimaryProps() {
return {
text: this.$options.i18n.editModalUpdate,
attributes: {
loading: this.isUpdating,
disabled: this.isUpdating,
category: 'primary',
variant: 'confirm',
},
};
},
arrowIconName() {
return this.isCollapsed ? 'chevron-right' : 'chevron-down';
},
@ -110,16 +93,10 @@ export default {
},
},
methods: {
...mapActions(['deleteImage', 'updateImage']),
...mapActions(['deleteImage']),
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
},
resetEditFields() {
this.modalUrl = this.url;
this.modalUrlText = this.urlText;
this.editModalVisible = false;
this.modalVisible = false;
},
async onDelete() {
try {
this.isDeleting = true;
@ -129,21 +106,6 @@ export default {
this.modalVisible = false;
}
},
async onUpdate() {
try {
this.isUpdating = true;
await this.updateImage({
imageId: this.id,
url: this.modalUrl,
urlText: this.modalUrlText,
});
} finally {
this.isUpdating = false;
this.modalUrl = '';
this.modalUrlText = '';
this.editModalVisible = false;
}
},
},
};
</script>
@ -164,7 +126,7 @@ export default {
text: $options.i18n.modalCancel,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary.prevent="onDelete"
@hidden="resetEditFields"
@hidden="modalVisible = false"
>
<template #modal-title>
<gl-sprintf :message="$options.i18n.modalTitle">
@ -176,46 +138,15 @@ export default {
<p>{{ $options.i18n.modalDescription }}</p>
</gl-modal>
<gl-modal
modal-id="edit-metric-modal"
size="sm"
:action-primary="updateActionPrimaryProps"
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
<metric-image-details-modal
edit
:image-id="id"
:filename="filename"
:url="url"
:url-text="urlText"
:visible="editModalVisible"
data-testid="metric-image-edit-modal"
@hidden="resetEditFields"
@primary.prevent="onUpdate"
>
<template #modal-title>
<gl-sprintf :message="$options.i18n.editModalTitle">
<template #filename>
{{ filename }}
</template>
</gl-sprintf>
</template>
<gl-form-group :label="__('Text (optional)')" label-for="upload-text-input">
<gl-form-input
id="upload-text-input"
v-model="modalUrlText"
data-testid="metric-image-text-field"
/>
</gl-form-group>
<gl-form-group
:label="__('Link (optional)')"
label-for="upload-url-input"
:description="s__('Incidents|Must start with http or https')"
>
<gl-form-input
id="upload-url-input"
v-model="modalUrl"
data-testid="metric-image-url-field"
/>
</gl-form-group>
</gl-modal>
@hidden="editModalVisible = false"
/>
<template #header>
<div class="gl-w-full gl-display-flex gl-flex-direction-row gl-justify-content-space-between">
@ -242,7 +173,7 @@ export default {
v-if="canUpdate"
v-gl-tooltip.bottom
icon="pencil"
:aria-label="__('Edit')"
:aria-label="$options.i18n.editButtonLabel"
:title="$options.i18n.editIconTitle"
data-testid="edit-button"
@click="editModalVisible = true"
@ -251,7 +182,7 @@ export default {
v-if="canUpdate"
v-gl-tooltip.bottom
icon="remove"
:aria-label="__('Delete')"
:aria-label="$options.i18n.modalDelete"
:title="$options.i18n.deleteIconTitle"
data-testid="delete-button"
@click="modalVisible = true"

View File

@ -58,7 +58,7 @@ export default {
</script>
<template>
<div
<li
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
:class="optionalClasses"
>
@ -159,5 +159,5 @@ export default {
</div>
<div class="gl-w-9"></div>
</div>
</div>
</li>
</template>

View File

@ -17,7 +17,7 @@ class Projects::MirrorsController < Projects::ApplicationController
end
def update
if push_mirror_create? && Feature.enabled?(:use_remote_mirror_create_service, project)
if push_mirror_create?
service = ::RemoteMirrors::CreateService.new(project, current_user, push_mirror_attributes)
result = service.execute

View File

@ -4,6 +4,8 @@
# projects to a GitLab instance. It associates the import with the responsible
# user.
class BulkImport < ApplicationRecord
include AfterCommitQueue
MIN_MAJOR_VERSION = 14
MIN_MINOR_VERSION_FOR_PROJECT = 4
@ -26,6 +28,7 @@ class BulkImport < ApplicationRecord
state :finished, value: 2
state :timeout, value: 3
state :failed, value: -1
state :canceled, value: -2
event :start do
transition created: :started
@ -44,11 +47,21 @@ class BulkImport < ApplicationRecord
transition any => :failed
end
event :cancel do
transition any => :canceled
end
# rubocop:disable Style/SymbolProc
after_transition any => [:finished, :failed, :timeout] do |bulk_import|
bulk_import.update_has_failures
end
# rubocop:enable Style/SymbolProc
after_transition any => [:canceled] do |bulk_import|
bulk_import.run_after_commit do
bulk_import.propagate_cancel
end
end
end
def source_version_info
@ -74,11 +87,17 @@ class BulkImport < ApplicationRecord
update!(has_failures: true)
end
def propagate_cancel
return unless entities.any?
entities.each(&:cancel)
end
def supports_batched_export?
source_version_info >= self.class.min_gl_version_for_migration_in_batches
end
def completed?
finished? || failed? || timeout?
finished? || failed? || timeout? || canceled?
end
end

View File

@ -27,6 +27,7 @@ module BulkImports
state :timeout, value: 3
state :failed, value: -1
state :skipped, value: -2
state :canceled, value: -3
event :start do
transition created: :started
@ -53,6 +54,10 @@ module BulkImports
event :cleanup_stale do
transition [:created, :started] => :timeout
end
event :cancel do
transition any => :canceled
end
end
end
end

View File

@ -18,6 +18,8 @@
# The tree structure of the entities results in the same structure for imported
# Groups and Projects.
class BulkImports::Entity < ApplicationRecord
include AfterCommitQueue
self.table_name = 'bulk_import_entities'
FailedError = Class.new(StandardError)
@ -71,6 +73,7 @@ class BulkImports::Entity < ApplicationRecord
state :finished, value: 2
state :timeout, value: 3
state :failed, value: -1
state :canceled, value: -2
event :start do
transition created: :started
@ -90,11 +93,21 @@ class BulkImports::Entity < ApplicationRecord
transition started: :timeout
end
event :cancel do
transition any => :canceled
end
# rubocop:disable Style/SymbolProc
after_transition any => [:finished, :failed, :timeout] do |entity|
entity.update_has_failures
end
# rubocop:enable Style/SymbolProc
after_transition any => [:canceled] do |entity|
entity.run_after_commit do
entity.propagate_cancel
end
end
end
def self.all_human_statuses
@ -221,6 +234,10 @@ class BulkImports::Entity < ApplicationRecord
end
end
def propagate_cancel
trackers.each(&:cancel)
end
private
def validate_parent_is_a_group

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class BulkImports::Tracker < ApplicationRecord
include AfterCommitQueue
self.table_name = 'bulk_import_trackers'
alias_attribute :pipeline_name, :relation
@ -52,6 +54,7 @@ class BulkImports::Tracker < ApplicationRecord
state :timeout, value: 4
state :failed, value: -1
state :skipped, value: -2
state :canceled, value: -3
event :start do
transition enqueued: :started
@ -83,6 +86,10 @@ class BulkImports::Tracker < ApplicationRecord
transition any => :failed
end
event :cancel do
transition any => :canceled
end
event :cleanup_stale do
transition [:created, :started] => :timeout
end
@ -90,6 +97,12 @@ class BulkImports::Tracker < ApplicationRecord
after_transition any => [:finished, :failed] do |tracker|
BulkImports::ObjectCounter.persist!(tracker)
end
after_transition any => [:canceled] do |tracker|
tracker.run_after_commit do
tracker.propagate_cancel
end
end
end
def checksums
@ -111,6 +124,10 @@ class BulkImports::Tracker < ApplicationRecord
pipeline_class.relation.to_sym
end
def propagate_cancel
batches.each(&:cancel)
end
private
def cached_checksums

View File

@ -2,7 +2,10 @@
module Ci
class Partition < Ci::ApplicationRecord
MAX_PARTITION_SIZE = 100.gigabytes
validates :id, :status, presence: true
validates :status, uniqueness: { if: ->(partition) { partition.status_changed? && partition.current? } }
state_machine :status, initial: :preparing do
state :preparing, value: 0
@ -13,6 +16,14 @@ module Ci
event :ready do
transition preparing: :ready
end
event :switch_writes do
transition ready: :current
end
before_transition [:ready] => :current do
Ci::Partition.with_status(:current).update_all(status: Ci::Partition.statuses[:active])
end
end
scope :id_after, ->(partition_id) { where(arel_table[:id].gt(partition_id)) }
@ -29,6 +40,14 @@ module Ci
def create_next!
create!(id: last.id.next, status: statuses[:preparing])
end
def next_available(partition_id)
Ci::Partition
.with_status(:ready)
.id_after(partition_id)
.order(id: :asc)
.first
end
end
def above_threshold?(threshold)

View File

@ -18,10 +18,10 @@ module Packages
validates :file_type, presence: true
validates :file_type, inclusion: { in: %w[unknown] },
if: -> { package_file&.package&.debian_incoming? || package_file&.package&.processing? }
if: -> { package_file&.package&.incoming? || package_file&.package&.processing? }
validates :file_type,
inclusion: { in: %w[source dsc deb udeb buildinfo changes ddeb] },
if: -> { package_file&.package&.debian_package? && !package_file&.package&.processing? }
if: -> { !package_file&.package&.incoming? && !package_file&.package&.processing? }
validates :component,
presence: true,

View File

@ -8,9 +8,8 @@ class Packages::Debian::GroupDistribution < ApplicationRecord
include Packages::Debian::Distribution
def packages
Packages::Package
::Packages::Debian::Package
.for_projects(group.all_projects.public_only)
.debian
.with_debian_codename(codename)
.with_codename(codename)
end
end

View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
module Packages
module Debian
class Package < Packages::Package
INSTALLABLE_STATUSES = [:default, :hidden].freeze
self.allow_legacy_sti_class = true
has_one :publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
has_one :distribution, through: :publication, source: :distribution, inverse_of: :packages,
class_name: 'Packages::Debian::ProjectDistribution'
accepts_nested_attributes_for :publication
delegate :codename, :suite, to: :distribution, prefix: :distribution
validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :version?
validates :name, inclusion: { in: [Packages::Debian::INCOMING_PACKAGE_NAME] }, unless: :version?
validates :version,
presence: true,
format: { with: Gitlab::Regex.debian_version_regex },
if: :version?
validate :forbidden_changes
scope :with_codename, ->(codename) do
joins(:distribution).where(Packages::Debian::ProjectDistribution.table_name => { codename: codename })
end
scope :with_codename_or_suite, ->(codename_or_suite) do
joins(:distribution)
.where(Packages::Debian::ProjectDistribution.table_name => { codename: codename_or_suite })
.or(where(Packages::Debian::ProjectDistribution.table_name => { suite: codename_or_suite }))
end
scope :preload_debian_file_metadata, -> { preload(package_files: :debian_file_metadatum) }
def self.incoming_package!
default
.with_version(nil)
.find_by!(name: Packages::Debian::INCOMING_PACKAGE_NAME)
end
def self.existing_packages_with(name:, version:)
with_name(name)
.with_version(version)
.not_pending_destruction
end
def incoming?
name == Packages::Debian::INCOMING_PACKAGE_NAME && version.nil?
end
private
def forbidden_changes
return unless persisted?
# Debian incoming
return unless version_was.nil? || version.nil?
errors.add(:version, _('cannot be changed')) if version_changed?
end
end
end
end

View File

@ -8,5 +8,5 @@ class Packages::Debian::ProjectDistribution < ApplicationRecord
include Packages::Debian::Distribution
has_many :publications, class_name: 'Packages::Debian::Publication', inverse_of: :distribution, foreign_key: :distribution_id
has_many :packages, class_name: 'Packages::Package', through: :publications
has_many :packages, class_name: 'Packages::Debian::Package', through: :publications
end

View File

@ -2,23 +2,15 @@
class Packages::Debian::Publication < ApplicationRecord
belongs_to :package,
-> { where(package_type: :debian).where.not(version: nil) },
inverse_of: :debian_publication,
class_name: 'Packages::Package'
-> { where.not(version: nil) },
inverse_of: :publication,
class_name: 'Packages::Debian::Package'
belongs_to :distribution,
inverse_of: :publications,
class_name: 'Packages::Debian::ProjectDistribution',
foreign_key: :distribution_id
validates :package, presence: true
validate :valid_debian_package_type
validates :distribution, presence: true
private
def valid_debian_package_type
return errors.add(:package, _('type must be Debian')) unless package&.debian?
return errors.add(:package, _('must be a Debian package')) unless package.debian_package?
end
end

View File

@ -54,14 +54,10 @@ class Packages::Package < ApplicationRecord
has_one :terraform_module_metadatum, inverse_of: :package, class_name: 'Packages::TerraformModule::Metadatum'
has_many :build_infos, inverse_of: :package
has_many :pipelines, through: :build_infos, disable_joins: true
has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
has_one :debian_distribution, through: :debian_publication, source: :distribution, inverse_of: :packages, class_name: 'Packages::Debian::ProjectDistribution'
has_many :matching_package_protection_rules, -> (package) { where(package_type: package.package_type).for_package_name(package.name) }, through: :project, source: :package_protection_rules
accepts_nested_attributes_for :debian_publication
accepts_nested_attributes_for :maven_metadatum
delegate :codename, :suite, to: :debian_distribution, prefix: :debian_distribution
delegate :target_sha, to: :composer_metadatum, prefix: :composer
validates :project, presence: true
@ -84,8 +80,6 @@ class Packages::Package < ApplicationRecord
validates :name, format: { with: Gitlab::Regex.npm_package_name_regex, message: Gitlab::Regex.npm_package_name_regex_message }, if: :npm?
validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget?
validates :name, format: { with: Gitlab::Regex.terraform_module_package_name_regex }, if: :terraform_module?
validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :debian_package?
validates :name, inclusion: { in: [Packages::Debian::INCOMING_PACKAGE_NAME] }, if: :debian_incoming?
validates :version, format: { with: Gitlab::Regex.nuget_version_regex }, if: :nuget?
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
@ -97,11 +91,6 @@ class Packages::Package < ApplicationRecord
presence: true,
format: { with: Gitlab::Regex.generic_package_version_regex },
if: :generic?
validates :version,
presence: true,
format: { with: Gitlab::Regex.debian_version_regex },
if: :debian_package?
validate :forbidden_debian_changes, if: :debian?
scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
scope :with_name, ->(name) { where(name: name) }
@ -147,14 +136,6 @@ class Packages::Package < ApplicationRecord
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }
scope :including_dependency_links_with_nuget_metadatum, -> { includes(dependency_links: [:dependency, :nuget_metadatum]) }
scope :with_debian_codename, ->(codename) do
joins(:debian_distribution).where(Packages::Debian::ProjectDistribution.table_name => { codename: codename })
end
scope :with_debian_codename_or_suite, ->(codename_or_suite) do
joins(:debian_distribution).where(Packages::Debian::ProjectDistribution.table_name => { codename: codename_or_suite })
.or(where(Packages::Debian::ProjectDistribution.table_name => { suite: codename_or_suite }))
end
scope :preload_debian_file_metadata, -> { preload(package_files: :debian_file_metadatum) }
scope :with_composer_target, -> (target) do
includes(:composer_metadatum)
.joins(:composer_metadatum)
@ -219,7 +200,8 @@ class Packages::Package < ApplicationRecord
golang: 'Packages::Go::Package',
rubygems: 'Packages::Rubygems::Package',
conan: 'Packages::Conan::Package',
rpm: 'Packages::Rpm::Package'
rpm: 'Packages::Rpm::Package',
debian: 'Packages::Debian::Package'
}.freeze
def self.only_maven_packages_with_path(path, use_cte: false)
@ -252,16 +234,6 @@ class Packages::Package < ApplicationRecord
find_by!(name: name, version: version)
end
def self.debian_incoming_package!
find_by!(name: Packages::Debian::INCOMING_PACKAGE_NAME, version: nil, package_type: :debian, status: :default)
end
def self.existing_debian_packages_with(name:, version:)
debian.with_name(name)
.with_version(version)
.not_pending_destruction
end
def self.pluck_names
pluck(:name)
end
@ -319,14 +291,6 @@ class Packages::Package < ApplicationRecord
terraform_module?
end
def debian_incoming?
debian? && version.nil?
end
def debian_package?
debian? && !version.nil?
end
def package_settings
project.namespace.package_settings
end
@ -417,13 +381,4 @@ class Packages::Package < ApplicationRecord
project.root_namespace.path == ::Packages::Npm.scope_of(name)
end
def forbidden_debian_changes
return unless persisted?
# Debian incoming
if version_was.nil? || version.nil?
errors.add(:version, _('cannot be changed')) if version_changed?
end
end
end

View File

@ -3,7 +3,6 @@
module Ci
module Partitions
class CreateService
MAX_PARTITION_SIZE = 100.gigabytes
HEADROOM_PARTITIONS = 3
def initialize(partition)
@ -26,7 +25,7 @@ module Ci
end
def above_threshold?
partition.above_threshold?(MAX_PARTITION_SIZE)
partition.above_threshold?(Ci::Partition::MAX_PARTITION_SIZE)
end
def headroom_available?

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Ci
module Partitions
class SyncService
def initialize(partition)
@partition = partition
end
def execute
return unless Feature.enabled?(:ci_partitioning_automation, :instance)
return unless partition
sync_available_partitions_statuses!
next_ci_partition = next_available_partition
return unless next_ci_partition.present? && above_threshold?
next_ci_partition.switch_writes!
end
private
attr_reader :partition
def above_threshold?
partition.above_threshold?(Ci::Partition::MAX_PARTITION_SIZE)
end
def sync_available_partitions_statuses!
Ci::Partition.id_after(partition.id).each do |partition|
partition.ready! if partition.all_partitions_exist?
end
end
def next_available_partition
Ci::Partition.next_available(partition.id)
end
end
end
end

View File

@ -107,7 +107,7 @@ module Packages
end
def incoming
@package_file.package.project.packages.debian_incoming_package!
::Packages::Debian::Package.for_projects(@package_file.package.project).incoming_package!
end
strong_memoize_attr(:incoming)
end

View File

@ -32,7 +32,7 @@ module Packages
cleanup_temp_package
end
::Packages::Debian::GenerateDistributionWorker.perform_async(:project, package.debian_distribution.id)
::Packages::Debian::GenerateDistributionWorker.perform_async(:project, package.distribution.id)
end
end
@ -90,10 +90,9 @@ module Packages
end
def package
packages = temp_package.project
.packages
.existing_debian_packages_with(name: package_name, version: package_version)
package = packages.with_debian_codename_or_suite(package_distribution)
packages = ::Packages::Debian::Package.for_projects(temp_package.project)
.existing_packages_with(name: package_name, version: package_version)
package = packages.with_codename_or_suite(package_distribution)
.first
unless package
@ -101,7 +100,7 @@ module Packages
if package_in_other_distribution
raise ArgumentError, "Debian package #{package_name} #{package_version} exists " \
"in distribution #{package_in_other_distribution.debian_distribution.codename}"
"in distribution #{package_in_other_distribution.distribution.codename}"
end
end
@ -153,7 +152,7 @@ module Packages
return unless using_temporary_package?
package.update!(
debian_publication_attributes: { distribution_id: distribution.id }
publication_attributes: { distribution_id: distribution.id }
)
end

View File

@ -2,6 +2,8 @@
module Users
class ActivityService
LEASE_TIMEOUT = 1.minute.to_i
def initialize(author:, namespace: nil, project: nil)
@user = if author.respond_to?(:username)
author
@ -30,6 +32,11 @@ module Users
today = Date.today
return if user.last_activity_on == today
lease = Gitlab::ExclusiveLease.new("activity_service:#{user.id}", timeout: LEASE_TIMEOUT)
# Skip transaction checks for exclusive lease as it is breaking system specs.
# See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/441536
return unless Gitlab::ExclusiveLease.skipping_transaction_check { lease.try_obtain }
user.update_attribute(:last_activity_on, today)
Gitlab::UsageDataCounters::HLLRedisCounter.track_event('unique_active_user', values: user.id)

View File

@ -71,6 +71,7 @@ module BulkImports
def run
return batch.skip! if tracker.failed? || tracker.finished?
return cancel_batch if tracker.canceled?
logger.info(log_attributes(message: 'Batch tracker started'))
batch.start!
@ -149,5 +150,11 @@ module BulkImports
}.merge(extra)
)
end
def cancel_batch
batch.cancel!
logger.info(log_attributes(message: 'Batch tracker canceled'))
end
end
end

View File

@ -72,7 +72,9 @@ module BulkImports
attr_reader :pipeline_tracker, :entity
def run
return if pipeline_tracker.canceled?
return skip_tracker if entity.failed?
return cancel_tracker if entity.canceled?
raise(Pipeline::FailedError, "Export from source instance failed: #{export_status.error}") if export_failed?
raise(Pipeline::ExpiredError, 'Empty export status on source instance') if empty_export_timeout?
@ -183,6 +185,12 @@ module BulkImports
pipeline_tracker.update!(status_event: 'skip', jid: jid)
end
def cancel_tracker
logger.info(log_attributes(message: 'Canceling pipeline due to canceled entity'))
pipeline_tracker.update!(status_event: 'cancel', jid: jid)
end
def log_attributes(extra = {})
logger.default_attributes.merge(extra)
end

View File

@ -12,6 +12,12 @@ module Ci
def perform
Ci::Partitions::SetupDefaultService.new.execute
ci_partition_current = Ci::Partition.current
return unless ci_partition_current
Ci::Partitions::CreateService.new(ci_partition_current).execute
Ci::Partitions::SyncService.new(ci_partition_current).execute
end
end
end

View File

@ -1,9 +0,0 @@
---
name: use_remote_mirror_create_service
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/455515
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149263
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/455631
milestone: '17.0'
group: group::source code
type: gitlab_com_derisk
default_enabled: false

View File

@ -4,8 +4,9 @@ classes:
- Elastic::IndexSetting
feature_categories:
- global_search
description: TODO
description: Describes the settings (such as number of shards and replicas) for each Elasticsearch or Opensearch index
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56344
milestone: '13.11'
gitlab_schema: gitlab_main
gitlab_schema: gitlab_main_cell
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/442659
exempt_from_sharding: true # data is specific to each cell's Elasticsearch cluster, no customer data

View File

@ -13,5 +13,6 @@ feature_categories:
- user_management
description: Stores events created by users interacting with various product features
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/a847501fd2ffc1c4becc7d0d352d80168d9b3568
milestone: "2.2"
milestone: '2.2'
gitlab_schema: gitlab_main_cell
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/462801

View File

@ -2,6 +2,7 @@
table_name: packages_packages
classes:
- Packages::Conan::Package
- Packages::Debian::Package
- Packages::Go::Package
- Packages::MlModel::Package
- Packages::Package

View File

@ -8,3 +8,4 @@ description: Stores log of push events
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12463
milestone: '9.5'
gitlab_schema: gitlab_main
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/462802

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class AddUniqueIndexOnStatusToCiPartition < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.1'
TABLE_NAME = :ci_partitions
INDEX_NAME = :index_ci_partitions_on_current_status
CURRENT_STATUS = 2
def up
add_concurrent_index(TABLE_NAME, :status, unique: true, where: "status = #{CURRENT_STATUS}", name: INDEX_NAME)
end
def down
remove_concurrent_index_by_name(TABLE_NAME, INDEX_NAME)
end
end

View File

@ -0,0 +1 @@
121da77b8cb11e6b16b02bb5eea3ba8d3e7c83fbc5dde23ab94a8ec7c126f95a

View File

@ -25017,6 +25017,8 @@ CREATE INDEX index_ci_namespace_mirrors_on_traversal_ids_unnest ON ci_namespace_
CREATE UNIQUE INDEX index_ci_namespace_monthly_usages_on_namespace_id_and_date ON ci_namespace_monthly_usages USING btree (namespace_id, date);
CREATE UNIQUE INDEX index_ci_partitions_on_current_status ON ci_partitions USING btree (status) WHERE (status = 2);
CREATE INDEX index_ci_pending_builds_id_on_protected_partial ON ci_pending_builds USING btree (id) WHERE (protected = true);
CREATE UNIQUE INDEX index_ci_pending_builds_on_build_id ON ci_pending_builds USING btree (build_id);

View File

@ -306,3 +306,37 @@ curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
"source_title": "Issue title"
}
```
## Cancel a migration
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/438281) in GitLab 17.1.
Cancel a direct transfer migration. Requires administrator access.
```plaintext
POST /bulk_imports/:id/cancel
```
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/bulk_imports/1/cancel"
```
```json
{
"id": 1,
"status": "canceled",
"source_type": "gitlab",
"created_at": "2021-06-18T09:45:55.358Z",
"updated_at": "2021-06-18T09:46:27.003Z"
}
```
Possible response status codes:
| Status | Description |
|--------|---------------------------------|
| 200 | Migration successfully canceled |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Migration not found |
| 503 | Service unavailable |

View File

@ -449,7 +449,8 @@ GET /users/:user_id/projects
"pipeline_artifacts_size": 0,
"packages_size": 0,
"snippets_size": 0,
"uploads_size": 0
"uploads_size": 0,
"container_registry_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
@ -580,7 +581,8 @@ GET /users/:user_id/projects
"pipeline_artifacts_size": 0,
"packages_size": 0,
"snippets_size": 0,
"uploads_size": 0
"uploads_size": 0,
"container_registry_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
@ -702,7 +704,8 @@ Example response:
"pipeline_artifacts_size": 0,
"packages_size": 0,
"snippets_size": 0,
"uploads_size": 0
"uploads_size": 0,
"container_registry_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
@ -822,7 +825,8 @@ Example response:
"pipeline_artifacts_size": 0,
"packages_size": 0,
"snippets_size": 0,
"uploads_size": 0
"uploads_size": 0,
"container_registry_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
@ -958,7 +962,8 @@ Example response:
"pipeline_artifacts_size": 0,
"packages_size": 0,
"snippets_size": 0,
"uploads_size": 0
"uploads_size": 0,
"container_registry_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
@ -1078,7 +1083,8 @@ Example response:
"pipeline_artifacts_size": 0,
"packages_size": 0,
"snippets_size": 0,
"uploads_size": 0
"uploads_size": 0,
"container_registry_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
@ -1265,7 +1271,8 @@ GET /projects/:id
"pipeline_artifacts_size": 0,
"packages_size": 0,
"snippets_size": 0,
"uploads_size": 0
"uploads_size": 0,
"container_registry_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {

View File

@ -21,8 +21,12 @@ TL;DR
## 3. Proposal
There is an [investigation](https://gitlab.com/gitlab-org/gitlab/-/issues/458338) to solve this problem using [direct transfer](../../../../user/group/import/index.md).
## 4. Evaluation
## 4.1. Pros
## 4.2. Cons
Direct transfer does not migrate users and users cannot exist on more than one Cell. This means in Cells 1.0, for migrations across Cells, any user contributions will be assigned to the user performing the import.

View File

@ -131,13 +131,39 @@ Installation instructions will be added to the Developer documentation. [issue](
_This list will expand in the near future, but the overall architecture will be the same_
### Self Hosted models fitting into the current architecture
```mermaid
sequenceDiagram
actor User
participant GitLab
participant AIGateway as AI Gateway
participant SelfHostedModel as Self Hosted Model
participant GitLabAIVendor as GitLab AI Vendor
User ->> GitLab: Send request
GitLab ->> GitLab: Check if self-hosted model is configured
alt Self-hosted model configured
GitLab ->> AIGateway: Create prompt and send request
AIGateway ->> SelfHostedModel: Perform API request to AI model
SelfHostedModel -->> AIGateway: Respond to the prompt
AIGateway -->> GitLab: Forward AI response
else
GitLab ->> AIGateway: Create prompt and send request
AIGateway ->> GitLabAIVendor: Perform API request to AI model
GitLabAIVendor -->> AIGateway: Respond to the prompt
AIGateway -->> GitLab: Forward AI response
end
GitLab -->> User: Forward AI response
```
### GitLab Duo Feature Support
| Feature | Default Model | [Mistral AI 7B v0.1](https://huggingface.co/mistralai/Mistral-7B-v0.1) | [Mixtral 8x22B](https://huggingface.co/mistral-community/Mixtral-8x22B-v0.1) |
|---------------------|------------------|----------------------------------------------------------------|---------------------|
| GitLab Duo Chat | Anthropic Claude-2 <br/> Vertex AI Codey textembedding-gecko | Not planned | Not planned |
| Code Completion | Vertex AI Codey code-gecko | ✅ | ✅ |
| Code Generation | Anthropic Claude-2 | ✅ | ✅ |
| Code Generation | Anthropic Claude-3 | ✅ | ✅ |
| Git Suggestions | Vertex AI Codey codechat-bison | Not planned | Not planned |
| Discussion Summary | Vertex AI Codey text-bison | Not planned | Not planned |
| Issue Description Generation | Anthropic Claude-2 | Not planned | Not planned |

View File

@ -167,7 +167,7 @@ On GitLab Dedicated this feature is not available.
AI Impact analytics displays SDLC metrics for a group or project in the month-to-date and the past six months. You can use this table to observe how changes in the AI usage metric correlate with changes in other metrics.
The metric **Monthly Code Suggestions Usage rate** is calculated as the number of monthly unique Code Suggestions users divided by total monthly [unique contributors](../../user/profile/contributions_calendar.md#user-contribution-events). GitLab considers the total monthly unique code contributors, meaning only users with `pushed` events are included in the calculation.
The metric **Monthly Code Suggestions Usage rate** represents users that engage with Code Suggestions every month. It is calculated as the number of monthly unique Code Suggestions users divided by total monthly [unique contributors](../../user/profile/contributions_calendar.md#user-contribution-events). Only unique code contributors, meaning users with `pushed` events, are included in the calculation.
The month-over-month comparison of the AI Usage unique users rate gives a more accurate indication of this metric, as it eliminates factors such as developer experience level and project type or complexity.

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Use AsciiDoc files in your GitLab project, and understand AsciiDoc syntax."
---
# AsciiDoc

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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"
description: "Learn about the GitLab tools for building, tracking, and delivering the code for your project."
---
# Get started managing code

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "To speed up project creation in your group, build custom project templates and share them with your group."
---
# Custom group-level project templates

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Manage Git access to projects by adding CA certificates to your top-level group, instead of individual groups."
---
# Manage group's SSH certificates

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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"
description: "Build, automate, and customize changelogs in your GitLab project."
---
# Changelogs

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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"
description: "Define custom Git attributes for your GitLab project to set options for file handling, display, locking, and storage."
---
# Git attributes

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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"
description: "Syntax highlighting helps you read files in your GitLab project more easily, and identify what files contain."
---
# Syntax Highlighting

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Integrate GitLab with Beyond Identity to verify GPG keys added to user accounts."
---
# Beyond Identity

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Integrate GitLab with GitGuardian to get alerts for policy violations and security issues before they can be exploited."
---
# GitGuardian

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Use protected tags in Git to control who can create tags, and prevent accidental tag updates or deletion."
---
# Protected tags

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "How comma-separated values (CSV) files display in GitLab projects."
---
# CSV files

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "How GeoJSON files are rendered when viewed in GitLab projects."
---
# GeoJSON files

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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"
description: "GitLab projects display Jupyter Notebook files as clean, human-readable files instead of raw files."
---
# Jupyter Notebook files

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Troubleshooting problems with repository mirroring for GitLab projects."
---
# Troubleshooting repository mirroring

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Sign commits in your GitLab repository with GPG (GNU Privacy Guard) keys."
---
# Sign commits with GPG

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Why you should sign your GitLab commits cryptographically, and how to verify signed commits."
---
# Signed commits

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Sign commits in your GitLab repository with SSH keys."
---
# Sign commits with SSH keys

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "Sign commits and tags in your GitLab repository with X.509 certificates."
---
# Sign commits and tags with X.509 certificates

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
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
description: "System notes track the history of changes made to an object, like a merge request or issue, in your GitLab project."
---
# System notes

View File

@ -348,11 +348,13 @@ To add a task to an iteration:
1. Next to **Iteration**, select **Add to iteration**.
1. From the dropdown list, select the iteration to be associated with the task.
## Set time tracking
## Estimate and track spent time
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/438577) in GitLab 17.0.
To set time tracking, refer to the [time tracking page](project/time_tracking.md)
You can estimate and track the time you spend on a task.
For more information, see [Time tracking](project/time_tracking.md).
## View task system notes

View File

@ -231,6 +231,31 @@ module API
get ':import_id/entities/:entity_id/failures' do
present paginate(bulk_import_entity.failures), with: Entities::BulkImports::EntityFailure
end
desc 'Cancel GitLab Migration' do
detail 'This feature was introduced in GitLab 17.1'
success code: 200, model: Entities::BulkImport
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
end
params do
requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration"
end
post ':import_id/cancel' do
authenticated_as_admin!
bulk_import = BulkImport.find(params[:import_id])
bulk_import.cancel!
status :ok
present bulk_import, with: Entities::BulkImport
end
end
end
end

View File

@ -13,6 +13,7 @@ module API
expose :packages_size
expose :snippets_size
expose :uploads_size
expose :container_registry_size
end
end
end

View File

@ -98,30 +98,18 @@ module API
use :mirror_branches_setting
end
post ':id/remote_mirrors' do
if Feature.enabled?(:use_remote_mirror_create_service, user_project)
service = ::RemoteMirrors::CreateService.new(
user_project,
current_user,
declared_params(include_missing: false)
)
service = ::RemoteMirrors::CreateService.new(
user_project,
current_user,
declared_params(include_missing: false)
)
result = service.execute
result = service.execute
if result.success?
present result.payload[:remote_mirror], with: Entities::RemoteMirror
else
render_api_error!(result.message, 400)
end
if result.success?
present result.payload[:remote_mirror], with: Entities::RemoteMirror
else
create_params = declared_params(include_missing: false)
verify_mirror_branches_setting(create_params)
new_mirror = user_project.remote_mirrors.create(create_params)
if new_mirror.persisted?
present new_mirror, with: Entities::RemoteMirror
else
render_validation_error!(new_mirror)
end
render_api_error!(result.message, 400)
end
end

View File

@ -5,5 +5,11 @@ module Gitlab
def can?(...)
Ability.allowed?(...)
end
def can_any?(user, abilities, subject = :global, **opts)
abilities.any? do |ability|
can?(user, ability, subject, **opts)
end
end
end
end

View File

@ -250,5 +250,3 @@ spotbugs-sast:
- if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead.
exists:
- '**/*.groovy'
- '**/*.scala'
- '**/*.kt'

View File

@ -19,8 +19,9 @@ module Gitlab
end
def key
key_path
attributes[:key_path]
end
alias_method :key_path, :key
def events
events_from_new_structure || events_from_old_structure || {}
@ -34,6 +35,14 @@ module Gitlab
end
end
def status
attributes[:status]
end
def value_json_schema
attributes[:value_json_schema]
end
def to_context
return unless %w[redis redis_hll].include?(data_source)
@ -84,6 +93,10 @@ module Gitlab
VALID_SERVICE_PING_STATUSES.include?(attributes[:status])
end
def data_source
attributes[:data_source]
end
def internal_events?
data_source == 'internal_events'
end
@ -154,14 +167,6 @@ module Gitlab
private
def method_missing(method, *args)
attributes[method] || super
end
def respond_to_missing?(method, *args)
attributes[method].present? || super
end
def events_from_new_structure
events = attributes[:events]
return unless events

View File

@ -13,6 +13,7 @@ module Gitlab
include Gitlab::View::Presenter::Base
delegator_override_with Gitlab::Routing.url_helpers
delegator_override :can?
delegator_override :can_any?
delegator_override :declarative_policy_delegate
delegator_override :present
delegator_override :web_url

View File

@ -147,14 +147,15 @@ class GroupSeeder
@resource_count.times do |_|
group = Group.find(group_id)
author = group.group_members.non_invite.sample.user
epic_params = {
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.paragraphs(3).join("\n\n"),
author: group.group_members.non_invite.sample.user,
author: author,
group: group
}
Epic.create!(epic_params)
::Epics::CreateService.new(group: group, current_user: author, params: epic_params).execute
end
end
end

View File

@ -9038,6 +9038,9 @@ msgstr ""
msgid "BranchRules|Allowed to merge (%{total})"
msgstr ""
msgid "BranchRules|Allowed to merge updated"
msgstr ""
msgid "BranchRules|Allowed to push and merge"
msgstr ""
@ -9113,6 +9116,9 @@ msgstr ""
msgid "BranchRules|Edit"
msgstr ""
msgid "BranchRules|Edit allowed to merge"
msgstr ""
msgid "BranchRules|From users with push access."
msgstr ""
@ -27199,9 +27205,6 @@ msgstr ""
msgid "Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident"
msgstr ""
msgid "Incidents|Must start with http or https"
msgstr ""
msgid "Incidents|Must start with http:// or https://"
msgstr ""
@ -62161,9 +62164,6 @@ msgstr ""
msgid "mrWidget|Your password"
msgstr ""
msgid "must be a Debian package"
msgstr ""
msgid "must be a boolean value"
msgstr ""
@ -62766,9 +62766,6 @@ msgstr ""
msgid "two-factor authentication settings"
msgstr ""
msgid "type must be Debian"
msgstr ""
msgid "type parameter is missing and is required"
msgstr ""

View File

@ -10,18 +10,17 @@ module RuboCop
# # bad
# def disable_project_joins_for_blob?
# Elastic::DataMigrationService
# .migration_has_finished?(:backfill_project_permissions_in_blobs_using_permutations)
# .migration_has_finished?(:backfill_archived_on_issues)
# end
#
# # good
# def disable_project_joins_for_blob?
# Elastic::DataMigrationService.migration_has_finished?(:backfill_project_permissions_in_blobs)
# Elastic::DataMigrationService.migration_has_finished?(:backfill_archived_on_issues)
# end
class AvoidCheckingFinishedOnDeprecatedMigrations < RuboCop::Cop::Base
MSG = 'Migration is deprecated and can not be used with `migration_has_finished?`.'
DEPRECATED_MIGRATIONS = [
:backfill_project_permissions_in_blobs_using_permutations,
:backfill_archived_on_issues
].freeze

View File

@ -199,67 +199,6 @@ RSpec.describe Projects::MirrorsController, feature_category: :source_code_manag
end
end
context 'when feature flag "use_remote_mirror_create_service" is disabled' do
before do
stub_feature_flags(use_remote_mirror_create_service: false)
end
context 'With valid URL for a push' do
let(:remote_mirror_attributes) do
{ "0" => { "enabled" => "0", url: 'https://updated.example.com' } }
end
it 'processes a successful update' do
do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-push-remote-settings'))
expect(flash[:notice]).to match(/successfully updated/)
end
it 'creates a RemoteMirror object' do
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.to change(RemoteMirror, :count).by(1)
end
context 'with json format' do
it 'processes a successful update' do
do_put(project, { remote_mirrors_attributes: remote_mirror_attributes }, { format: :json })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
'id' => project.id,
'remote_mirrors_attributes' => a_kind_of(Array)
)
end
end
end
context 'With invalid URL for a push' do
let(:remote_mirror_attributes) do
{ "0" => { "enabled" => "0", url: 'ftp://invalid.invalid' } }
end
it 'processes an unsuccessful update' do
do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-push-remote-settings'))
expect(flash[:alert]).to match(/Only allowed schemes are/)
end
it 'does not create a RemoteMirror object' do
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.not_to change(RemoteMirror, :count)
end
context 'with json format' do
it 'processes an unsuccessful update' do
do_put(project, { remote_mirrors_attributes: remote_mirror_attributes }, { format: :json })
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['remote_mirrors.url']).to include(/Only allowed schemes are/)
end
end
end
end
context 'when user deletes the remote mirror' do
let(:remote_mirror_attributes) do
{ id: project.remote_mirrors.first.id, _destroy: 1 }

View File

@ -33,5 +33,9 @@ FactoryBot.define do
trait :skipped do
status { -2 }
end
trait :canceled do
status { -3 }
end
end
end

View File

@ -33,6 +33,10 @@ FactoryBot.define do
batched { true }
end
trait :canceled do
status { -3 }
end
trait :stale do
created_at { 1.day.ago }
end

View File

@ -0,0 +1,86 @@
# frozen_string_literal: true
FactoryBot.define do
factory :debian_package, class: 'Packages::Debian::Package' do
project
creator { project&.creator }
status { :default }
sequence(:name) { |n| "#{FFaker::Lorem.word}#{n}" }
sequence(:version) { |n| "1.0-#{n}" }
package_type { :debian }
trait :pending_destruction do
status { :pending_destruction }
end
transient do
without_package_files { false }
with_changes_file { false }
file_metadatum_trait { processing? ? :unknown : :keep }
published_in { :create }
end
publication do
if published_in == :create
association(:debian_publication, package: instance)
elsif published_in
association(:debian_publication, package: instance, distribution: published_in)
end
end
package_files do
package_files = []
unless without_package_files
package_files.push(
association(:debian_package_file, :source, file_metadatum_trait, package: instance),
association(:debian_package_file, :dsc, file_metadatum_trait, package: instance),
association(:debian_package_file, :deb, file_metadatum_trait, package: instance),
association(:debian_package_file, :deb_dev, file_metadatum_trait, package: instance),
association(:debian_package_file, :udeb, file_metadatum_trait, package: instance),
association(:debian_package_file, :ddeb, file_metadatum_trait, package: instance),
association(:debian_package_file, :buildinfo, file_metadatum_trait, package: instance)
)
end
if with_changes_file
package_files.push(association(:debian_package_file, :changes, file_metadatum_trait, package: instance))
end
package_files
end
factory :debian_incoming do
name { 'incoming' }
version { nil }
transient do
without_package_files { false }
file_metadatum_trait { :unknown }
published_in { nil }
end
end
factory :debian_temporary_with_files do
status { :processing }
transient do
without_package_files { false }
with_changes_file { false }
file_metadatum_trait { :unknown }
published_in { nil }
end
end
factory :debian_temporary_with_changes do
status { :processing }
transient do
without_package_files { true }
with_changes_file { true }
file_metadatum_trait { :unknown }
published_in { nil }
end
end
end
end

View File

@ -43,80 +43,6 @@ FactoryBot.define do
end
end
factory :debian_package do
sequence(:name) { |n| "#{FFaker::Lorem.word}#{n}" }
sequence(:version) { |n| "1.0-#{n}" }
package_type { :debian }
transient do
without_package_files { false }
with_changes_file { false }
file_metadatum_trait { processing? ? :unknown : :keep }
published_in { :create }
end
after :build do |package, evaluator|
if evaluator.published_in == :create
build(:debian_publication, package: package)
elsif !evaluator.published_in.nil?
create(:debian_publication, package: package, distribution: evaluator.published_in)
end
end
after :create do |package, evaluator|
if evaluator.published_in == :create
package.debian_publication.save!
end
unless evaluator.without_package_files
create :debian_package_file, :source, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :dsc, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :deb, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :deb_dev, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :udeb, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :ddeb, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :buildinfo, evaluator.file_metadatum_trait, package: package
end
if evaluator.with_changes_file
create :debian_package_file, :changes, evaluator.file_metadatum_trait, package: package
end
end
factory :debian_incoming do
name { 'incoming' }
version { nil }
transient do
without_package_files { false }
file_metadatum_trait { :unknown }
published_in { nil }
end
end
factory :debian_temporary_with_files do
status { :processing }
transient do
without_package_files { false }
with_changes_file { false }
file_metadatum_trait { :unknown }
published_in { nil }
end
end
factory :debian_temporary_with_changes do
status { :processing }
transient do
without_package_files { true }
with_changes_file { true }
file_metadatum_trait { :unknown }
published_in { nil }
end
end
end
factory :helm_package do
sequence(:name) { |n| "package-#{n}" }
sequence(:version) { |n| "v1.0.#{n}" }

View File

@ -150,10 +150,6 @@ FactoryBot.define do
avatar { fixture_file_upload('spec/fixtures/dk.png') }
end
trait :with_last_activity_on_today do
last_activity_on { Date.today }
end
trait :with_sign_ins do
sign_in_count { 3 }
current_sign_in_at { FFaker::Time.between(10.days.ago, 1.day.ago) }

View File

@ -590,18 +590,42 @@ describe('diffs/components/app', () => {
expect(wrapper.findComponent(DiffsFileTree).exists()).toBe(true);
});
it('should pass renderDiffFiles to file tree as true when files are present', () => {
it('should pass visible to file tree as true when files are present', () => {
createComponent({
extendStore: ({ state }) => {
state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
},
});
expect(wrapper.findComponent(DiffsFileTree).props('renderDiffFiles')).toBe(true);
expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(true);
});
it('should pass renderDiffFiles to file tree as false without files', () => {
it('should pass visible to file tree as false without files', () => {
createComponent({});
expect(wrapper.findComponent(DiffsFileTree).props('renderDiffFiles')).toBe(false);
expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(false);
});
it('should hide file tree when toggled', async () => {
createComponent({
extendStore: ({ state }) => {
state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
},
});
wrapper.findComponent(DiffsFileTree).vm.$emit('toggled');
await nextTick();
expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(false);
});
it('should show file tree when toggled', async () => {
createComponent({
extendStore: ({ state }) => {
state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
},
});
wrapper.findComponent(DiffsFileTree).vm.$emit('toggled');
await nextTick();
wrapper.findComponent(DiffsFileTree).vm.$emit('toggled');
await nextTick();
expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(true);
});
});

View File

@ -4,20 +4,14 @@ import { Mousetrap } from '~/lib/mousetrap';
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
import TreeList from '~/diffs/components/tree_list.vue';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import { SET_SHOW_TREE_LIST } from '~/diffs/store/mutation_types';
import createDiffsStore from '../create_diffs_store';
describe('DiffsFileTree', () => {
let wrapper;
let store;
const createComponent = ({ renderDiffFiles = true, showTreeList = true } = {}) => {
store = createDiffsStore();
store.commit(`diffs/${SET_SHOW_TREE_LIST}`, showTreeList);
const createComponent = ({ visible = true } = {}) => {
wrapper = shallowMount(DiffsFileTree, {
store,
propsData: {
renderDiffFiles,
visible,
},
});
};
@ -35,7 +29,7 @@ describe('DiffsFileTree', () => {
describe('when renderDiffFiles and showTreeList are false', () => {
beforeEach(() => {
createComponent({ renderDiffFiles: false, showTreeList: false });
createComponent({ visible: false });
});
it('tree list is hidden', () => {
@ -44,18 +38,11 @@ describe('DiffsFileTree', () => {
});
});
it('emits toggled event', async () => {
createComponent();
store.commit(`diffs/${SET_SHOW_TREE_LIST}`, false);
await nextTick();
expect(wrapper.emitted('toggled')).toStrictEqual([[]]);
});
it('toggles when "f" hotkey is pressed', async () => {
createComponent();
Mousetrap.trigger('f');
await nextTick();
expect(wrapper.findComponent(TreeList).exists()).toBe(false);
expect(wrapper.emitted('toggled')).toStrictEqual([[]]);
});
describe('size', () => {
@ -84,13 +71,6 @@ describe('DiffsFileTree', () => {
checkWidth(200);
});
it('sets width of tree list', () => {
createComponent({}, ({ state }) => {
state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
});
checkWidth(320);
});
it('updates width', async () => {
const WIDTH = 500;
createComponent();

View File

@ -8,12 +8,17 @@ import axios from '~/lib/utils/axios_utils';
import MergeRequestTabs, { getActionFromHref } from '~/merge_request_tabs';
import Diff from '~/diff';
import '~/lib/utils/common_utils';
import '~/lib/utils/url_utility';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/webpack', () => ({
resetServiceWorkersPublicPath: jest.fn(),
}));
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
describe('MergeRequestTabs', () => {
const testContext = {};
const stubLocation = {};
@ -127,7 +132,7 @@ describe('MergeRequestTabs', () => {
testContext.class.bindEvents();
$('.merge-request-tabs .commits-tab a').trigger(metakeyEvent);
expect(window.open).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
});
it('opens page when commits badge is clicked', () => {
@ -139,7 +144,7 @@ describe('MergeRequestTabs', () => {
testContext.class.bindEvents();
$('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent);
expect(window.open).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
});
});
@ -151,7 +156,7 @@ describe('MergeRequestTabs', () => {
testContext.class.clickTab({ ...clickTabParams, metaKey: true });
expect(window.open).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
});
it('opens page tab in a new browser tab with Cmd+Click - Mac', () => {
@ -162,7 +167,7 @@ describe('MergeRequestTabs', () => {
testContext.class.clickTab({ ...clickTabParams, ctrlKey: true });
expect(window.open).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
});
it('opens page tab in a new browser tab with Middle-click - Mac/PC', () => {
@ -173,7 +178,7 @@ describe('MergeRequestTabs', () => {
testContext.class.clickTab({ ...clickTabParams, which: 2 });
expect(window.open).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
});
});

View File

@ -10,22 +10,22 @@ describe('DateRangeFilter', () => {
let wrapper;
const defaultTimeRange = {
value: '1h',
startDate: new Date(),
endDate: new Date(),
const defaultProps = {
selected: {
value: '1h',
startDate: new Date(),
endDate: new Date(),
},
};
const mount = (selected) => {
const mount = (props = defaultProps) => {
wrapper = shallowMountExtended(DateRangeFilter, {
propsData: {
selected,
},
propsData: props,
});
};
beforeEach(() => {
mount(defaultTimeRange);
mount();
});
const findDateRangesDropdown = () => wrapper.findComponent(DateRangesDropdown);
@ -34,7 +34,7 @@ describe('DateRangeFilter', () => {
it('renders the date ranges dropdown with the default selected value and options', () => {
const dateRangesDropdown = findDateRangesDropdown();
expect(dateRangesDropdown.exists()).toBe(true);
expect(dateRangesDropdown.props('selected')).toBe(defaultTimeRange.value);
expect(dateRangesDropdown.props('selected')).toBe(defaultProps.selected.value);
expect(dateRangesDropdown.props('dateRangeOptions')).toMatchInlineSnapshot(`
Array [
Object {
@ -101,8 +101,23 @@ describe('DateRangeFilter', () => {
`);
});
it('renders dateRangeOptions based on dateOptions if specified', () => {
mount({ ...defaultProps, dateOptions: [{ value: '7m', title: 'Last 7 minutes' }] });
expect(findDateRangesDropdown().props('dateRangeOptions')).toMatchInlineSnapshot(`
Array [
Object {
"endDate": 2024-04-23T04:00:00.000Z,
"startDate": 2024-04-23T03:53:00.000Z,
"text": "Last 7 minutes",
"value": "7m",
},
]
`);
});
it('does not set the selected value if not specified', () => {
mount(undefined);
mount({ selected: undefined });
expect(findDateRangesDropdown().props('selected')).toBe('');
});
@ -112,7 +127,9 @@ describe('DateRangeFilter', () => {
startDate: new Date('2022-01-01'),
endDate: new Date('2022-01-02'),
};
mount({ value: 'custom', startDate: timeRange.startDate, endDate: timeRange.endDate });
mount({
selected: { value: 'custom', startDate: timeRange.startDate, endDate: timeRange.endDate },
});
expect(findDateRangesPicker().exists()).toBe(true);
expect(findDateRangesPicker().props('defaultStartDate')).toBe(timeRange.startDate);
@ -153,17 +170,19 @@ describe('DateRangeFilter', () => {
});
describe('start opened', () => {
it('sets startOpend to true if custom date is selected without start and end date', () => {
mount({ value: 'custom' });
it('sets startOpened to true if custom date is selected without start and end date', () => {
mount({ selected: { value: 'custom' } });
expect(findDateRangesPicker().props('startOpened')).toBe(true);
});
it('sets startOpend to false if custom date is selected with start and end date', () => {
it('sets startOpened to false if custom date is selected with start and end date', () => {
mount({
value: 'custom',
startDate: new Date('2022-01-01'),
endDate: new Date('2022-01-02'),
selected: {
value: 'custom',
startDate: new Date('2022-01-01'),
endDate: new Date('2022-01-02'),
},
});
expect(findDateRangesPicker().props('startOpened')).toBe(false);
@ -183,4 +202,16 @@ describe('DateRangeFilter', () => {
'2024-04-24T00:00:00.000Z',
);
});
it('sets max-date-range to maxDateRange', () => {
mount({
selected: {
value: 'custom',
startDate: new Date('2022-01-01'),
endDate: new Date('2022-01-02'),
},
maxDateRange: 7,
});
expect(findDateRangesPicker().props('maxDateRange')).toBe(7);
});
});

Some files were not shown because too many files have changed in this diff Show More