Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e4220eecca
commit
cace5e8ff1
|
@ -0,0 +1,12 @@
|
|||
glfm-verify:
|
||||
# NOTE: We do not restrict this job to any specific subset of file changes via rules, because
|
||||
# there are potentially many different source files within the codebase which could
|
||||
# change the contents of the generated GLFM files. It is therefore safer to always
|
||||
# run this job to ensure that no changes are missed.
|
||||
extends:
|
||||
- .rspec-ee-base-pg12
|
||||
stage: test
|
||||
needs: ["setup-test-env"]
|
||||
script:
|
||||
- !reference [.base-script, script]
|
||||
- bundle exec scripts/glfm/verify-all-generated-files-are-up-to-date.rb
|
|
@ -4,10 +4,14 @@ import { concatPagination } from '@apollo/client/utilities';
|
|||
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import typeDefs from '~/work_items/graphql/typedefs.graphql';
|
||||
import { WIDGET_TYPE_MILESTONE } from '~/work_items/constants';
|
||||
|
||||
export const temporaryConfig = {
|
||||
typeDefs,
|
||||
cacheConfig: {
|
||||
possibleTypes: {
|
||||
LocalWorkItemWidget: ['LocalWorkItemMilestone'],
|
||||
},
|
||||
typePolicies: {
|
||||
Project: {
|
||||
fields: {
|
||||
|
@ -18,6 +22,28 @@ export const temporaryConfig = {
|
|||
},
|
||||
WorkItem: {
|
||||
fields: {
|
||||
mockWidgets: {
|
||||
read(widgets) {
|
||||
return (
|
||||
widgets || [
|
||||
{
|
||||
__typename: 'LocalWorkItemMilestone',
|
||||
type: WIDGET_TYPE_MILESTONE,
|
||||
nodes: [
|
||||
{
|
||||
dueDate: null,
|
||||
expired: false,
|
||||
id: 'gid://gitlab/Milestone/30',
|
||||
title: 'v4.0',
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
__typename: 'Milestone',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
);
|
||||
},
|
||||
},
|
||||
widgets: {
|
||||
merge(existing = [], incoming) {
|
||||
if (existing.length === 0) {
|
||||
|
|
|
@ -51,7 +51,6 @@ export default {
|
|||
isModalVisible: false,
|
||||
isLoading: true,
|
||||
isSearchEmpty: false,
|
||||
searchEmptyMessage: '',
|
||||
targetGroup: null,
|
||||
targetParentGroup: null,
|
||||
showEmptyState: false,
|
||||
|
@ -88,10 +87,6 @@ export default {
|
|||
},
|
||||
},
|
||||
created() {
|
||||
this.searchEmptyMessage = this.hideProjects
|
||||
? COMMON_STR.GROUP_SEARCH_EMPTY
|
||||
: COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
|
||||
|
||||
eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
|
||||
eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
|
||||
eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
|
||||
|
@ -259,7 +254,7 @@ export default {
|
|||
const hasGroups = groups && groups.length > 0;
|
||||
|
||||
if (this.renderEmptyState) {
|
||||
this.isSearchEmpty = this.filterGroupsBy !== null && !hasGroups;
|
||||
this.isSearchEmpty = fromSearch && !hasGroups;
|
||||
} else {
|
||||
this.isSearchEmpty = !hasGroups;
|
||||
}
|
||||
|
@ -294,7 +289,6 @@ export default {
|
|||
v-else
|
||||
:groups="groups"
|
||||
:search-empty="isSearchEmpty"
|
||||
:search-empty-message="searchEmptyMessage"
|
||||
:page-info="pageInfo"
|
||||
:action="action"
|
||||
/>
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
<script>
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
|
||||
import { getParameterByName } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
emptyStateTitle: __('No results found'),
|
||||
emptyStateDescription: __('Edit your search and try again'),
|
||||
},
|
||||
components: {
|
||||
PaginationLinks,
|
||||
GlEmptyState,
|
||||
},
|
||||
props: {
|
||||
groups: {
|
||||
|
@ -20,10 +27,6 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
searchEmptyMessage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -43,12 +46,11 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container">
|
||||
<div
|
||||
<gl-empty-state
|
||||
v-if="searchEmpty"
|
||||
class="has-no-search-results gl-font-style-italic gl-text-center gl-text-gray-600 gl-p-5"
|
||||
>
|
||||
{{ searchEmptyMessage }}
|
||||
</div>
|
||||
:title="$options.i18n.emptyStateTitle"
|
||||
:description="$options.i18n.emptyStateDescription"
|
||||
/>
|
||||
<template v-else>
|
||||
<group-folder :groups="groups" :action="action" />
|
||||
<pagination-links
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { GlTabs, GlTab, GlSearchBoxByType, GlSorting, GlSortingItem } from '@gitlab/ui';
|
||||
import { isString, debounce } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import GroupsStore from '../store/groups_store';
|
||||
import GroupsService from '../service/groups_service';
|
||||
import {
|
||||
|
@ -61,11 +62,6 @@ export default {
|
|||
return this.isAscending ? this.sort.asc : this.sort.desc;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search: debounce(async function debouncedSearch() {
|
||||
this.handleSearchOrSortChange();
|
||||
}, 250),
|
||||
},
|
||||
mounted() {
|
||||
this.search = this.$route.query?.filter || '';
|
||||
|
||||
|
@ -137,6 +133,14 @@ export default {
|
|||
|
||||
this.handleSearchOrSortChange();
|
||||
},
|
||||
handleSearchInput(value) {
|
||||
this.search = value;
|
||||
|
||||
this.debouncedSearch();
|
||||
},
|
||||
debouncedSearch: debounce(async function debouncedSearch() {
|
||||
this.handleSearchOrSortChange();
|
||||
}, DEBOUNCE_DELAY),
|
||||
},
|
||||
i18n: {
|
||||
[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: __('Subgroups and projects'),
|
||||
|
@ -169,9 +173,10 @@ export default {
|
|||
<div class="gl-lg-display-flex gl-justify-content-end gl-mx-n2 gl-my-n2">
|
||||
<div class="gl-p-2 gl-lg-form-input-md gl-w-full">
|
||||
<gl-search-box-by-type
|
||||
v-model="search"
|
||||
:value="search"
|
||||
:placeholder="$options.i18n.searchPlaceholder"
|
||||
data-qa-selector="groups_filter_field"
|
||||
@input="handleSearchInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="gl-p-2 gl-w-full gl-lg-w-auto">
|
||||
|
|
|
@ -24,8 +24,6 @@ export const COMMON_STR = {
|
|||
EDIT_BTN_TITLE: s__('GroupsTree|Edit'),
|
||||
REMOVE_BTN_TITLE: s__('GroupsTree|Delete'),
|
||||
OPTIONS_DROPDOWN_TITLE: s__('GroupsTree|Options'),
|
||||
GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
|
||||
GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
|
||||
};
|
||||
|
||||
export const ITEM_TYPE = {
|
||||
|
|
|
@ -99,7 +99,9 @@ export function startIde(options) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (gon.features?.vscodeWebIde) {
|
||||
const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde);
|
||||
|
||||
if (useNewWebIde) {
|
||||
initGitlabWebIDE(ideElement);
|
||||
} else {
|
||||
resetServiceWorkersPublicPath();
|
||||
|
|
|
@ -7,8 +7,7 @@ export const initGitlabWebIDE = async (el) => {
|
|||
const baseUrl = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin);
|
||||
|
||||
// what: Pull what we need from the element. We will replace it soon.
|
||||
const { path_with_namespace: projectPath } = JSON.parse(el.dataset.project);
|
||||
const { cspNonce: nonce, branchName: ref } = el.dataset;
|
||||
const { cspNonce: nonce, branchName: ref, projectPath } = el.dataset;
|
||||
|
||||
// what: Clean up the element, but preserve id.
|
||||
// why: This way we don't inherit any `ide-loading` side-effects. This
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
<script>
|
||||
import { GlAvatarLabeled, GlListbox } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
|
||||
const USERS_PER_PAGE = 20;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAvatarLabeled,
|
||||
GlListbox,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
usersQuery: {
|
||||
query: searchUsersQuery,
|
||||
variables() {
|
||||
return {
|
||||
search: this.search,
|
||||
first: USERS_PER_PAGE,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data;
|
||||
},
|
||||
debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
user: '',
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
userId() {
|
||||
return getIdFromGraphQLId(this.user);
|
||||
},
|
||||
users() {
|
||||
return [
|
||||
{ text: __('(no user)'), value: '' },
|
||||
...(this.usersQuery?.users.nodes || []).map((u) => ({
|
||||
username: `@${u.username}`,
|
||||
avatarUrl: u.avatarUrl,
|
||||
text: u.name,
|
||||
value: u.id,
|
||||
})),
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clearTransform() {
|
||||
// FIXME: workaround for listbox issue
|
||||
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1986
|
||||
const { listbox } = this.$refs;
|
||||
if (listbox.querySelector('.dropdown-menu')) {
|
||||
listbox.querySelector('.dropdown-menu').style.transform = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-listbox
|
||||
ref="listbox"
|
||||
v-model="user"
|
||||
:items="users"
|
||||
searchable
|
||||
is-check-centered
|
||||
:searching="$apollo.loading"
|
||||
@click.capture.native="clearTransform"
|
||||
@search="search = $event"
|
||||
>
|
||||
<template #list-item="{ item }">
|
||||
<gl-avatar-labeled
|
||||
shape="circle"
|
||||
:size="32"
|
||||
:src="item.avatarUrl"
|
||||
:label="item.text"
|
||||
:sub-label="item.username"
|
||||
/>
|
||||
</template>
|
||||
</gl-listbox>
|
||||
<input type="hidden" :name="name" :value="userId" />
|
||||
</div>
|
||||
</template>
|
|
@ -1,3 +1,19 @@
|
|||
import UsersSelect from '~/users_select';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import UserSelect from './components/user_select.vue';
|
||||
|
||||
new UsersSelect(); // eslint-disable-line no-new
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
Array.from(document.querySelectorAll('.js-gitlab-user')).forEach(
|
||||
(node) =>
|
||||
new Vue({
|
||||
el: node,
|
||||
apolloProvider,
|
||||
render: (h) => h(UserSelect, { props: { name: node.dataset.name } }),
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -25,6 +25,8 @@ import {
|
|||
Tracking,
|
||||
IssuableAttributeState,
|
||||
IssuableAttributeType,
|
||||
LocalizedIssuableAttributeType,
|
||||
IssuableAttributeTypeKeyMap,
|
||||
issuableAttributesQueries,
|
||||
noAttributeId,
|
||||
defaultEpicSort,
|
||||
|
@ -229,7 +231,9 @@ export default {
|
|||
return timeFor(this.currentAttribute?.dueDate);
|
||||
},
|
||||
i18n() {
|
||||
return dropdowni18nText(this.issuableAttribute, this.issuableType);
|
||||
const localizedAttribute =
|
||||
LocalizedIssuableAttributeType[IssuableAttributeTypeKeyMap[this.issuableAttribute]];
|
||||
return dropdowni18nText(localizedAttribute, this.issuableType);
|
||||
},
|
||||
isEpic() {
|
||||
// MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { invert } from 'lodash';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
|
||||
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||
|
@ -251,6 +252,12 @@ export const IssuableAttributeType = {
|
|||
Milestone: 'milestone',
|
||||
};
|
||||
|
||||
export const LocalizedIssuableAttributeType = {
|
||||
Milestone: s__('Issuable|milestone'),
|
||||
};
|
||||
|
||||
export const IssuableAttributeTypeKeyMap = invert(IssuableAttributeType);
|
||||
|
||||
export const IssuableAttributeState = {
|
||||
[IssuableAttributeType.Milestone]: 'active',
|
||||
};
|
||||
|
|
|
@ -819,13 +819,14 @@ UsersSelect.prototype.renderRow = function (
|
|||
const tooltipAttributes = tooltip
|
||||
? `data-container="body" data-placement="left" data-title="${tooltip}"`
|
||||
: '';
|
||||
const dataUserSuggested = user.suggested ? `data-user-suggested=${user.suggested}` : '';
|
||||
|
||||
const name =
|
||||
user?.availability && isUserBusy(user.availability)
|
||||
? sprintf(__('%{name} (Busy)'), { name: user.name })
|
||||
: user.name;
|
||||
return `
|
||||
<li data-user-id=${user.id}>
|
||||
<li data-user-id=${user.id} ${dataUserSuggested}>
|
||||
<a href="#" class="dropdown-menu-user-link gl-display-flex! gl-align-items-center ${linkClasses}" ${tooltipAttributes}>
|
||||
${this.renderRowAvatar(issuableType, user, img)}
|
||||
<span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
|
||||
|
|
|
@ -28,7 +28,7 @@ const nonStandardEvents = {
|
|||
},
|
||||
counter: {},
|
||||
},
|
||||
testReport: {
|
||||
testSummary: {
|
||||
uniqueUser: {
|
||||
expand: ['i_testing_summary_widget_total'],
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup } from '@gitlab/ui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
import FormUrlMaskItem from './form_url_mask_item.vue';
|
||||
|
@ -11,19 +12,60 @@ export default {
|
|||
GlFormInput,
|
||||
GlFormRadio,
|
||||
GlFormRadioGroup,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
initialUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
initialUrlVariables: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
maskEnabled: false,
|
||||
url: null,
|
||||
maskEnabled: !isEmpty(this.initialUrlVariables),
|
||||
url: this.initialUrl,
|
||||
items: isEmpty(this.initialUrlVariables) ? [{}] : this.initialUrlVariables,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
maskedUrl() {
|
||||
return this.url;
|
||||
if (!this.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let maskedUrl = this.url;
|
||||
|
||||
this.items.forEach(({ key, value }) => {
|
||||
if (!key || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacementExpression = new RegExp(value, 'g');
|
||||
maskedUrl = maskedUrl.replace(replacementExpression, `{${key}}`);
|
||||
});
|
||||
|
||||
return maskedUrl;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onItemInput({ index, key, value }) {
|
||||
this.$set(this.items, index, { key, value });
|
||||
},
|
||||
addItem() {
|
||||
this.items.push({});
|
||||
},
|
||||
removeItem(index) {
|
||||
this.items.splice(index, 1);
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
addItem: s__('Webhooks|+ Mask another portion of URL'),
|
||||
radioFullUrlText: s__('Webhooks|Show full URL'),
|
||||
radioMaskUrlText: s__('Webhooks|Mask portions of URL'),
|
||||
radioMaskUrlHelp: s__('Webhooks|Do not show sensitive data such as tokens in the UI.'),
|
||||
|
@ -49,6 +91,7 @@ export default {
|
|||
v-model="url"
|
||||
name="hook[url]"
|
||||
:placeholder="$options.i18n.urlPlaceholder"
|
||||
data-testid="form-url"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<div class="gl-mt-5">
|
||||
|
@ -63,9 +106,27 @@ export default {
|
|||
</gl-form-radio-group>
|
||||
|
||||
<div v-if="maskEnabled" class="gl-ml-6" data-testid="url-mask-section">
|
||||
<form-url-mask-item :index="0" />
|
||||
<form-url-mask-item
|
||||
v-for="({ key, value }, index) in items"
|
||||
:key="index"
|
||||
:index="index"
|
||||
:item-key="key"
|
||||
:item-value="value"
|
||||
@input="onItemInput"
|
||||
@remove="removeItem"
|
||||
/>
|
||||
<div class="gl-mb-5">
|
||||
<gl-link @click="addItem">{{ $options.i18n.addItem }}</gl-link>
|
||||
</div>
|
||||
|
||||
<gl-form-group :label="$options.i18n.urlPreview" label-for="webhook-url-preview">
|
||||
<gl-form-input id="webhook-url-preview" :value="maskedUrl" readonly />
|
||||
<gl-form-input
|
||||
id="webhook-url-preview"
|
||||
:value="maskedUrl"
|
||||
readonly
|
||||
name="hook[url]"
|
||||
data-testid="form-url-preview"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,16 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
itemKey: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
itemValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
keyInputId() {
|
||||
|
@ -30,6 +40,15 @@ export default {
|
|||
inputName(type) {
|
||||
return `hook[url_variables][][${type}]`;
|
||||
},
|
||||
onKeyInput(key) {
|
||||
this.$emit('input', { index: this.index, key, value: this.itemValue });
|
||||
},
|
||||
onValueInput(value) {
|
||||
this.$emit('input', { index: this.index, key: this.itemKey, value });
|
||||
},
|
||||
onRemoveClick() {
|
||||
this.$emit('remove', this.index);
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
keyLabel: s__('Webhooks|How it looks in the UI'),
|
||||
|
@ -39,14 +58,19 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-5">
|
||||
<div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-3">
|
||||
<gl-form-group
|
||||
:label="$options.i18n.valueLabel"
|
||||
:label-for="valueInputId"
|
||||
class="gl-flex-grow-1 gl-mb-0"
|
||||
data-testid="mask-item-value"
|
||||
>
|
||||
<gl-form-input :id="valueInputId" :name="inputName('value')" />
|
||||
<gl-form-input
|
||||
:id="valueInputId"
|
||||
:name="inputName('value')"
|
||||
:value="itemValue"
|
||||
@input="onValueInput"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
:label="$options.i18n.keyLabel"
|
||||
|
@ -54,8 +78,13 @@ export default {
|
|||
class="gl-flex-grow-1 gl-mb-0"
|
||||
data-testid="mask-item-key"
|
||||
>
|
||||
<gl-form-input :id="keyInputId" :name="inputName('key')" />
|
||||
<gl-form-input
|
||||
:id="keyInputId"
|
||||
:name="inputName('key')"
|
||||
:value="itemKey"
|
||||
@input="onKeyInput"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-button icon="remove" />
|
||||
<gl-button icon="remove" :aria-label="__('Remove')" @click="onRemoveClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -8,11 +8,18 @@ export default () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { url: initialUrl, urlVariables } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'WebhookFormRoot',
|
||||
render(createElement) {
|
||||
return createElement(FormUrlApp, {});
|
||||
return createElement(FormUrlApp, {
|
||||
props: {
|
||||
initialUrl,
|
||||
initialUrlVariables: urlVariables ? JSON.parse(urlVariables) : undefined,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
WIDGET_TYPE_WEIGHT,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
WORK_ITEM_VIEWED_STORAGE_KEY,
|
||||
WIDGET_TYPE_MILESTONE,
|
||||
WIDGET_TYPE_ITERATION,
|
||||
} from '../constants';
|
||||
|
||||
|
@ -40,6 +41,7 @@ import WorkItemDescription from './work_item_description.vue';
|
|||
import WorkItemDueDate from './work_item_due_date.vue';
|
||||
import WorkItemAssignees from './work_item_assignees.vue';
|
||||
import WorkItemLabels from './work_item_labels.vue';
|
||||
import WorkItemMilestone from './work_item_milestone.vue';
|
||||
import WorkItemInformation from './work_item_information.vue';
|
||||
|
||||
export default {
|
||||
|
@ -67,6 +69,7 @@ export default {
|
|||
LocalStorageSync,
|
||||
WorkItemTypeIcon,
|
||||
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
|
||||
WorkItemMilestone,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
|
@ -208,6 +211,9 @@ export default {
|
|||
workItemIteration() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
|
||||
},
|
||||
workItemMilestone() {
|
||||
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
/** make sure that if the user has not even dismissed the alert ,
|
||||
|
@ -411,6 +417,17 @@ export default {
|
|||
:work-item-type="workItemType"
|
||||
@error="updateError = $event"
|
||||
/>
|
||||
<template v-if="workItemsMvc2Enabled">
|
||||
<work-item-milestone
|
||||
v-if="workItemMilestone"
|
||||
:work-item-id="workItem.id"
|
||||
:work-item-milestone="workItemMilestone.nodes[0]"
|
||||
:work-item-type="workItemType"
|
||||
:can-update="canUpdate"
|
||||
:full-path="fullPath"
|
||||
@error="updateError = $event"
|
||||
/>
|
||||
</template>
|
||||
<work-item-weight
|
||||
v-if="workItemWeight"
|
||||
class="gl-mb-5"
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
<script>
|
||||
import {
|
||||
GlFormGroup,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
GlSkeletonLoader,
|
||||
GlSearchBoxByType,
|
||||
GlDropdownText,
|
||||
} from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { debounce } from 'lodash';
|
||||
import Tracking from '~/tracking';
|
||||
import { s__ } from '~/locale';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
|
||||
import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
|
||||
import {
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
sprintfWorkItem,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
} from '../constants';
|
||||
|
||||
const noMilestoneId = 'no-milestone-id';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
MILESTONE: s__('WorkItem|Milestone'),
|
||||
NONE: s__('WorkItem|None'),
|
||||
MILESTONE_PLACEHOLDER: s__('WorkItem|Add to milestone'),
|
||||
NO_MATCHING_RESULTS: s__('WorkItem|No matching results'),
|
||||
NO_MILESTONE: s__('WorkItem|No milestone'),
|
||||
MILESTONE_FETCH_ERROR: s__(
|
||||
'WorkItem|Something went wrong while fetching milestones. Please try again.',
|
||||
),
|
||||
},
|
||||
components: {
|
||||
GlFormGroup,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
GlSkeletonLoader,
|
||||
GlSearchBoxByType,
|
||||
GlDropdownText,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemMilestone: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localMilestone: this.workItemMilestone,
|
||||
searchTerm: '',
|
||||
shouldFetch: false,
|
||||
updateInProgress: false,
|
||||
isFocused: false,
|
||||
milestones: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tracking() {
|
||||
return {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
label: 'item_milestone',
|
||||
property: `type_${this.workItemType}`,
|
||||
};
|
||||
},
|
||||
emptyPlaceholder() {
|
||||
return this.canUpdate ? this.$options.i18n.MILESTONE_PLACEHOLDER : this.$options.i18n.NONE;
|
||||
},
|
||||
dropdownText() {
|
||||
return this.localMilestone?.title || this.emptyPlaceholder;
|
||||
},
|
||||
isLoadingMilestones() {
|
||||
return this.$apollo.queries.milestones.loading;
|
||||
},
|
||||
isNoMilestone() {
|
||||
return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id;
|
||||
},
|
||||
dropdownClasses() {
|
||||
return {
|
||||
'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
|
||||
'is-not-focused': !this.isFocused,
|
||||
};
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
apollo: {
|
||||
milestones: {
|
||||
query: projectMilestonesQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
title: this.searchTerm,
|
||||
first: 20,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.shouldFetch;
|
||||
},
|
||||
update(data) {
|
||||
return data?.workspace?.attributes?.nodes || [];
|
||||
},
|
||||
error() {
|
||||
this.$emit('error', this.i18n.MILESTONE_FETCH_ERROR);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleMilestoneClick(milestone) {
|
||||
this.localMilestone = milestone;
|
||||
},
|
||||
onDropdownShown() {
|
||||
this.$refs.search.focusInput();
|
||||
this.shouldFetch = true;
|
||||
this.isFocused = true;
|
||||
},
|
||||
onDropdownHide() {
|
||||
this.isFocused = false;
|
||||
this.searchTerm = '';
|
||||
this.shouldFetch = false;
|
||||
this.updateMilestone();
|
||||
},
|
||||
setSearchKey(value) {
|
||||
this.searchTerm = value;
|
||||
},
|
||||
isMilestoneChecked(milestone) {
|
||||
return this.localMilestone?.id === milestone?.id;
|
||||
},
|
||||
updateMilestone() {
|
||||
if (this.workItemMilestone?.id === this.localMilestone?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.track('updated_milestone');
|
||||
this.updateInProgress = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: localUpdateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.workItemId,
|
||||
milestone: {
|
||||
milestoneId: this.localMilestone?.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.workItemUpdate.errors.length) {
|
||||
throw new Error(data.workItemUpdate.errors.join('\n'));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
|
||||
this.$emit('error', msg);
|
||||
Sentry.captureException(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.updateInProgress = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-group
|
||||
class="work-item-dropdown"
|
||||
:label="$options.i18n.MILESTONE"
|
||||
label-class="gl-pb-0! gl-overflow-wrap-break gl-mt-3"
|
||||
label-cols="3"
|
||||
label-cols-lg="2"
|
||||
>
|
||||
<span
|
||||
v-if="!canUpdate"
|
||||
class="gl-text-secondary gl-ml-4 gl-mt-3 gl-display-inline-block gl-line-height-normal"
|
||||
data-testid="disabled-text"
|
||||
>
|
||||
{{ dropdownText }}
|
||||
</span>
|
||||
<gl-dropdown
|
||||
v-else
|
||||
:toggle-class="dropdownClasses"
|
||||
:text="dropdownText"
|
||||
:loading="updateInProgress"
|
||||
@shown="onDropdownShown"
|
||||
@hide="onDropdownHide"
|
||||
>
|
||||
<template #header>
|
||||
<gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" />
|
||||
</template>
|
||||
<gl-dropdown-item
|
||||
data-testid="no-milestone"
|
||||
is-check-item
|
||||
:is-checked="isNoMilestone"
|
||||
@click="handleMilestoneClick({ id: 'no-milestone-id' })"
|
||||
>
|
||||
{{ $options.i18n.NO_MILESTONE }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-text v-if="isLoadingMilestones">
|
||||
<gl-skeleton-loader :height="90">
|
||||
<rect width="380" height="10" x="10" y="15" rx="4" />
|
||||
<rect width="280" height="10" x="10" y="30" rx="4" />
|
||||
<rect width="380" height="10" x="10" y="50" rx="4" />
|
||||
<rect width="280" height="10" x="10" y="65" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</gl-dropdown-text>
|
||||
<template v-else-if="milestones.length">
|
||||
<gl-dropdown-item
|
||||
v-for="milestone in milestones"
|
||||
:key="milestone.id"
|
||||
is-check-item
|
||||
:is-checked="isMilestoneChecked(milestone)"
|
||||
@click="handleMilestoneClick(milestone)"
|
||||
>
|
||||
{{ milestone.title }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
<gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>
|
||||
</gl-dropdown>
|
||||
</gl-form-group>
|
||||
</template>
|
|
@ -17,6 +17,7 @@ export const WIDGET_TYPE_LABELS = 'LABELS';
|
|||
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
|
||||
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
|
||||
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
|
||||
export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
|
||||
export const WIDGET_TYPE_ITERATION = 'ITERATION';
|
||||
|
||||
export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
enum LocalWidgetType {
|
||||
ASSIGNEES
|
||||
MILESTONE
|
||||
}
|
||||
|
||||
interface LocalWorkItemWidget {
|
||||
|
@ -11,6 +12,15 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget {
|
|||
nodes: [UserCore]
|
||||
}
|
||||
|
||||
type LocalWorkItemMilestone implements LocalWorkItemWidget {
|
||||
type: LocalWidgetType!
|
||||
nodes: [Milestone!]
|
||||
}
|
||||
|
||||
extend type WorkItem {
|
||||
mockWidgets: [LocalWorkItemWidget]
|
||||
}
|
||||
|
||||
input LocalUserInput {
|
||||
id: ID!
|
||||
name: String
|
||||
|
@ -19,9 +29,14 @@ input LocalUserInput {
|
|||
avatarUrl: String
|
||||
}
|
||||
|
||||
input LocalMilestoneInput {
|
||||
milestoneId: ID!
|
||||
}
|
||||
|
||||
input LocalUpdateWorkItemInput {
|
||||
id: WorkItemID!
|
||||
assignees: [LocalUserInput!]
|
||||
milestone: LocalMilestoneInput!
|
||||
}
|
||||
|
||||
type LocalWorkItemPayload {
|
||||
|
|
|
@ -3,5 +3,16 @@
|
|||
query workItem($id: WorkItemID!) {
|
||||
workItem(id: $id) {
|
||||
...WorkItem
|
||||
mockWidgets @client {
|
||||
... on LocalWorkItemMilestone {
|
||||
type
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
expired
|
||||
dueDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.work-item-iteration {
|
||||
.work-item-dropdown {
|
||||
.gl-dropdown-toggle {
|
||||
background: none !important;
|
||||
|
||||
|
@ -82,4 +82,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ class IdeController < ApplicationController
|
|||
push_frontend_feature_flag(:build_service_proxy)
|
||||
push_frontend_feature_flag(:schema_linting)
|
||||
push_frontend_feature_flag(:reject_unsigned_commits_by_gitlab)
|
||||
push_frontend_feature_flag(:vscode_web_ide, current_user)
|
||||
define_index_vars
|
||||
end
|
||||
|
||||
|
@ -27,7 +26,7 @@ class IdeController < ApplicationController
|
|||
namespace: project&.namespace, user: current_user)
|
||||
end
|
||||
|
||||
render layout: 'fullscreen', locals: { minimal: Feature.enabled?(:vscode_web_ide, current_user) }
|
||||
render layout: 'fullscreen', locals: { minimal: helpers.use_new_web_ide? }
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -56,7 +56,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
|
|||
:gitpod_enabled,
|
||||
:render_whitespace_in_code,
|
||||
:markdown_surround_selection,
|
||||
:markdown_automatic_lists
|
||||
:markdown_automatic_lists,
|
||||
:use_legacy_web_ide
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -57,7 +57,7 @@ module Types
|
|||
field :deployments,
|
||||
Types::DeploymentType.connection_type,
|
||||
null: true,
|
||||
description: 'Deployments of the environment. This field can only be resolved for one project in any single request.',
|
||||
description: 'Deployments of the environment. This field can only be resolved for one environment in any single request.',
|
||||
resolver: Resolvers::DeploymentsResolver do
|
||||
extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
|
||||
end
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module HooksHelper
|
||||
def webhook_form_data(hook)
|
||||
{
|
||||
url: hook.url,
|
||||
url_variables: nil
|
||||
}
|
||||
end
|
||||
|
||||
def link_to_test_hook(hook, trigger)
|
||||
path = test_hook_path(hook, trigger)
|
||||
trigger_human_name = trigger.to_s.tr('_', ' ').camelize
|
||||
|
|
|
@ -2,6 +2,32 @@
|
|||
|
||||
module IdeHelper
|
||||
def ide_data
|
||||
{
|
||||
'can-use-new-web-ide' => can_use_new_web_ide?.to_s,
|
||||
'use-new-web-ide' => use_new_web_ide?.to_s,
|
||||
'user-preferences-path' => profile_preferences_path,
|
||||
'branch-name' => @branch
|
||||
}.merge(use_new_web_ide? ? new_ide_data : legacy_ide_data)
|
||||
end
|
||||
|
||||
def can_use_new_web_ide?
|
||||
Feature.enabled?(:vscode_web_ide, current_user)
|
||||
end
|
||||
|
||||
def use_new_web_ide?
|
||||
can_use_new_web_ide? && !current_user.use_legacy_web_ide
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_ide_data
|
||||
{
|
||||
'project-path' => @project&.path_with_namespace,
|
||||
'csp-nonce' => content_security_policy_nonce
|
||||
}
|
||||
end
|
||||
|
||||
def legacy_ide_data
|
||||
{
|
||||
'empty-state-svg-path' => image_path('illustrations/multi_file_editor_empty.svg'),
|
||||
'no-changes-state-svg-path' => image_path('illustrations/multi-editor_no_changes_empty.svg'),
|
||||
|
@ -13,7 +39,6 @@ module IdeHelper
|
|||
'clientside-preview-enabled': Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?.to_s,
|
||||
'render-whitespace-in-code': current_user.render_whitespace_in_code.to_s,
|
||||
'codesandbox-bundler-url': Gitlab::CurrentSettings.web_ide_clientside_preview_bundler_url,
|
||||
'branch-name' => @branch,
|
||||
'default-branch' => @project && @project.default_branch,
|
||||
'file-path' => @path,
|
||||
'merge-request' => @merge_request,
|
||||
|
@ -24,13 +49,10 @@ module IdeHelper
|
|||
'web-terminal-svg-path' => image_path('illustrations/web-ide_promotion.svg'),
|
||||
'web-terminal-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'interactive-web-terminals-for-the-web-ide'),
|
||||
'web-terminal-config-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'web-ide-configuration-file'),
|
||||
'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration'),
|
||||
'csp-nonce' => content_security_policy_nonce
|
||||
'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration')
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def convert_to_project_entity_json(project)
|
||||
return unless project
|
||||
|
||||
|
|
|
@ -410,6 +410,13 @@ class ApplicationSetting < ApplicationRecord
|
|||
allow_nil: false,
|
||||
inclusion: { in: [true, false], message: N_('must be a boolean value') }
|
||||
|
||||
# rubocop:disable Cop/StaticTranslationDefinition
|
||||
validates :deactivate_dormant_users_period,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 90, message: _("'%{value}' days of inactivity must be greater than or equal to 90") },
|
||||
if: :deactivate_dormant_users?
|
||||
# rubocop:enable Cop/StaticTranslationDefinition
|
||||
|
||||
Gitlab::SSHPublicKey.supported_types.each do |type|
|
||||
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
|
||||
end
|
||||
|
|
|
@ -1364,9 +1364,9 @@ module Ci
|
|||
self.builds.latest.build_matchers(project)
|
||||
end
|
||||
|
||||
def authorized_cluster_agents
|
||||
strong_memoize(:authorized_cluster_agents) do
|
||||
::Clusters::AgentAuthorizationsFinder.new(project).execute.map(&:agent)
|
||||
def cluster_agent_authorizations
|
||||
strong_memoize(:cluster_agent_authorizations) do
|
||||
::Clusters::AgentAuthorizationsFinder.new(project).execute
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ module Clusters
|
|||
end
|
||||
|
||||
def config
|
||||
nil
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -354,6 +354,7 @@ class User < ApplicationRecord
|
|||
:markdown_automatic_lists, :markdown_automatic_lists=,
|
||||
:diffs_deletion_color, :diffs_deletion_color=,
|
||||
:diffs_addition_color, :diffs_addition_color=,
|
||||
:use_legacy_web_ide, :use_legacy_web_ide=,
|
||||
to: :user_preference
|
||||
|
||||
delegate :path, to: :namespace, allow_nil: true, prefix: true
|
||||
|
|
|
@ -22,6 +22,7 @@ class UserPreference < ApplicationRecord
|
|||
validates :diffs_deletion_color, :diffs_addition_color,
|
||||
format: { with: ColorsHelper::HEX_COLOR_PATTERN },
|
||||
allow_blank: true
|
||||
validates :use_legacy_web_ide, allow_nil: false, inclusion: { in: [true, false] }
|
||||
|
||||
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
|
||||
|
||||
|
|
|
@ -34,7 +34,9 @@ module Ci
|
|||
|
||||
def runner_variables
|
||||
stop_expanding_file_vars = ::Feature.enabled?(:ci_stop_expanding_file_vars_for_runners, project)
|
||||
variables.sort_and_expand_all(keep_undefined: true, expand_file_vars: !stop_expanding_file_vars).to_runner_variables
|
||||
variables
|
||||
.sort_and_expand_all(keep_undefined: true, expand_file_vars: !stop_expanding_file_vars, project: project)
|
||||
.to_runner_variables
|
||||
end
|
||||
|
||||
def refspecs
|
||||
|
|
|
@ -22,8 +22,9 @@ module BulkImports
|
|||
subdir_path = export_subdir_path(upload)
|
||||
mkdir_p(subdir_path)
|
||||
download_or_copy_upload(uploader, File.join(subdir_path, uploader.filename))
|
||||
rescue Errno::ENAMETOOLONG => e
|
||||
# Do not fail entire export process if downloaded file has filename that exceeds 255 characters.
|
||||
rescue StandardError => e
|
||||
# Do not fail entire project export if something goes wrong during file download
|
||||
# (e.g. downloaded file has filename that exceeds 255 characters).
|
||||
# Ignore raised exception, skip such upload, log the error and keep going with the export instead.
|
||||
Gitlab::ErrorTracking.log_exception(e, portable_id: portable.id, portable_class: portable.class.name, upload_id: upload.id)
|
||||
end
|
||||
|
|
|
@ -14,7 +14,8 @@ module Ci
|
|||
url: Gitlab::Kas.tunnel_url
|
||||
)
|
||||
|
||||
agents.each do |agent|
|
||||
agent_authorizations.each do |authorization|
|
||||
agent = authorization.agent
|
||||
user = user_name(agent)
|
||||
|
||||
template.add_user(
|
||||
|
@ -24,6 +25,7 @@ module Ci
|
|||
|
||||
template.add_context(
|
||||
name: context_name(agent),
|
||||
namespace: context_namespace(authorization),
|
||||
cluster: cluster_name,
|
||||
user: user
|
||||
)
|
||||
|
@ -36,8 +38,8 @@ module Ci
|
|||
|
||||
attr_reader :pipeline, :token, :template
|
||||
|
||||
def agents
|
||||
pipeline.authorized_cluster_agents
|
||||
def agent_authorizations
|
||||
pipeline.cluster_agent_authorizations
|
||||
end
|
||||
|
||||
def cluster_name
|
||||
|
@ -52,6 +54,10 @@ module Ci
|
|||
[agent.project.full_path, agent.name].join(delimiter)
|
||||
end
|
||||
|
||||
def context_namespace(authorization)
|
||||
authorization.config['default_namespace']
|
||||
end
|
||||
|
||||
def agent_token(agent)
|
||||
['ci', agent.id, token].join(delimiter)
|
||||
end
|
||||
|
|
|
@ -54,10 +54,10 @@
|
|||
- dormant_users_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: dormant_users_help_link }
|
||||
= f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after a period of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more.%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe }
|
||||
.form-group
|
||||
= f.label :deactivate_dormant_users_period, _('Period of inactivity (days)'), class: 'label-light'
|
||||
= f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '1'
|
||||
= f.label :deactivate_dormant_users_period, _('Days of inactivity before deactivation'), class: 'label-light'
|
||||
= f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '90', step: '1'
|
||||
.form-text.text-muted
|
||||
= _('Period of inactivity before deactivation.')
|
||||
= _('Must be 90 days or more.')
|
||||
|
||||
.form-group
|
||||
= f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light'
|
||||
|
|
|
@ -23,23 +23,21 @@
|
|||
%p
|
||||
= html_escape(_('Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. "By %{link_open}@johnsmith%{link_close}"). It will also associate and/or assign these issues and comments with the selected user.')) % { link_open: '<a href="#">'.html_safe, link_close: '</a>'.html_safe }
|
||||
|
||||
.table-holder
|
||||
%table.table
|
||||
%thead
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
%th= _("ID")
|
||||
%th= _("Name")
|
||||
%th= _("Email")
|
||||
%th= _("GitLab User")
|
||||
%tbody
|
||||
- @user_map.each do |id, user|
|
||||
%tr
|
||||
%th= _("ID")
|
||||
%th= _("Name")
|
||||
%th= _("Email")
|
||||
%th= _("GitLab User")
|
||||
%tbody
|
||||
- @user_map.each do |id, user|
|
||||
%tr
|
||||
%td= id
|
||||
%td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
|
||||
%td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
|
||||
%td
|
||||
= users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control',
|
||||
scope: :all, email_user: true, selected: user[:gitlab_user])
|
||||
%td= id
|
||||
%td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control gl-form-input'
|
||||
%td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control gl-form-input'
|
||||
%td
|
||||
.js-gitlab-user{ data: { name: "users[#{id}][gitlab_user]" } }
|
||||
|
||||
.form-actions
|
||||
= submit_tag _('Continue to the next step'), class: 'gl-button btn btn-confirm'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
= form_errors(hook)
|
||||
|
||||
- if Feature.enabled?(:webhook_form_mask_url)
|
||||
.js-vue-webhook-form
|
||||
.js-vue-webhook-form{ data: webhook_form_data(hook) }
|
||||
- else
|
||||
.form-group
|
||||
= form.label :url, s_('Webhooks|URL'), class: 'label-bold'
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTargetsToElasticReindexingTasks < Gitlab::Database::Migration[2.0]
|
||||
def change
|
||||
add_column :elastic_reindexing_tasks, :targets, :text, array: true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUseLegacyWebIdeToUserPreferences < Gitlab::Database::Migration[2.0]
|
||||
enable_lock_retries!
|
||||
|
||||
def change
|
||||
add_column :user_preferences, :use_legacy_web_ide, :boolean, default: false, null: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateInvalidDormantUserSetting < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
def up
|
||||
execute("update application_settings set deactivate_dormant_users_period=90 where deactivate_dormant_users_period < 90")
|
||||
end
|
||||
# rubocop:enable Layout/LineLength
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
ae45bc7d67354b64e359ac7fadefec6a0d81cd529f5ae2517a6a6a5d250f9024
|
|
@ -0,0 +1 @@
|
|||
3c2445871613743560b2dd0a111fafab30f503b1c462e7ba7aee03f85e25f775
|
|
@ -0,0 +1 @@
|
|||
dbf241baf6d3deb1ef29a7cdca012050cab51c5f86762a0363d9dc4dc14fd804
|
|
@ -14981,6 +14981,7 @@ CREATE TABLE elastic_reindexing_tasks (
|
|||
delete_original_index_at timestamp with time zone,
|
||||
max_slices_running smallint DEFAULT 60 NOT NULL,
|
||||
slice_multiplier smallint DEFAULT 2 NOT NULL,
|
||||
targets text[],
|
||||
CONSTRAINT check_7f64acda8e CHECK ((char_length(error_message) <= 255))
|
||||
);
|
||||
|
||||
|
@ -22272,6 +22273,7 @@ CREATE TABLE user_preferences (
|
|||
diffs_deletion_color text,
|
||||
diffs_addition_color text,
|
||||
markdown_automatic_lists boolean DEFAULT true NOT NULL,
|
||||
use_legacy_web_ide boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT check_89bf269f41 CHECK ((char_length(diffs_deletion_color) <= 7)),
|
||||
CONSTRAINT check_d07ccd35f7 CHECK ((char_length(diffs_addition_color) <= 7))
|
||||
);
|
||||
|
|
|
@ -61,6 +61,8 @@ verification methods:
|
|||
| Blobs | CI Secure Files _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
|
||||
| Blobs | Incident Metric Images _(file system)_ | Geo with API/Managed | SHA256 checksum |
|
||||
| Blobs | Incident Metric Images _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
|
||||
| Blobs | Alert Metric Images _(file system)_ | Geo with API | SHA256 checksum |
|
||||
| Blobs | Alert Metric Images _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
|
||||
|
||||
- (*1*): Redis replication can be used as part of HA with Redis sentinel. It's not used between Geo sites.
|
||||
- (*2*): Object storage replication can be performed by Geo or by your object storage provider/appliance
|
||||
|
@ -207,10 +209,10 @@ Requires additional configuration. See [instructions](container_registry.md) to
|
|||
|[Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | **Yes** (13.12) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_terraform_state_version_replication`, enabled by default. Verification was behind the feature flag `geo_terraform_state_version_verification`, which was removed in 14.0. |
|
||||
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_merge_request_diff_replication`, enabled by default. Verification was behind the feature flag `geo_merge_request_diff_verification`, removed in 14.7.|
|
||||
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | N/A | N/A | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
|
||||
| [GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification was behind the feature flag `geo_pages_deployment_verification`, removed in 14.7. |
|
||||
| [Project-level Secure files](../../../ci/secure_files/index.md) | **Yes** (15.3) | **Yes** (15.3) | **Yes** (15.3) | [No](object_storage.md#verification-of-files-in-object-storage) | |
|
||||
| [Incident Metric Images](../../../operations/incident_management/incidents.md#metrics) | **Yes** (15.5) | **Yes**(15.5) | Yes | Yes | Replication/Verification is handled via the Uploads data type |
|
||||
|[Alert Metric Images](../../../operations/incident_management/alerts.md#metrics-tab) | [Planned](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | No | No | |
|
||||
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification was behind the feature flag `geo_pages_deployment_verification`, removed in 14.7. |
|
||||
|[Project-level Secure files](../../../ci/secure_files/index.md) | **Yes** (15.3) | **Yes** (15.3) | **Yes** (15.3) | [No](object_storage.md#verification-of-files-in-object-storage) | |
|
||||
| [Incident Metric Images](../../../operations/incident_management/incidents.md#metrics) | **Yes** (15.5) | **Yes**(15.5) | **Yes** (15.5) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication/Verification is handled via the Uploads data type. | |
|
||||
|[Alert Metric Images](../../../operations/incident_management/alerts.md#metrics-tab) | **Yes** (15.5) | **Yes** (15.5) | **Yes** (15.5) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication/Verification is handled via the Uploads data type. |
|
||||
|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | N/A | N/A | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
|
||||
|[Elasticsearch integration](../../../integration/advanced_search/elasticsearch.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | No | Not planned because further product discovery is required and Elasticsearch (ES) clusters can be rebuilt. Secondaries use the same ES cluster as the primary. |
|
||||
|[Dependency proxy images](../../../user/packages/dependency_proxy/index.md) | [Planned](https://gitlab.com/groups/gitlab-org/-/epics/8833) | No | No | No | Blocked by [Geo: Secondary Mimicry](https://gitlab.com/groups/gitlab-org/-/epics/1528). Replication of this cache is not needed for disaster recovery purposes because it can be recreated from external sources. |
|
||||
|
|
|
@ -240,6 +240,24 @@ project.id
|
|||
# => 2537
|
||||
```
|
||||
|
||||
## Time an operation
|
||||
|
||||
If you'd like to time one or more operations, use the following format, replacing
|
||||
the placeholder `<operation>` with your Ruby or Rails commands of choice:
|
||||
|
||||
```ruby
|
||||
# A single operation
|
||||
Benchmark.measure { <operation> }
|
||||
|
||||
# A breakdown of multiple operations
|
||||
Benchmark.bm do |x|
|
||||
x.report(:label1) { <operation_1> }
|
||||
x.report(:label2) { <operation_2> }
|
||||
end
|
||||
```
|
||||
|
||||
For more information, review [our developer documentation about benchmarks](../../development/performance.md#benchmarks).
|
||||
|
||||
## Active Record objects
|
||||
|
||||
### Looking up database-persisted objects
|
||||
|
|
|
@ -62,19 +62,6 @@ Notify.test_email(e, "Test email for #{n}", 'Test email').deliver_now
|
|||
Notify.test_email(u.email, "Test email for #{u.name}", 'Test email').deliver_now
|
||||
```
|
||||
|
||||
## Time an operation
|
||||
|
||||
```ruby
|
||||
# A single operation
|
||||
Benchmark.measure { <operation> }
|
||||
|
||||
# A breakdown of multiple operations
|
||||
Benchmark.bm do |x|
|
||||
x.report(:label1) { <operation_1> }
|
||||
x.report(:label2) { <operation_2> }
|
||||
end
|
||||
```
|
||||
|
||||
## Imports and exports
|
||||
|
||||
### Import a project
|
||||
|
|
|
@ -11916,7 +11916,7 @@ Describes where code is deployed for a project.
|
|||
|
||||
##### `Environment.deployments`
|
||||
|
||||
Deployments of the environment. This field can only be resolved for one project in any single request.
|
||||
Deployments of the environment. This field can only be resolved for one environment in any single request.
|
||||
|
||||
Returns [`DeploymentConnection`](#deploymentconnection).
|
||||
|
||||
|
|
|
@ -25,12 +25,12 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `module_namespace` | string | yes | The group to which Terraform module's project belongs. |
|
||||
| `module_namespace` | string | yes | The top-level group (namespace) to which Terraform module's project or subgroup belongs.|
|
||||
| `module_name` | string | yes | The module name. |
|
||||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/versions"
|
||||
curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/versions"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
@ -88,7 +88,7 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system
|
|||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local"
|
||||
curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
@ -127,7 +127,7 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
|
|||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0"
|
||||
curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
@ -166,7 +166,7 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
|
|||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/download"
|
||||
curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/download"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
@ -195,7 +195,7 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
|
|||
| `module_version` | string | yes | Specific module version to download. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/download"
|
||||
curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/download"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
@ -220,11 +220,11 @@ GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/
|
|||
| `module_version` | string | yes | Specific module version to download. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file"
|
||||
curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file"
|
||||
```
|
||||
|
||||
To write the output to file:
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file" --output hello-world-local.tgz
|
||||
curl --header "Authorization: Bearer <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file" --output hello-world-local.tgz
|
||||
```
|
||||
|
|
|
@ -9,16 +9,19 @@ type: reference, api
|
|||
|
||||
## List repository tree
|
||||
|
||||
> Iterating pages of results with a number (`?page=2`) [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67509) in GitLab 14.3.
|
||||
|
||||
Get a list of repository files and directories in a project. This endpoint can
|
||||
be accessed without authentication if the repository is publicly accessible.
|
||||
|
||||
This command provides essentially the same functionality as the `git ls-tree` command. For more information, see the section _Tree Objects_ in the [Git internals documentation](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects/#_tree_objects).
|
||||
This command provides essentially the same features as the `git ls-tree`
|
||||
command. For more information, refer to the section
|
||||
[Tree Objects](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects/#_tree_objects)
|
||||
in the Git internals documentation.
|
||||
|
||||
WARNING:
|
||||
This endpoint is changing to keyset-based pagination. Iterating pages of results
|
||||
with a number (`?page=2`) [is deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67509).
|
||||
Support for iterating with a number became supported in GitLab 15.0. Use
|
||||
the new [keyset pagination system](index.md#keyset-based-pagination) instead.
|
||||
This endpoint changed to [keyset-based pagination](index.md#keyset-based-pagination)
|
||||
in GitLab 15.0. Iterating pages of results with a number (`?page=2`) is unsupported.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/repository/tree
|
||||
|
@ -29,12 +32,12 @@ Supported attributes:
|
|||
| Attribute | Type | Required | Description |
|
||||
| :---------- | :------------- | :------- | :---------- |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `path` | string | no | The path inside repository. Used to get content of subdirectories. |
|
||||
| `ref` | string | no | The name of a repository branch or tag or if not given the default branch. |
|
||||
| `recursive` | boolean | no | Boolean value used to get a recursive tree (false by default). |
|
||||
| `per_page` | integer | no | Number of results to show per page. If not specified, defaults to `20`. [Learn more on pagination](index.md#pagination). |
|
||||
| `pagination` | string | no | If set to `keyset`, use the new keyset pagination method. |
|
||||
| `page_token` | string | no | The tree record ID at which to fetch the next page. Used only with keyset pagination. |
|
||||
| `pagination` | string | no | If `keyset`, use the [keyset-based pagination method](index.md#keyset-based-pagination). |
|
||||
| `path` | string | no | The path inside the repository. Used to get content of subdirectories. |
|
||||
| `per_page` | integer | no | Number of results to show per page. If not specified, defaults to `20`. [Learn more on pagination](index.md#pagination). |
|
||||
| `recursive` | boolean | no | Boolean value used to get a recursive tree. Default is `false`. |
|
||||
| `ref` | string | no | The name of a repository branch or tag or, if not given, the default branch. |
|
||||
|
||||
```json
|
||||
[
|
||||
|
@ -92,9 +95,9 @@ Supported attributes:
|
|||
|
||||
## Get a blob from repository
|
||||
|
||||
Allows you to receive information about blob in repository like size and
|
||||
content. Blob content is Base64 encoded. This endpoint can be accessed
|
||||
without authentication if the repository is publicly accessible.
|
||||
Allows you to receive information, such as size and content, about blobs in a repository.
|
||||
Blob content is Base64 encoded. This endpoint can be accessed without authentication,
|
||||
if the repository is publicly accessible.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/repository/blobs/:sha
|
||||
|
@ -109,7 +112,7 @@ Supported attributes:
|
|||
|
||||
## Raw blob content
|
||||
|
||||
Get the raw file contents for a blob by blob SHA. This endpoint can be accessed
|
||||
Get the raw file contents for a blob, by blob SHA. This endpoint can be accessed
|
||||
without authentication if the repository is publicly accessible.
|
||||
|
||||
```plaintext
|
||||
|
@ -131,24 +134,32 @@ Supported attributes:
|
|||
Get an archive of the repository. This endpoint can be accessed without
|
||||
authentication if the repository is publicly accessible.
|
||||
|
||||
This endpoint has a rate limit threshold of 5 requests per minute for GitLab.com users.
|
||||
For GitLab.com users, this endpoint has a rate limit threshold of 5 requests per minute.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/repository/archive[.format]
|
||||
```
|
||||
|
||||
`format` is an optional suffix for the archive format. Default is
|
||||
`tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, `tbz2`, `tb2`,
|
||||
`bz2`, `tar`, and `zip`. For example, specifying `archive.zip`
|
||||
would send an archive in ZIP format.
|
||||
`format` is an optional suffix for the archive format, and defaults to
|
||||
`tar.gz`. For example, specifying `archive.zip` sends an archive in ZIP format.
|
||||
Available options are:
|
||||
|
||||
- `bz2`
|
||||
- `tar`
|
||||
- `tar.bz2`
|
||||
- `tar.gz`
|
||||
- `tb2`
|
||||
- `tbz`
|
||||
- `tbz2`
|
||||
- `zip`
|
||||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:------------|:---------------|:---------|:----------------------|
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `sha` | string | no | The commit SHA to download. A tag, branch reference, or SHA can be used. This defaults to the tip of the default branch if not specified. |
|
||||
| `path` | string | no | The subpath of the repository to download. This defaults to the whole repository (empty string). |
|
||||
| `path` | string | no | The subpath of the repository to download. If an empty string, defaults to the whole repository. |
|
||||
| `sha` | string | no | The commit SHA to download. A tag, branch reference, or SHA can be used. If not specified, defaults to the tip of the default branch. |
|
||||
|
||||
Example request:
|
||||
|
||||
|
@ -159,7 +170,8 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.com/api/v4/pr
|
|||
## Compare branches, tags or commits
|
||||
|
||||
This endpoint can be accessed without authentication if the repository is
|
||||
publicly accessible. Diffs can have an empty diff string if [diff limits](../development/diffs.md#diff-limits) are reached.
|
||||
publicly accessible. Diffs can have an empty diff string if
|
||||
[diff limits](../development/diffs.md#diff-limits) are reached.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/repository/compare
|
||||
|
@ -172,8 +184,8 @@ Supported attributes:
|
|||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `from` | string | yes | The commit SHA or branch name. |
|
||||
| `to` | string | yes | The commit SHA or branch name. |
|
||||
| `from_project_id` | integer | no | The ID to compare from |
|
||||
| `straight` | boolean | no | Comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)'. Default is `false`. |
|
||||
| `from_project_id` | integer | no | The ID to compare from. |
|
||||
| `straight` | boolean | no | Comparison method: `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)'. Default is `false`. |
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/repository/compare?from=master&to=feature
|
||||
|
@ -217,6 +229,9 @@ Example response:
|
|||
|
||||
## Contributors
|
||||
|
||||
> - Attributes `additions` and `deletions` [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39653) in GitLab 13.4, because they [always returned `0`](https://gitlab.com/gitlab-org/gitlab/-/issues/233119).
|
||||
> - Attributes `additions` and `deletions` [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38920) in GitLab 14.0.
|
||||
|
||||
Get repository contributors list. This endpoint can be accessed without
|
||||
authentication if the repository is publicly accessible.
|
||||
|
||||
|
@ -224,9 +239,6 @@ authentication if the repository is publicly accessible.
|
|||
GET /projects/:id/repository/contributors
|
||||
```
|
||||
|
||||
WARNING:
|
||||
The `additions` and `deletions` attributes are [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39653) as of GitLab 13.4, because they [always return `0`](https://gitlab.com/gitlab-org/gitlab/-/issues/233119).
|
||||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
@ -255,16 +267,16 @@ Example response:
|
|||
|
||||
## Merge Base
|
||||
|
||||
Get the common ancestor for 2 or more refs (commit SHAs, branch names or tags).
|
||||
Get the common ancestor for 2 or more refs, such as commit SHAs, branch names, or tags.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/repository/merge_base
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | -------------- | -------- | ------------------------------------------------------------------------------- |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) |
|
||||
| `refs` | array | yes | The refs to find the common ancestor of, multiple refs can be passed |
|
||||
| --------- | -------------- | -------- | ---------------------------------------------------------------------------------- |
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). |
|
||||
| `refs` | array | yes | The refs to find the common ancestor of. Accepts multiple refs. |
|
||||
|
||||
Example request:
|
||||
|
||||
|
@ -293,17 +305,16 @@ Example response:
|
|||
|
||||
## Add changelog data to a changelog file
|
||||
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/351) in GitLab 13.9.
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/351) in GitLab 13.9.
|
||||
> - Commit range limits [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89032) in GitLab 15.1 [with a flag](../administration/feature_flags.md) named `changelog_commits_limitation`. Enabled by default.
|
||||
|
||||
Generate changelog data based on commits in a repository.
|
||||
|
||||
Given a version (using [semantic versioning](https://semver.org/)) and a range
|
||||
Given a [semantic version](https://semver.org/) and a range
|
||||
of commits, GitLab generates a changelog for all commits that use a particular
|
||||
[Git trailer](https://git-scm.com/docs/git-interpret-trailers).
|
||||
|
||||
The output of this process is a new section in a changelog file in the Git
|
||||
repository of the given project. The output format is in Markdown, and can be
|
||||
customized.
|
||||
[Git trailer](https://git-scm.com/docs/git-interpret-trailers). GitLab adds
|
||||
a new Markdown-formatted section to a changelog file in the Git repository of
|
||||
the project. The output format can be customized.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/repository/changelog
|
||||
|
@ -314,30 +325,21 @@ Supported attributes:
|
|||
| Attribute | Type | Required | Description |
|
||||
| :-------- | :------- | :--------- | :---------- |
|
||||
| `version` | string | yes | The version to generate the changelog for. The format must follow [semantic versioning](https://semver.org/). |
|
||||
| `from` | string | no | The start of the range of commits (as a SHA) to use for generating the changelog. This commit itself isn't included in the list. |
|
||||
| `to` | string | no | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. Defaults to the branch specified in the `branch` attribute. |
|
||||
| `date` | datetime | no | The date and time of the release, defaults to the current time. |
|
||||
| `branch` | string | no | The branch to commit the changelog changes to, defaults to the project's default branch. |
|
||||
| `trailer` | string | no | The Git trailer to use for including commits, defaults to `Changelog`. |
|
||||
| `branch` | string | no | The branch to commit the changelog changes to. Defaults to the project's default branch. |
|
||||
| `config_file` | string | no | Path to the changelog configuration file in the project's Git repository. Defaults to `.gitlab/changelog_config.yml`. |
|
||||
| `file` | string | no | The file to commit the changes to, defaults to `CHANGELOG.md`. |
|
||||
| `message` | string | no | The commit message to produce when committing the changes, defaults to `Add changelog for version X` where X is the value of the `version` argument. |
|
||||
| `date` | datetime | no | The date and time of the release. Defaults to the current time. |
|
||||
| `file` | string | no | The file to commit the changes to. Defaults to `CHANGELOG.md`. |
|
||||
| `from` | string | no | The SHA of the commit that marks the beginning of the range of commits to include in the changelog. This commit isn't included in the changelog. |
|
||||
| `message` | string | no | The commit message to use when committing the changes. Defaults to `Add changelog for version X`, where `X` is the value of the `version` argument. |
|
||||
| `to` | string | no | The SHA of the commit that marks the end of the range of commits to include in the changelog. This commit _is_ included in the changelog. Defaults to the branch specified in the `branch` attribute. Limited to 15000 commits unless the feature flag `changelog_commits_limitation` is disabled. |
|
||||
| `trailer` | string | no | The Git trailer to use for including commits. Defaults to `Changelog`. Case-sensitive: `Example` does not match `example` or `eXaMpLE`. |
|
||||
|
||||
WARNING:
|
||||
GitLab treats trailers case-sensitively. If you set the `trailer` field to
|
||||
`Example`, GitLab _won't_ include commits that use the trailer `example`,
|
||||
`eXaMpLE`, or anything else that isn't _exactly_ `Example`.
|
||||
|
||||
WARNING:
|
||||
The allowed commits range between `from` and `to` is limited to 15000 commits. To disable
|
||||
this restriction, [turn off the feature flag](../administration/feature_flags.md)
|
||||
`changelog_commits_limitation`.
|
||||
### Requirements for `from` attribute
|
||||
|
||||
If the `from` attribute is unspecified, GitLab uses the Git tag of the last
|
||||
stable version that came before the version specified in the `version`
|
||||
attribute. This requires that Git tag names follow a specific format, allowing
|
||||
GitLab to extract a version from the tag names. By default, GitLab considers
|
||||
tags using these formats:
|
||||
attribute. For GitLab to extract version numbers from tag names, Git tag names
|
||||
must follow a specific format. By default, GitLab considers tags using these formats:
|
||||
|
||||
- `vX.Y.Z`
|
||||
- `X.Y.Z`
|
||||
|
@ -350,7 +352,7 @@ For example, consider a project with the following tags:
|
|||
- v1.1.0
|
||||
- v2.0.0
|
||||
|
||||
If the `version` attribute is `2.1.0`, GitLab uses tag v2.0.0. And when the
|
||||
If the `version` attribute is `2.1.0`, GitLab uses tag `v2.0.0`. And when the
|
||||
version is `1.1.1`, or `1.2.0`, GitLab uses tag v1.1.0. The tag `v1.0.0-pre1` is
|
||||
never used, because pre-release tags are ignored.
|
||||
|
||||
|
@ -372,7 +374,8 @@ This command generates a changelog for version `1.0.0`.
|
|||
The commit range:
|
||||
|
||||
- Starts with the tag of the last release.
|
||||
- Ends with the last commit on the target branch. The default target branch is the project's default branch.
|
||||
- Ends with the last commit on the target branch. The default target branch is
|
||||
the project's default branch.
|
||||
|
||||
If the last tag is `v0.9.0` and the default branch is `main`, the range of commits
|
||||
included in this example is `v0.9.0..main`:
|
||||
|
@ -638,28 +641,28 @@ At the top level, the following variable is available:
|
|||
|
||||
In a category, the following variables are available:
|
||||
|
||||
- `title`: the title of the category (after it has been remapped).
|
||||
- `count`: the number of entries in this category.
|
||||
- `entries`: the entries that belong to this category.
|
||||
- `single_change`: a boolean that indicates if there is only one change (`true`),
|
||||
or multiple changes (`false`).
|
||||
- `entries`: the entries that belong to this category.
|
||||
- `title`: the title of the category (after it has been remapped).
|
||||
|
||||
In an entry, the following variables are available (here `foo.bar` means that
|
||||
`bar` is a sub-field of `foo`):
|
||||
|
||||
- `title`: the title of the changelog entry (this is the commit title).
|
||||
- `commit.reference`: a reference to the commit, for example,
|
||||
`gitlab-org/gitlab@0a4cdd86ab31748ba6dac0f69a8653f206e5cfc7`.
|
||||
- `commit.trailers`: an object containing all the Git trailers that were present
|
||||
in the commit body.
|
||||
- `author.reference`: a reference to the commit author (for example, `@alice`).
|
||||
- `author.contributor`: a boolean set to `true` when the author is not a project
|
||||
member, otherwise `false`.
|
||||
- `author.credit`: a boolean set to `true` when `author.contributor` is `true` or
|
||||
when `include_groups` is configured, and the author is a member of one of the
|
||||
groups.
|
||||
- `author.reference`: a reference to the commit author (for example, `@alice`).
|
||||
- `commit.reference`: a reference to the commit, for example,
|
||||
`gitlab-org/gitlab@0a4cdd86ab31748ba6dac0f69a8653f206e5cfc7`.
|
||||
- `commit.trailers`: an object containing all the Git trailers that were present
|
||||
in the commit body.
|
||||
- `merge_request.reference`: a reference to the merge request that first
|
||||
introduced the change (for example, `gitlab-org/gitlab!50063`).
|
||||
- `title`: the title of the changelog entry (this is the commit title).
|
||||
|
||||
The `author` and `merge_request` objects might not be present if the data
|
||||
couldn't be determined. For example, when a commit is created without a
|
||||
|
@ -732,11 +735,11 @@ Supported attributes:
|
|||
| Attribute | Type | Required | Description |
|
||||
| :-------- | :------- | :--------- | :---------- |
|
||||
| `version` | string | yes | The version to generate the changelog for. The format must follow [semantic versioning](https://semver.org/). |
|
||||
| `config_file` | string | no | The path of changelog configuration file in the project's Git repository, defaults to `.gitlab/changelog_config.yml`. |
|
||||
| `date` | datetime | no | The date and time of the release, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z`. Defaults to the current time. |
|
||||
| `from` | string | no | The start of the range of commits (as a SHA) to use for generating the changelog. This commit itself isn't included in the list. |
|
||||
| `to` | string | no | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. Defaults to the branch specified in the `branch` attribute. |
|
||||
| `date` | datetime | no | The date and time of the release, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z`. Defaults to the current time. |
|
||||
| `trailer` | string | no | The Git trailer to use for including commits, defaults to `Changelog`. |
|
||||
| `config_file` | string | no | The path of changelog configuration file in the project's Git repository, defaults to `.gitlab/changelog_config.yml`. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: token" "https://gitlab.com/api/v4/projects/42/repository/changelog?version=1.0.0"
|
||||
|
|
|
@ -738,6 +738,20 @@ subgraph output:<br/>test results/output
|
|||
end
|
||||
```
|
||||
|
||||
#### `verify-all-generated-files-are-up-to-date.rb` script
|
||||
|
||||
The `scripts/glfm/verify-all-generated-files-are-up-to-date.rb` script
|
||||
runs the [`update-specification.rb`](#update-specificationrb-script).
|
||||
[`update-example-snapshots.rb`](#update-example-snapshotsrb-script) scripts,
|
||||
It fails with an exception and non-zero return code if running these scripts
|
||||
results in any diffs to the generated and committed
|
||||
[output specification files](#output-specification-files) or
|
||||
[example snapshot files](#example-snapshot-files).
|
||||
|
||||
This script is run via the `glfm-verify` CI job to ensure that all changes to the
|
||||
[input specification files](#input-specification-files)
|
||||
are reflected in the generated output specification and example snapshot files.
|
||||
|
||||
### Specification files
|
||||
|
||||
These files represent the GLFM specification itself. They are all
|
||||
|
|
|
@ -391,7 +391,7 @@ We store these results also when running nightly scheduled CI jobs on the
|
|||
default branch on `gitlab.com`. Statistics of these profiling data are
|
||||
[available online](https://gitlab-org.gitlab.io/rspec_profiling_stats/). For
|
||||
example, you can find which tests take longest to run or which execute the most
|
||||
queries. This can be handy for optimizing our tests or identifying performance
|
||||
queries. Use this to optimize our tests or identify performance
|
||||
issues in our code.
|
||||
|
||||
## Memory optimization
|
||||
|
|
|
@ -171,11 +171,12 @@ Users can also be deactivated using the [GitLab API](../../api/users.md#deactiva
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320875) in GitLab 14.0.
|
||||
> - Customizable time period [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336747) in GitLab 15.4
|
||||
> - The lower limit for inactive period set to 90 days [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100793) in GitLab 15.5
|
||||
|
||||
Administrators can enable automatic deactivation of users who either:
|
||||
|
||||
- Were created more than a week ago and have not signed in.
|
||||
- Have no activity for a specified period of time (defaults to 90 days).
|
||||
- Have no activity for a specified period of time (default and minimum is 90 days).
|
||||
|
||||
To do this:
|
||||
|
||||
|
@ -183,7 +184,7 @@ To do this:
|
|||
1. On the left sidebar, select **Settings > General**.
|
||||
1. Expand the **Account and limit** section.
|
||||
1. Under **Dormant users**, check **Deactivate dormant users after a period of inactivity**.
|
||||
1. Under **Period of inactivity (days)**, enter a period of time before deactivation.
|
||||
1. Under **Days of inactivity before deactivation**, enter the number of days before deactivation. Minimum value is 90 days.
|
||||
1. Select **Save changes**.
|
||||
|
||||
When this feature is enabled, GitLab runs a job once a day to deactivate the dormant users.
|
||||
|
|
|
@ -12,7 +12,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
GitLab allows you to import all the required Git repositories
|
||||
based on a manifest file like the one used by the
|
||||
[Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml).
|
||||
This feature can be very handy when you need to import a project with many
|
||||
Use the manifest to import a project with many
|
||||
repositories like the Android Open Source Project (AOSP).
|
||||
|
||||
## Requirements
|
||||
|
|
|
@ -15,7 +15,7 @@ notification email address as an attachment.
|
|||
collected from issues into a **[comma-separated values](https://en.wikipedia.org/wiki/Comma-separated_values)** (CSV)
|
||||
file, which stores tabular data in plain text.
|
||||
|
||||
> _CSVs are a handy way of getting data from one program to another where one
|
||||
> _CSVs are a way of getting data from one program to another where one
|
||||
program cannot read the other ones normal output._ [Ref](https://www.quora.com/What-is-a-CSV-file-and-its-uses)
|
||||
|
||||
<!-- vale gitlab.Spelling = NO -->
|
||||
|
|
|
@ -72,7 +72,7 @@ module Gitlab
|
|||
Collection.new(@variables.reject(&block))
|
||||
end
|
||||
|
||||
def expand_value(value, keep_undefined: false, expand_file_vars: true)
|
||||
def expand_value(value, keep_undefined: false, expand_file_vars: true, project: nil)
|
||||
value.gsub(Item::VARIABLES_REGEXP) do
|
||||
match = Regexp.last_match # it is either a valid variable definition or a ($$ / %%)
|
||||
full_match = match[0]
|
||||
|
@ -88,6 +88,16 @@ module Gitlab
|
|||
if variable # VARIABLE_NAME is an existing variable
|
||||
next variable.value unless variable.file?
|
||||
|
||||
# Will be cleaned up with https://gitlab.com/gitlab-org/gitlab/-/issues/378266
|
||||
if project
|
||||
# We only log if `project` exists to make sure it is called from `Ci::BuildRunnerPresenter`
|
||||
# when the variables are sent to Runner.
|
||||
Gitlab::AppJsonLogger.info(
|
||||
event: 'file_variable_is_referenced_in_another_variable',
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
expand_file_vars ? variable.value : full_match
|
||||
elsif keep_undefined
|
||||
full_match # we do not touch the variable definition
|
||||
|
@ -97,7 +107,7 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def sort_and_expand_all(keep_undefined: false, expand_file_vars: true)
|
||||
def sort_and_expand_all(keep_undefined: false, expand_file_vars: true, project: nil)
|
||||
sorted = Sort.new(self)
|
||||
return self.class.new(self, sorted.errors) unless sorted.valid?
|
||||
|
||||
|
@ -112,7 +122,8 @@ module Gitlab
|
|||
# expand variables as they are added
|
||||
variable = item.to_runner_variable
|
||||
variable[:value] = new_collection.expand_value(variable[:value], keep_undefined: keep_undefined,
|
||||
expand_file_vars: expand_file_vars)
|
||||
expand_file_vars: expand_file_vars,
|
||||
project: project)
|
||||
new_collection.append(variable)
|
||||
end
|
||||
|
||||
|
|
|
@ -173,9 +173,21 @@ module Gitlab
|
|||
def alter_sequence_statements(old_table:, new_table:)
|
||||
sequences_owned_by(old_table).map do |seq_info|
|
||||
seq_name, column_name = seq_info.values_at(:name, :column_name)
|
||||
<<~SQL.chomp
|
||||
|
||||
statement_parts = []
|
||||
|
||||
# If a different user owns the old table, the conversion process will fail to reassign the sequence
|
||||
# ownership to the new parent table (as it will be owned by the current user).
|
||||
# Force the old table to be owned by the current user in that case.
|
||||
unless current_user_owns_table?(old_table)
|
||||
statement_parts << set_current_user_owns_table_statement(old_table)
|
||||
end
|
||||
|
||||
statement_parts << <<~SQL.chomp
|
||||
ALTER SEQUENCE #{quote_table_name(seq_name)} OWNED BY #{quote_table_name(new_table)}.#{quote_column_name(column_name)}
|
||||
SQL
|
||||
|
||||
statement_parts.join(SQL_STATEMENT_SEPARATOR)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -206,6 +218,23 @@ module Gitlab
|
|||
{ name: name, column_name: column_name }
|
||||
end
|
||||
end
|
||||
|
||||
def table_owner(table_name)
|
||||
connection.select_value(<<~SQL, nil, [table_name])
|
||||
SELECT tableowner FROM pg_tables WHERE tablename = $1
|
||||
SQL
|
||||
end
|
||||
|
||||
def current_user_owns_table?(table_name)
|
||||
current_user = connection.select_value('select current_user')
|
||||
table_owner(table_name) == current_user
|
||||
end
|
||||
|
||||
def set_current_user_owns_table_statement(table_name)
|
||||
<<~SQL.chomp
|
||||
ALTER TABLE #{connection.quote_table_name(table_name)} OWNER TO CURRENT_USER
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -86,8 +86,9 @@ module Gitlab
|
|||
mkdir_p(File.join(uploads_export_path, secret))
|
||||
|
||||
download_or_copy_upload(upload, upload_path)
|
||||
rescue Errno::ENAMETOOLONG => e
|
||||
# Do not fail entire project export if downloaded file has filename that exceeds 255 characters.
|
||||
rescue StandardError => e
|
||||
# Do not fail entire project export if something goes wrong during file download
|
||||
# (e.g. downloaded file has filename that exceeds 255 characters).
|
||||
# Ignore raised exception, skip such upload, log the error and keep going with the export instead.
|
||||
Gitlab::ErrorTracking.log_exception(e, project_id: @project.id)
|
||||
end
|
||||
|
|
|
@ -1253,6 +1253,9 @@ msgstr ""
|
|||
msgid "'%{template_name}' is unknown or invalid"
|
||||
msgstr ""
|
||||
|
||||
msgid "'%{value}' days of inactivity must be greater than or equal to 90"
|
||||
msgstr ""
|
||||
|
||||
msgid "(%d closed)"
|
||||
msgid_plural "(%d closed)"
|
||||
msgstr[0] ""
|
||||
|
@ -1291,6 +1294,9 @@ msgstr ""
|
|||
msgid "(max size 15 MB)"
|
||||
msgstr ""
|
||||
|
||||
msgid "(no user)"
|
||||
msgstr ""
|
||||
|
||||
msgid "(optional)"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12565,6 +12571,9 @@ msgstr ""
|
|||
msgid "Days"
|
||||
msgstr ""
|
||||
|
||||
msgid "Days of inactivity before deactivation"
|
||||
msgstr ""
|
||||
|
||||
msgid "Days to merge"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19520,12 +19529,6 @@ msgstr ""
|
|||
msgid "GroupsTree|Loading groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|No groups matched your search"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|No groups or projects matched your search"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Options"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22424,6 +22427,18 @@ msgstr ""
|
|||
msgid "IssuableStatus|promoted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issuable|epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issuable|escalation policy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issuable|iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issuable|milestone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issue"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26426,6 +26441,9 @@ msgstr ""
|
|||
msgid "Multiplier to apply to polling intervals. Decimal values are supported. Defaults to 1."
|
||||
msgstr ""
|
||||
|
||||
msgid "Must be 90 days or more."
|
||||
msgstr ""
|
||||
|
||||
msgid "My awesome group"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29354,12 +29372,6 @@ msgstr ""
|
|||
msgid "Period in seconds"
|
||||
msgstr ""
|
||||
|
||||
msgid "Period of inactivity (days)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Period of inactivity before deactivation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Permalink"
|
||||
msgstr ""
|
||||
|
||||
|
@ -45217,6 +45229,9 @@ msgstr ""
|
|||
msgid "Webhooks Help"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|+ Mask another portion of URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|A comment is added to a confidential issue."
|
||||
msgstr ""
|
||||
|
||||
|
@ -45797,6 +45812,9 @@ msgstr ""
|
|||
msgid "WorkItem|Add to iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add to milestone"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Are you sure you want to cancel editing?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -45853,12 +45871,18 @@ msgstr ""
|
|||
msgid "WorkItem|Learn about tasks."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Milestone"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|No iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|No matching results"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|No milestone"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts."
|
||||
msgstr ""
|
||||
|
||||
|
@ -45907,6 +45931,9 @@ msgstr ""
|
|||
msgid "WorkItem|Something went wrong when trying to create a child. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong while fetching milestones. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong while updating the %{workItemType}. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -13,10 +13,6 @@ module QA
|
|||
element :group_id_content
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/groups/constants.js' do
|
||||
element :no_result_text, 'No groups or projects matched your search' # rubocop:disable QA/ElementWithPattern
|
||||
end
|
||||
|
||||
view 'app/views/shared/members/_access_request_links.html.haml' do
|
||||
element :leave_group_link
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../lib/glfm/verify_all_generated_files_are_up_to_date'
|
||||
Glfm::VerifyAllGeneratedFilesAreUpToDate.new.process
|
|
@ -23,15 +23,16 @@ module Glfm
|
|||
GLFM_EXAMPLE_METADATA_YML_PATH =
|
||||
specification_input_glfm_path.join('glfm_example_metadata.yml')
|
||||
GLFM_EXAMPLE_NORMALIZATIONS_YML_PATH = specification_input_glfm_path.join('glfm_example_normalizations.yml')
|
||||
GLFM_SPEC_TXT_PATH = specification_path.join('output/spec.txt')
|
||||
GLFM_SPEC_HTML_PATH = specification_path.join('output/spec.html')
|
||||
GLFM_SPEC_OUTPUT_PATH = specification_path.join('output')
|
||||
GLFM_SPEC_TXT_PATH = GLFM_SPEC_OUTPUT_PATH.join('spec.txt')
|
||||
GLFM_SPEC_HTML_PATH = GLFM_SPEC_OUTPUT_PATH.join('spec.html')
|
||||
|
||||
# Example Snapshot (ES) files
|
||||
es_fixtures_path = File.expand_path("../../../glfm_specification/example_snapshots", __dir__)
|
||||
ES_EXAMPLES_INDEX_YML_PATH = File.join(es_fixtures_path, 'examples_index.yml')
|
||||
ES_MARKDOWN_YML_PATH = File.join(es_fixtures_path, 'markdown.yml')
|
||||
ES_HTML_YML_PATH = File.join(es_fixtures_path, 'html.yml')
|
||||
ES_PROSEMIRROR_JSON_YML_PATH = File.join(es_fixtures_path, 'prosemirror_json.yml')
|
||||
EXAMPLE_SNAPSHOTS_PATH = File.expand_path("../../../glfm_specification/example_snapshots", __dir__)
|
||||
ES_EXAMPLES_INDEX_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'examples_index.yml')
|
||||
ES_MARKDOWN_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'markdown.yml')
|
||||
ES_HTML_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'html.yml')
|
||||
ES_PROSEMIRROR_JSON_YML_PATH = File.join(EXAMPLE_SNAPSHOTS_PATH, 'prosemirror_json.yml')
|
||||
|
||||
# Other constants used for processing files
|
||||
GLFM_SPEC_TXT_HEADER = <<~MARKDOWN
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
require_relative 'constants'
|
||||
require_relative 'shared'
|
||||
|
||||
# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#verify-all-generated-files-are-up-to-daterb-script
|
||||
# for details on the implementation and usage of this script. This developers guide
|
||||
# contains diagrams and documentation of this script,
|
||||
# including explanations and examples of all files it reads and writes.
|
||||
module Glfm
|
||||
class VerifyAllGeneratedFilesAreUpToDate
|
||||
include Constants
|
||||
include Shared
|
||||
|
||||
def process
|
||||
verify_cmd = "git status --porcelain #{GLFM_SPEC_OUTPUT_PATH} #{EXAMPLE_SNAPSHOTS_PATH}"
|
||||
verify_cmd_output = run_external_cmd(verify_cmd)
|
||||
unless verify_cmd_output.empty?
|
||||
msg = "ERROR: Cannot run `#{__FILE__}` because `#{verify_cmd}` shows the following uncommitted changes:\n" \
|
||||
"#{verify_cmd_output}"
|
||||
raise(msg)
|
||||
end
|
||||
|
||||
output('Verifying all generated files are up to date after running GLFM scripts...')
|
||||
|
||||
output("Running `yarn install --frozen-lockfile` to ensure `yarn check-dependencies` doesn't fail...")
|
||||
run_external_cmd('yarn install --frozen-lockfile')
|
||||
|
||||
# noinspection RubyMismatchedArgumentType
|
||||
update_specification_script = File.expand_path('../../glfm/update-specification.rb', __dir__)
|
||||
# noinspection RubyMismatchedArgumentType
|
||||
update_example_snapshots_script = File.expand_path('../../glfm/update-example-snapshots.rb', __dir__)
|
||||
|
||||
output("Running `#{update_specification_script}`...")
|
||||
run_external_cmd(update_specification_script)
|
||||
|
||||
output("Running `#{update_example_snapshots_script}`...")
|
||||
run_external_cmd(update_example_snapshots_script)
|
||||
|
||||
output("Running `#{verify_cmd}` to check that no modifications to generated files have occurred...")
|
||||
verify_cmd_output = run_external_cmd(verify_cmd)
|
||||
|
||||
return if verify_cmd_output.empty?
|
||||
|
||||
raise "The following files were modified by running GLFM scripts. Please review, verify, and commit " \
|
||||
"the changes:\n#{verify_cmd_output}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -53,7 +53,8 @@ RSpec.describe Profiles::PreferencesController do
|
|||
first_day_of_week: '1',
|
||||
preferred_language: 'jp',
|
||||
tab_width: '5',
|
||||
render_whitespace_in_code: 'true'
|
||||
render_whitespace_in_code: 'true',
|
||||
use_legacy_web_ide: 'true'
|
||||
}.with_indifferent_access
|
||||
|
||||
expect(user).to receive(:assign_attributes).with(ActionController::Parameters.new(prefs).permit!)
|
||||
|
|
|
@ -177,10 +177,10 @@ RSpec.describe 'Admin updates settings' do
|
|||
end
|
||||
|
||||
it 'change Dormant users period' do
|
||||
expect(page).to have_field _('Period of inactivity (days)')
|
||||
expect(page).to have_field _('Days of inactivity before deactivation')
|
||||
|
||||
page.within(find('[data-testid="account-limit"]')) do
|
||||
fill_in _('application_setting_deactivate_dormant_users_period'), with: '35'
|
||||
fill_in _('application_setting_deactivate_dormant_users_period'), with: '90'
|
||||
click_button 'Save changes'
|
||||
end
|
||||
|
||||
|
@ -188,7 +188,7 @@ RSpec.describe 'Admin updates settings' do
|
|||
|
||||
page.refresh
|
||||
|
||||
expect(page).to have_field _('Period of inactivity (days)'), with: '35'
|
||||
expect(page).to have_field _('Days of inactivity before deactivation'), with: '90'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -228,9 +228,9 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
describe ':label_name parameter' do
|
||||
let(:common_labels) { create_list(:label, 3) }
|
||||
let(:distinct_labels) { create_list(:label, 3) }
|
||||
let(:merge_requests) do
|
||||
let_it_be(:common_labels) { create_list(:label, 3) }
|
||||
let_it_be(:distinct_labels) { create_list(:label, 3) }
|
||||
let_it_be(:merge_requests) do
|
||||
common_attrs = {
|
||||
source_project: project1, target_project: project1, author: user
|
||||
}
|
||||
|
@ -561,7 +561,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
context 'filtering by created_at/updated_at' do
|
||||
let(:new_project) { create(:project, forked_from_project: project1) }
|
||||
let_it_be(:new_project) { create(:project, forked_from_project: project1) }
|
||||
|
||||
let!(:new_merge_request) do
|
||||
create(:merge_request,
|
||||
|
@ -584,7 +584,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
target_project: new_project)
|
||||
end
|
||||
|
||||
before do
|
||||
before_all do
|
||||
new_project.add_maintainer(user)
|
||||
end
|
||||
|
||||
|
@ -646,10 +646,10 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
context 'filtering by the merge request deployments' do
|
||||
let(:gstg) { create(:environment, project: project4, name: 'gstg') }
|
||||
let(:gprd) { create(:environment, project: project4, name: 'gprd') }
|
||||
let_it_be(:gstg) { create(:environment, project: project4, name: 'gstg') }
|
||||
let_it_be(:gprd) { create(:environment, project: project4, name: 'gprd') }
|
||||
|
||||
let(:mr1) do
|
||||
let_it_be(:mr1) do
|
||||
create(
|
||||
:merge_request,
|
||||
:simple,
|
||||
|
@ -660,7 +660,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
)
|
||||
end
|
||||
|
||||
let(:mr2) do
|
||||
let_it_be(:mr2) do
|
||||
create(
|
||||
:merge_request,
|
||||
:simple,
|
||||
|
@ -671,7 +671,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
)
|
||||
end
|
||||
|
||||
let(:deploy1) do
|
||||
let_it_be(:deploy1) do
|
||||
create(
|
||||
:deployment,
|
||||
:success,
|
||||
|
@ -683,7 +683,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
)
|
||||
end
|
||||
|
||||
let(:deploy2) do
|
||||
let_it_be(:deploy2) do
|
||||
create(
|
||||
:deployment,
|
||||
:success,
|
||||
|
@ -695,7 +695,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
)
|
||||
end
|
||||
|
||||
before do
|
||||
before_all do
|
||||
deploy1.link_merge_requests(MergeRequest.where(id: mr1.id))
|
||||
deploy2.link_merge_requests(MergeRequest.where(id: mr2.id))
|
||||
end
|
||||
|
@ -833,13 +833,13 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
context 'when projects require different access levels for merge requests' do
|
||||
let(:user) { create(:user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:public_project) { create(:project, :public) }
|
||||
let(:internal) { create(:project, :internal) }
|
||||
let(:private_project) { create(:project, :private) }
|
||||
let(:public_with_private_repo) { create(:project, :public, :repository, :repository_private) }
|
||||
let(:internal_with_private_repo) { create(:project, :internal, :repository, :repository_private) }
|
||||
let_it_be(:public_project) { create(:project, :public) }
|
||||
let_it_be(:internal) { create(:project, :internal) }
|
||||
let_it_be(:private_project) { create(:project, :private) }
|
||||
let_it_be(:public_with_private_repo) { create(:project, :public, :repository, :repository_private) }
|
||||
let_it_be(:internal_with_private_repo) { create(:project, :internal, :repository, :repository_private) }
|
||||
|
||||
let(:merge_requests) { described_class.new(user, {}).execute }
|
||||
|
||||
|
@ -850,7 +850,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
let!(:mr_internal_private_repo_access) { create(:merge_request, source_project: internal_with_private_repo) }
|
||||
|
||||
context 'with admin user' do
|
||||
let(:user) { create(:user, :admin) }
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it 'returns all merge requests' do
|
||||
|
@ -968,7 +968,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
let_it_be(:labels) { create_list(:label, 2, project: project) }
|
||||
let_it_be(:merge_requests) { create_list(:merge_request, 4, :unique_branches, author: user, target_project: project, source_project: project, labels: labels) }
|
||||
|
||||
before do
|
||||
before_all do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import eventHub from '~/groups/event_hub';
|
|||
import GroupsService from '~/groups/service/groups_service';
|
||||
import GroupsStore from '~/groups/store/groups_store';
|
||||
import EmptyState from '~/groups/components/empty_state.vue';
|
||||
import GroupsComponent from '~/groups/components/groups.vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as urlUtilities from '~/lib/utils/url_utility';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
|
@ -388,24 +389,27 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
action | groups | fromSearch | renderEmptyState | expected
|
||||
${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${true}
|
||||
${''} | ${[]} | ${false} | ${true} | ${false}
|
||||
${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${true} | ${false}
|
||||
${'subgroups_and_projects'} | ${[]} | ${true} | ${true} | ${false}
|
||||
action | groups | fromSearch | shouldRenderEmptyState | searchEmpty
|
||||
${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${false}
|
||||
${''} | ${[]} | ${false} | ${false} | ${false}
|
||||
${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${false} | ${false}
|
||||
${'subgroups_and_projects'} | ${[]} | ${true} | ${false} | ${true}
|
||||
`(
|
||||
'when `action` is $action, `groups` is $groups, `fromSearch` is $fromSearch, and `renderEmptyState` is $renderEmptyState',
|
||||
({ action, groups, fromSearch, renderEmptyState, expected }) => {
|
||||
it(`${expected ? 'renders' : 'does not render'} empty state`, async () => {
|
||||
'when `action` is $action, `groups` is $groups, and `fromSearch` is $fromSearch',
|
||||
({ action, groups, fromSearch, shouldRenderEmptyState, searchEmpty }) => {
|
||||
it(`${shouldRenderEmptyState ? 'renders' : 'does not render'} empty state`, async () => {
|
||||
createShallowComponent({
|
||||
propsData: { action, renderEmptyState },
|
||||
propsData: { action, renderEmptyState: true },
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
vm.updateGroups(groups, fromSearch);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.findComponent(EmptyState).exists()).toBe(expected);
|
||||
expect(wrapper.findComponent(EmptyState).exists()).toBe(shouldRenderEmptyState);
|
||||
expect(wrapper.findComponent(GroupsComponent).props('searchEmpty')).toBe(searchEmpty);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -445,18 +449,6 @@ describe('AppComponent', () => {
|
|||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', async () => {
|
||||
createShallowComponent();
|
||||
await nextTick();
|
||||
expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search');
|
||||
});
|
||||
|
||||
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', async () => {
|
||||
createShallowComponent({ propsData: { hideProjects: true } });
|
||||
await nextTick();
|
||||
expect(vm.searchEmptyMessage).toBe('No groups matched your search');
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeDestroy', () => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import GroupFolderComponent from '~/groups/components/group_folder.vue';
|
||||
|
@ -15,7 +16,6 @@ describe('GroupsComponent', () => {
|
|||
const defaultPropsData = {
|
||||
groups: mockGroups,
|
||||
pageInfo: mockPageInfo,
|
||||
searchEmptyMessage: 'No matching results',
|
||||
searchEmpty: false,
|
||||
};
|
||||
|
||||
|
@ -67,13 +67,16 @@ describe('GroupsComponent', () => {
|
|||
|
||||
expect(wrapper.findComponent(GroupFolderComponent).exists()).toBe(true);
|
||||
expect(findPaginationLinks().exists()).toBe(true);
|
||||
expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render empty search message when `searchEmpty` is `true`', () => {
|
||||
createComponent({ propsData: { searchEmpty: true } });
|
||||
|
||||
expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
|
||||
title: GroupsComponent.i18n.emptyStateTitle,
|
||||
description: GroupsComponent.i18n.emptyStateDescription,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ jest.mock('@gitlab/web-ide');
|
|||
|
||||
const ROOT_ELEMENT_ID = 'ide';
|
||||
const TEST_NONCE = 'test123nonce';
|
||||
const TEST_PROJECT = { path_with_namespace: 'group1/project1' };
|
||||
const TEST_PROJECT_PATH = 'group1/project1';
|
||||
const TEST_BRANCH_NAME = '12345-foo-patch';
|
||||
const TEST_GITLAB_URL = 'https://test-gitlab/';
|
||||
const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path';
|
||||
|
@ -18,7 +18,7 @@ describe('ide/init_gitlab_web_ide', () => {
|
|||
el.id = ROOT_ELEMENT_ID;
|
||||
// why: We'll test that this class is removed later
|
||||
el.classList.add('ide-loading');
|
||||
el.dataset.project = JSON.stringify(TEST_PROJECT);
|
||||
el.dataset.projectPath = TEST_PROJECT_PATH;
|
||||
el.dataset.cspNonce = TEST_NONCE;
|
||||
el.dataset.branchName = TEST_BRANCH_NAME;
|
||||
|
||||
|
@ -43,7 +43,7 @@ describe('ide/init_gitlab_web_ide', () => {
|
|||
it('calls start with element', () => {
|
||||
expect(start).toHaveBeenCalledWith(findRootElement(), {
|
||||
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
|
||||
projectPath: TEST_PROJECT.path_with_namespace,
|
||||
projectPath: TEST_PROJECT_PATH,
|
||||
ref: TEST_BRANCH_NAME,
|
||||
gitlabUrl: TEST_GITLAB_URL,
|
||||
nonce: TEST_NONCE,
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlListbox } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
|
||||
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import UserSelect from '~/pages/import/fogbugz/new_user_map/components/user_select.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const USERS_RESPONSE = {
|
||||
data: {
|
||||
users: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/User/44',
|
||||
avatarUrl: '/avatar1',
|
||||
webUrl: '/reported_user_22',
|
||||
name: 'Birgit Steuber',
|
||||
username: 'reported_user_22',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/User/43',
|
||||
avatarUrl: '/avatar2',
|
||||
webUrl: '/reported_user_21',
|
||||
name: 'Luke Spinka',
|
||||
username: 'reported_user_21',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
],
|
||||
__typename: 'UserCoreConnection',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('fogbugz user select component', () => {
|
||||
let wrapper;
|
||||
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(USERS_RESPONSE);
|
||||
|
||||
const createComponent = (propsData = { name: 'demo' }) => {
|
||||
const fakeApollo = createMockApollo([[searchUsersQuery, searchQueryHandlerSuccess]]);
|
||||
|
||||
wrapper = shallowMount(UserSelect, {
|
||||
apolloProvider: fakeApollo,
|
||||
propsData,
|
||||
});
|
||||
};
|
||||
|
||||
it('renders hidden input with name from props', () => {
|
||||
const name = 'test';
|
||||
createComponent({ name });
|
||||
expect(wrapper.find('input').attributes('name')).toBe(name);
|
||||
});
|
||||
|
||||
it('syncs input value with value emitted from listbox', async () => {
|
||||
createComponent();
|
||||
|
||||
const id = 8;
|
||||
|
||||
wrapper.findComponent(GlListbox).vm.$emit('select', `gid://gitlab/User/${id}`);
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.get('input').attributes('value')).toBe(id.toString());
|
||||
});
|
||||
|
||||
it('filters users when search is performed in listbox', async () => {
|
||||
createComponent();
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
wrapper.findComponent(GlListbox).vm.$emit('search', 'test');
|
||||
await nextTick();
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(searchQueryHandlerSuccess).toHaveBeenCalledWith({
|
||||
first: expect.anything(),
|
||||
search: 'test',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1144,7 +1144,7 @@ describe('MrWidgetOptions', () => {
|
|||
${'WidgetCodeQuality'} | ${'i_testing_code_quality_widget_total'}
|
||||
${'WidgetTerraform'} | ${'i_testing_terraform_widget_total'}
|
||||
${'WidgetIssues'} | ${'i_testing_issues_widget_total'}
|
||||
${'WidgetTestReport'} | ${'i_testing_summary_widget_total'}
|
||||
${'WidgetTestSummary'} | ${'i_testing_summary_widget_total'}
|
||||
`(
|
||||
"sends non-standard events for the '$widgetName' widget",
|
||||
async ({ widgetName, nonStandardEvent }) => {
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlFormRadio, GlFormRadioGroup } from '@gitlab/ui';
|
||||
import { GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
|
||||
|
||||
import FormUrlApp from '~/webhooks/components/form_url_app.vue';
|
||||
import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue';
|
||||
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
describe('FormUrlApp', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMountExtended(FormUrlApp);
|
||||
const createComponent = ({ props } = {}) => {
|
||||
wrapper = shallowMountExtended(FormUrlApp, {
|
||||
propsData: { ...props },
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -20,13 +23,17 @@ describe('FormUrlApp', () => {
|
|||
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
|
||||
const findUrlMaskDisable = () => findAllRadioButtons().at(0);
|
||||
const findUrlMaskEnable = () => findAllRadioButtons().at(1);
|
||||
const findAllUrlMaskItems = () => wrapper.findAllComponents(FormUrlMaskItem);
|
||||
const findAddItem = () => wrapper.findComponent(GlLink);
|
||||
const findFormUrl = () => wrapper.findByTestId('form-url');
|
||||
const findFormUrlPreview = () => wrapper.findByTestId('form-url-preview');
|
||||
const findUrlMaskSection = () => wrapper.findByTestId('url-mask-section');
|
||||
|
||||
describe('template', () => {
|
||||
it('renders radio buttons for URL masking', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findAllRadioButtons().length).toBe(2);
|
||||
expect(findAllRadioButtons()).toHaveLength(2);
|
||||
expect(findUrlMaskDisable().text()).toBe(FormUrlApp.i18n.radioFullUrlText);
|
||||
expect(findUrlMaskEnable().text()).toBe(FormUrlApp.i18n.radioMaskUrlText);
|
||||
});
|
||||
|
@ -48,6 +55,88 @@ describe('FormUrlApp', () => {
|
|||
it('renders mask section', () => {
|
||||
expect(findUrlMaskSection().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders an empty mask item by default', () => {
|
||||
expect(findAllUrlMaskItems()).toHaveLength(1);
|
||||
|
||||
const firstItem = findAllUrlMaskItems().at(0);
|
||||
expect(firstItem.props('itemKey')).toBeNull();
|
||||
expect(firstItem.props('itemValue')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with mask items', () => {
|
||||
const mockItem1 = { key: 'key1', value: 'value1' };
|
||||
const mockItem2 = { key: 'key2', value: 'value2' };
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: { initialUrlVariables: [mockItem1, mockItem2] },
|
||||
});
|
||||
});
|
||||
|
||||
it('renders masked URL preview', async () => {
|
||||
const mockUrl = 'https://test.host/value1?secret=value2';
|
||||
|
||||
findFormUrl().vm.$emit('input', mockUrl);
|
||||
await nextTick();
|
||||
|
||||
expect(findFormUrlPreview().attributes('value')).toBe(
|
||||
'https://test.host/{key1}?secret={key2}',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders mask items correctly', () => {
|
||||
expect(findAllUrlMaskItems()).toHaveLength(2);
|
||||
|
||||
const firstItem = findAllUrlMaskItems().at(0);
|
||||
expect(firstItem.props('itemKey')).toBe(mockItem1.key);
|
||||
expect(firstItem.props('itemValue')).toBe(mockItem1.value);
|
||||
|
||||
const secondItem = findAllUrlMaskItems().at(1);
|
||||
expect(secondItem.props('itemKey')).toBe(mockItem2.key);
|
||||
expect(secondItem.props('itemValue')).toBe(mockItem2.value);
|
||||
});
|
||||
|
||||
describe('on mask item input', () => {
|
||||
const mockInput = { index: 0, key: 'display', value: 'secret' };
|
||||
|
||||
it('updates mask item', async () => {
|
||||
const firstItem = findAllUrlMaskItems().at(0);
|
||||
firstItem.vm.$emit('input', mockInput);
|
||||
await nextTick();
|
||||
|
||||
expect(firstItem.props('itemKey')).toBe(mockInput.key);
|
||||
expect(firstItem.props('itemValue')).toBe(mockInput.value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when add item is clicked', () => {
|
||||
it('adds mask item', async () => {
|
||||
findAddItem().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(findAllUrlMaskItems()).toHaveLength(3);
|
||||
|
||||
const lastItem = findAllUrlMaskItems().at(-1);
|
||||
expect(lastItem.props('itemKey')).toBeNull();
|
||||
expect(lastItem.props('itemValue')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when remove item is clicked', () => {
|
||||
it('removes the correct mask item', async () => {
|
||||
const firstItem = findAllUrlMaskItems().at(0);
|
||||
firstItem.vm.$emit('remove');
|
||||
await nextTick();
|
||||
|
||||
expect(findAllUrlMaskItems()).toHaveLength(1);
|
||||
|
||||
const newFirstItem = findAllUrlMaskItems().at(0);
|
||||
expect(newFirstItem.props('itemKey')).toBe(mockItem2.key);
|
||||
expect(newFirstItem.props('itemValue')).toBe(mockItem2.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlButton, GlFormInput } from '@gitlab/ui';
|
||||
|
||||
import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue';
|
||||
|
@ -10,10 +11,13 @@ describe('FormUrlMaskItem', () => {
|
|||
const defaultProps = {
|
||||
index: 0,
|
||||
};
|
||||
const mockKey = 'key';
|
||||
const mockValue = 'value';
|
||||
const mockInput = 'input';
|
||||
|
||||
const createComponent = () => {
|
||||
const createComponent = ({ props } = {}) => {
|
||||
wrapper = shallowMountExtended(FormUrlMaskItem, {
|
||||
propsData: { ...defaultProps },
|
||||
propsData: { ...defaultProps, ...props },
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -42,10 +46,55 @@ describe('FormUrlMaskItem', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('on key input', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
|
||||
|
||||
findMaskItemKey().findComponent(GlFormInput).vm.$emit('input', mockInput);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('emits input event', () => {
|
||||
expect(wrapper.emitted('input')).toEqual([
|
||||
[{ index: defaultProps.index, key: mockInput, value: mockValue }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on value input', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
|
||||
|
||||
findMaskItemValue().findComponent(GlFormInput).vm.$emit('input', mockInput);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('emits input event', () => {
|
||||
expect(wrapper.emitted('input')).toEqual([
|
||||
[{ index: defaultProps.index, key: mockKey, value: mockInput }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders remove button', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findRemoveButton().props('icon')).toBe('remove');
|
||||
});
|
||||
|
||||
describe('when remove button is clicked', () => {
|
||||
const mockIndex = 5;
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent({ props: { index: mockIndex } });
|
||||
|
||||
findRemoveButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('emits remove event', () => {
|
||||
expect(wrapper.emitted('remove')).toEqual([[mockIndex]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ import WorkItemState from '~/work_items/components/work_item_state.vue';
|
|||
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
|
||||
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
|
||||
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
|
||||
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
|
||||
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
|
||||
import { i18n } from '~/work_items/constants';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
|
@ -28,6 +29,7 @@ import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subs
|
|||
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
|
||||
import { temporaryConfig } from '~/graphql_shared/issuable_client';
|
||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import {
|
||||
mockParent,
|
||||
|
@ -67,6 +69,7 @@ describe('WorkItemDetail component', () => {
|
|||
const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
|
||||
const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
|
||||
const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
|
||||
const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
|
||||
const findParent = () => wrapper.find('[data-testid="work-item-parent"]');
|
||||
const findParentButton = () => findParent().findComponent(GlButton);
|
||||
const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
|
||||
|
@ -82,6 +85,8 @@ describe('WorkItemDetail component', () => {
|
|||
subscriptionHandler = titleSubscriptionHandler,
|
||||
confidentialityMock = [updateWorkItemMutation, jest.fn()],
|
||||
error = undefined,
|
||||
includeWidgets = false,
|
||||
workItemsMvc2Enabled = false,
|
||||
} = {}) => {
|
||||
const handlers = [
|
||||
[workItemQuery, handler],
|
||||
|
@ -92,7 +97,13 @@ describe('WorkItemDetail component', () => {
|
|||
];
|
||||
|
||||
wrapper = shallowMount(WorkItemDetail, {
|
||||
apolloProvider: createMockApollo(handlers),
|
||||
apolloProvider: createMockApollo(
|
||||
handlers,
|
||||
{},
|
||||
{
|
||||
typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {},
|
||||
},
|
||||
),
|
||||
propsData: { isModal, workItemId },
|
||||
data() {
|
||||
return {
|
||||
|
@ -101,6 +112,9 @@ describe('WorkItemDetail component', () => {
|
|||
};
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
workItemsMvc2: workItemsMvc2Enabled,
|
||||
},
|
||||
hasIssueWeightsFeature: true,
|
||||
hasIterationsFeature: true,
|
||||
projectNamespace: 'namespace',
|
||||
|
@ -527,6 +541,19 @@ describe('WorkItemDetail component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('milestone widget', () => {
|
||||
it.each`
|
||||
description | includeWidgets | exists
|
||||
${'renders when widget is returned from API'} | ${true} | ${true}
|
||||
${'does not render when widget is not returned from API'} | ${false} | ${false}
|
||||
`('$description', async ({ includeWidgets, exists }) => {
|
||||
createComponent({ includeWidgets, workItemsMvc2Enabled: true });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findWorkItemMilestone().exists()).toBe(exists);
|
||||
});
|
||||
});
|
||||
|
||||
describe('work item information', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
|
|
@ -0,0 +1,247 @@
|
|||
import {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlSearchBoxByType,
|
||||
GlSkeletonLoader,
|
||||
GlFormGroup,
|
||||
GlDropdownText,
|
||||
} from '@gitlab/ui';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
|
||||
import { resolvers, temporaryConfig } from '~/graphql_shared/issuable_client';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
|
||||
import {
|
||||
projectMilestonesResponse,
|
||||
projectMilestonesResponseWithNoMilestones,
|
||||
mockMilestoneWidgetResponse,
|
||||
workItemResponseFactory,
|
||||
updateWorkItemMutationErrorResponse,
|
||||
} from 'jest/work_items/mock_data';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
|
||||
describe('WorkItemMilestone component', () => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
|
||||
const workItemId = 'gid://gitlab/WorkItem/1';
|
||||
const workItemType = 'Task';
|
||||
const fullPath = 'full-path';
|
||||
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
const findNoMilestoneDropdownItem = () => wrapper.findByTestId('no-milestone');
|
||||
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
|
||||
const findFirstDropdownItem = () => findDropdownItems().at(0);
|
||||
const findDropdownTexts = () => wrapper.findAllComponents(GlDropdownText);
|
||||
const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
|
||||
const findDisabledTextSpan = () => wrapper.findByTestId('disabled-text');
|
||||
const findDropdownTextAtIndex = (index) => findDropdownTexts().at(index);
|
||||
const findInputGroup = () => wrapper.findComponent(GlFormGroup);
|
||||
|
||||
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
|
||||
|
||||
const networkResolvedValue = new Error();
|
||||
|
||||
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse);
|
||||
const successSearchWithNoMatchingMilestones = jest
|
||||
.fn()
|
||||
.mockResolvedValue(projectMilestonesResponseWithNoMilestones);
|
||||
|
||||
const showDropdown = () => {
|
||||
findDropdown().vm.$emit('shown');
|
||||
};
|
||||
|
||||
const hideDropdown = () => {
|
||||
findDropdown().vm.$emit('hide');
|
||||
};
|
||||
|
||||
const createComponent = ({
|
||||
canUpdate = true,
|
||||
milestone = mockMilestoneWidgetResponse,
|
||||
searchQueryHandler = successSearchQueryHandler,
|
||||
} = {}) => {
|
||||
const apolloProvider = createMockApollo(
|
||||
[[projectMilestonesQuery, searchQueryHandler]],
|
||||
resolvers,
|
||||
{
|
||||
typePolicies: temporaryConfig.cacheConfig.typePolicies,
|
||||
},
|
||||
);
|
||||
|
||||
apolloProvider.clients.defaultClient.writeQuery({
|
||||
query: workItemQuery,
|
||||
variables: {
|
||||
id: workItemId,
|
||||
},
|
||||
data: workItemQueryResponse.data,
|
||||
});
|
||||
|
||||
wrapper = shallowMountExtended(WorkItemMilestone, {
|
||||
apolloProvider,
|
||||
propsData: {
|
||||
canUpdate,
|
||||
workItemMilestone: milestone,
|
||||
workItemId,
|
||||
workItemType,
|
||||
fullPath,
|
||||
},
|
||||
stubs: {
|
||||
GlDropdown,
|
||||
GlSearchBoxByType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('has "Milestone" label', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findInputGroup().exists()).toBe(true);
|
||||
expect(findInputGroup().attributes('label')).toBe(WorkItemMilestone.i18n.MILESTONE);
|
||||
});
|
||||
|
||||
describe('Default text with canUpdate false and milestone value', () => {
|
||||
describe.each`
|
||||
description | milestone | value
|
||||
${'when no milestone'} | ${null} | ${WorkItemMilestone.i18n.NONE}
|
||||
${'when milestone set'} | ${mockMilestoneWidgetResponse} | ${mockMilestoneWidgetResponse.title}
|
||||
`('$description', ({ milestone, value }) => {
|
||||
it(`has a value of "${value}"`, () => {
|
||||
createComponent({ canUpdate: false, milestone });
|
||||
|
||||
expect(findDisabledTextSpan().text()).toBe(value);
|
||||
expect(findDropdown().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Default text value when canUpdate true and no milestone set', () => {
|
||||
it(`has a value of "Add to milestone"`, () => {
|
||||
createComponent({ canUpdate: true, milestone: null });
|
||||
|
||||
expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dropdown search', () => {
|
||||
it('has the search box', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findSearchBox().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows no matching results when no items', () => {
|
||||
createComponent({
|
||||
searchQueryHandler: successSearchWithNoMatchingMilestones,
|
||||
});
|
||||
|
||||
expect(findDropdownTextAtIndex(0).text()).toBe(WorkItemMilestone.i18n.NO_MATCHING_RESULTS);
|
||||
expect(findDropdownItems()).toHaveLength(1);
|
||||
expect(findDropdownTexts()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dropdown options', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true });
|
||||
});
|
||||
|
||||
it('shows the skeleton loader when the items are being fetched on click', async () => {
|
||||
showDropdown();
|
||||
await nextTick();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the milestones in dropdown when the items have finished fetching', async () => {
|
||||
showDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(false);
|
||||
expect(findNoMilestoneDropdownItem().exists()).toBe(true);
|
||||
expect(findDropdownItems()).toHaveLength(
|
||||
projectMilestonesResponse.data.workspace.attributes.nodes.length + 1,
|
||||
);
|
||||
});
|
||||
|
||||
it('changes the milestone to null when clicked on no milestone', async () => {
|
||||
showDropdown();
|
||||
findFirstDropdownItem().vm.$emit('click');
|
||||
|
||||
hideDropdown();
|
||||
await nextTick();
|
||||
expect(findDropdown().props('loading')).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findDropdown().props('loading')).toBe(false);
|
||||
expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
|
||||
});
|
||||
|
||||
it('changes the milestone to the selected milestone', async () => {
|
||||
const milestoneIndex = 1;
|
||||
/** the index is -1 since no matching results is also a dropdown item */
|
||||
const milestoneAtIndex =
|
||||
projectMilestonesResponse.data.workspace.attributes.nodes[milestoneIndex - 1];
|
||||
showDropdown();
|
||||
|
||||
await waitForPromises();
|
||||
findDropdownItemAtIndex(milestoneIndex).vm.$emit('click');
|
||||
|
||||
hideDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findDropdown().props('text')).toBe(milestoneAtIndex.title);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handlers', () => {
|
||||
it.each`
|
||||
errorType | expectedErrorMessage | mockValue | resolveFunction
|
||||
${'graphql error'} | ${'Something went wrong while updating the task. Please try again.'} | ${updateWorkItemMutationErrorResponse} | ${'mockResolvedValue'}
|
||||
${'network error'} | ${'Something went wrong while updating the task. Please try again.'} | ${networkResolvedValue} | ${'mockRejectedValue'}
|
||||
`(
|
||||
'emits an error when there is a $errorType',
|
||||
async ({ mockValue, expectedErrorMessage, resolveFunction }) => {
|
||||
createComponent({
|
||||
mutationHandler: jest.fn()[resolveFunction](mockValue),
|
||||
canUpdate: true,
|
||||
});
|
||||
|
||||
showDropdown();
|
||||
findFirstDropdownItem().vm.$emit('click');
|
||||
hideDropdown();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[expectedErrorMessage]]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Tracking event', () => {
|
||||
it('tracks updating the milestone', async () => {
|
||||
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
createComponent({ canUpdate: true });
|
||||
|
||||
showDropdown();
|
||||
findFirstDropdownItem().vm.$emit('click');
|
||||
hideDropdown();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_milestone', {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
label: 'item_milestone',
|
||||
property: 'type_Task',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -186,6 +186,7 @@ export const workItemResponseFactory = ({
|
|||
datesWidgetPresent = true,
|
||||
labelsWidgetPresent = true,
|
||||
weightWidgetPresent = true,
|
||||
milestoneWidgetPresent = true,
|
||||
iterationWidgetPresent = true,
|
||||
confidential = false,
|
||||
canInviteMembers = false,
|
||||
|
@ -279,6 +280,16 @@ export const workItemResponseFactory = ({
|
|||
},
|
||||
}
|
||||
: { type: 'MOCK TYPE' },
|
||||
milestoneWidgetPresent
|
||||
? {
|
||||
__typename: 'WorkItemWidgetMilestone',
|
||||
dueDate: null,
|
||||
expired: false,
|
||||
id: 'gid://gitlab/Milestone/30',
|
||||
title: 'v4.0',
|
||||
type: 'MILESTONE',
|
||||
}
|
||||
: { type: 'MOCK TYPE' },
|
||||
{
|
||||
__typename: 'WorkItemWidgetHierarchy',
|
||||
type: 'HIERARCHY',
|
||||
|
@ -1059,3 +1070,55 @@ export const groupIterationsResponseWithNoIterations = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockMilestoneWidgetResponse = {
|
||||
dueDate: null,
|
||||
expired: false,
|
||||
id: 'gid://gitlab/Milestone/30',
|
||||
title: 'v4.0',
|
||||
};
|
||||
|
||||
export const projectMilestonesResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
attributes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Milestone/5',
|
||||
title: 'v4.0',
|
||||
webUrl: '/gitlab-org/gitlab-test/-/milestones/5',
|
||||
dueDate: null,
|
||||
expired: false,
|
||||
__typename: 'Milestone',
|
||||
state: 'active',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Milestone/4',
|
||||
title: 'v3.0',
|
||||
webUrl: '/gitlab-org/gitlab-test/-/milestones/4',
|
||||
dueDate: null,
|
||||
expired: false,
|
||||
__typename: 'Milestone',
|
||||
state: 'active',
|
||||
},
|
||||
],
|
||||
__typename: 'MilestoneConnection',
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const projectMilestonesResponseWithNoMilestones = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
attributes: {
|
||||
nodes: [],
|
||||
__typename: 'MilestoneConnection',
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -8,6 +8,13 @@ RSpec.describe HooksHelper do
|
|||
let(:service_hook) { create(:service_hook, integration: create(:drone_ci_integration)) }
|
||||
let(:system_hook) { create(:system_hook) }
|
||||
|
||||
describe '#webhook_form_data' do
|
||||
subject { helper.webhook_form_data(project_hook) }
|
||||
|
||||
it { expect(subject[:url]).to eq(project_hook.url) }
|
||||
it { expect(subject[:url_variables]).to be_nil }
|
||||
end
|
||||
|
||||
describe '#link_to_test_hook' do
|
||||
let(:trigger) { 'push_events' }
|
||||
|
||||
|
|
|
@ -5,75 +5,113 @@ require 'spec_helper'
|
|||
RSpec.describe IdeHelper do
|
||||
describe '#ide_data' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { project.creator }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(project.creator)
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
allow(helper).to receive(:content_security_policy_nonce).and_return('test-csp-nonce')
|
||||
end
|
||||
|
||||
context 'when instance vars are not set' do
|
||||
it 'returns instance data in the hash as nil' do
|
||||
expect(helper.ide_data)
|
||||
.to include(
|
||||
'branch-name' => nil,
|
||||
'file-path' => nil,
|
||||
'merge-request' => nil,
|
||||
'fork-info' => nil,
|
||||
'project' => nil,
|
||||
'preview-markdown-path' => nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when instance vars are set' do
|
||||
it 'returns instance data in the hash' do
|
||||
fork_info = { ide_path: '/test/ide/path' }
|
||||
context 'with vscode_web_ide=true and instance vars set' do
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: true)
|
||||
|
||||
self.instance_variable_set(:@branch, 'master')
|
||||
self.instance_variable_set(:@path, 'foo/bar')
|
||||
self.instance_variable_set(:@merge_request, '1')
|
||||
self.instance_variable_set(:@fork_info, fork_info)
|
||||
self.instance_variable_set(:@project, project)
|
||||
end
|
||||
|
||||
serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json
|
||||
|
||||
it 'returns hash' do
|
||||
expect(helper.ide_data)
|
||||
.to include(
|
||||
.to eq(
|
||||
'can-use-new-web-ide' => 'true',
|
||||
'use-new-web-ide' => 'true',
|
||||
'user-preferences-path' => profile_preferences_path,
|
||||
'branch-name' => 'master',
|
||||
'file-path' => 'foo/bar',
|
||||
'merge-request' => '1',
|
||||
'fork-info' => fork_info.to_json,
|
||||
'project' => serialized_project,
|
||||
'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project)
|
||||
'project-path' => project.path_with_namespace,
|
||||
'csp-nonce' => 'test-csp-nonce'
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not use new web ide if user.use_legacy_web_ide' do
|
||||
allow(user).to receive(:use_legacy_web_ide).and_return(true)
|
||||
|
||||
expect(helper.ide_data).to include('use-new-web-ide' => 'false')
|
||||
end
|
||||
end
|
||||
|
||||
context 'environments guidance experiment', :experiment do
|
||||
context 'with vscode_web_ide=false' do
|
||||
before do
|
||||
stub_experiments(in_product_guidance_environments_webide: :candidate)
|
||||
self.instance_variable_set(:@project, project)
|
||||
stub_feature_flags(vscode_web_ide: false)
|
||||
end
|
||||
|
||||
context 'when project has no enviornments' do
|
||||
it 'enables environment guidance' do
|
||||
expect(helper.ide_data).to include('enable-environments-guidance' => 'true')
|
||||
context 'when instance vars are not set' do
|
||||
it 'returns instance data in the hash as nil' do
|
||||
expect(helper.ide_data)
|
||||
.to include(
|
||||
'can-use-new-web-ide' => 'false',
|
||||
'use-new-web-ide' => 'false',
|
||||
'user-preferences-path' => profile_preferences_path,
|
||||
'branch-name' => nil,
|
||||
'file-path' => nil,
|
||||
'merge-request' => nil,
|
||||
'fork-info' => nil,
|
||||
'project' => nil,
|
||||
'preview-markdown-path' => nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when instance vars are set' do
|
||||
it 'returns instance data in the hash' do
|
||||
fork_info = { ide_path: '/test/ide/path' }
|
||||
|
||||
self.instance_variable_set(:@branch, 'master')
|
||||
self.instance_variable_set(:@path, 'foo/bar')
|
||||
self.instance_variable_set(:@merge_request, '1')
|
||||
self.instance_variable_set(:@fork_info, fork_info)
|
||||
self.instance_variable_set(:@project, project)
|
||||
|
||||
serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json
|
||||
|
||||
expect(helper.ide_data)
|
||||
.to include(
|
||||
'branch-name' => 'master',
|
||||
'file-path' => 'foo/bar',
|
||||
'merge-request' => '1',
|
||||
'fork-info' => fork_info.to_json,
|
||||
'project' => serialized_project,
|
||||
'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'environments guidance experiment', :experiment do
|
||||
before do
|
||||
stub_experiments(in_product_guidance_environments_webide: :candidate)
|
||||
self.instance_variable_set(:@project, project)
|
||||
end
|
||||
|
||||
context 'and the callout has been dismissed' do
|
||||
it 'disables environment guidance' do
|
||||
callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
|
||||
callout.update!(dismissed_at: Time.now - 1.week)
|
||||
allow(helper).to receive(:current_user).and_return(User.find(project.creator.id))
|
||||
expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
|
||||
context 'when project has no enviornments' do
|
||||
it 'enables environment guidance' do
|
||||
expect(helper.ide_data).to include('enable-environments-guidance' => 'true')
|
||||
end
|
||||
|
||||
context 'and the callout has been dismissed' do
|
||||
it 'disables environment guidance' do
|
||||
callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
|
||||
callout.update!(dismissed_at: Time.now - 1.week)
|
||||
allow(helper).to receive(:current_user).and_return(User.find(project.creator.id))
|
||||
expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project has environments' do
|
||||
it 'disables environment guidance' do
|
||||
create(:environment, project: project)
|
||||
context 'when the project has environments' do
|
||||
it 'disables environment guidance' do
|
||||
create(:environment, project: project)
|
||||
|
||||
expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
|
||||
expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -571,5 +571,42 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the file_variable_is_referenced_in_another_variable logging' do
|
||||
let(:collection) do
|
||||
Gitlab::Ci::Variables::Collection.new
|
||||
.append(key: 'VAR1', value: 'test-1')
|
||||
.append(key: 'VAR2', value: '$VAR1')
|
||||
.append(key: 'VAR3', value: '$VAR1', raw: true)
|
||||
.append(key: 'FILEVAR4', value: 'file-test-4', file: true)
|
||||
.append(key: 'VAR5', value: '$FILEVAR4')
|
||||
.append(key: 'VAR6', value: '$FILEVAR4', raw: true)
|
||||
end
|
||||
|
||||
subject(:sort_and_expand_all) { collection.sort_and_expand_all(project: project) }
|
||||
|
||||
context 'when a project is not passed' do
|
||||
let(:project) {}
|
||||
|
||||
it 'does not log anything' do
|
||||
expect(Gitlab::AppJsonLogger).not_to receive(:info)
|
||||
|
||||
sort_and_expand_all
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a project is passed' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it 'logs file_variable_is_referenced_in_another_variable once for VAR5' do
|
||||
expect(Gitlab::AppJsonLogger).to receive(:info).with(
|
||||
event: 'file_variable_is_referenced_in_another_variable',
|
||||
project_id: project.id
|
||||
).once
|
||||
|
||||
sort_and_expand_all
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -153,6 +153,21 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
|
|||
expect(parent_model.pluck(:id)).to match_array([1, 2, 3])
|
||||
end
|
||||
|
||||
context 'when the existing table is owned by a different user' do
|
||||
before do
|
||||
connection.execute(<<~SQL)
|
||||
CREATE USER other_user SUPERUSER;
|
||||
ALTER TABLE #{table_name} OWNER TO other_user;
|
||||
SQL
|
||||
end
|
||||
|
||||
let(:current_user) { model.connection.select_value('select current_user') }
|
||||
|
||||
it 'partitions without error' do
|
||||
expect { partition }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an error occurs during the conversion' do
|
||||
def fail_first_time
|
||||
# We can't directly use a boolean here, as we need something that will be passed by-reference to the proc
|
||||
|
|
|
@ -78,16 +78,30 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do
|
|||
context 'when upload is in object storage' do
|
||||
before do
|
||||
stub_uploads_object_storage(FileUploader)
|
||||
allow(manager).to receive(:download_or_copy_upload).and_raise(Errno::ENAMETOOLONG)
|
||||
end
|
||||
|
||||
it 'ignores problematic upload and logs exception' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(Errno::ENAMETOOLONG), project_id: project.id)
|
||||
shared_examples 'export with invalid upload' do
|
||||
it 'ignores problematic upload and logs exception' do
|
||||
allow(manager).to receive(:download_or_copy_upload).and_raise(exception)
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(exception), project_id: project.id)
|
||||
|
||||
manager.save # rubocop:disable Rails/SaveBang
|
||||
manager.save # rubocop:disable Rails/SaveBang
|
||||
|
||||
expect(shared.errors).to be_empty
|
||||
expect(File).not_to exist(exported_file_path)
|
||||
expect(shared.errors).to be_empty
|
||||
expect(File).not_to exist(exported_file_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filename is too long' do
|
||||
let(:exception) { Errno::ENAMETOOLONG }
|
||||
|
||||
include_examples 'export with invalid upload'
|
||||
end
|
||||
|
||||
context 'when network exception occurs' do
|
||||
let(:exception) { Net::OpenTimeout }
|
||||
|
||||
include_examples 'export with invalid upload'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe UpdateInvalidDormantUserSetting, :migration do
|
||||
let(:settings) { table(:application_settings) }
|
||||
|
||||
context 'with no rows in the application_settings table' do
|
||||
it 'does not insert a row' do
|
||||
expect { migrate! }.to not_change { settings.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a row in the application_settings table' do
|
||||
before do
|
||||
settings.create!(deactivate_dormant_users_period: days)
|
||||
end
|
||||
|
||||
context 'with deactivate_dormant_users_period set to a value greater than or equal to 90' do
|
||||
let(:days) { 90 }
|
||||
|
||||
it 'does not update the row' do
|
||||
expect { migrate! }
|
||||
.to not_change { settings.count }
|
||||
.and not_change { settings.first.deactivate_dormant_users_period }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with deactivate_dormant_users_period set to a value less than or equal to 90' do
|
||||
let(:days) { 1 }
|
||||
|
||||
it 'updates the existing row' do
|
||||
expect { migrate! }
|
||||
.to not_change { settings.count }
|
||||
.and change { settings.first.deactivate_dormant_users_period }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -203,6 +203,17 @@ RSpec.describe ApplicationSetting do
|
|||
it { is_expected.to allow_value([]).for(:valid_runner_registrars) }
|
||||
it { is_expected.to allow_value(%w(project group)).for(:valid_runner_registrars) }
|
||||
|
||||
context 'when deactivate_dormant_users is enabled' do
|
||||
before do
|
||||
stub_application_setting(deactivate_dormant_users: true)
|
||||
end
|
||||
|
||||
it { is_expected.not_to allow_value(nil).for(:deactivate_dormant_users_period) }
|
||||
it { is_expected.to allow_value(90).for(:deactivate_dormant_users_period) }
|
||||
it { is_expected.to allow_value(365).for(:deactivate_dormant_users_period) }
|
||||
it { is_expected.not_to allow_value(89).for(:deactivate_dormant_users_period) }
|
||||
end
|
||||
|
||||
context 'help_page_documentation_base_url validations' do
|
||||
it { is_expected.to allow_value(nil).for(:help_page_documentation_base_url) }
|
||||
it { is_expected.to allow_value('https://docs.gitlab.com').for(:help_page_documentation_base_url) }
|
||||
|
|
|
@ -5337,19 +5337,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#authorized_cluster_agents' do
|
||||
describe '#cluster_agent_authorizations' do
|
||||
let(:pipeline) { create(:ci_empty_pipeline, :created) }
|
||||
let(:agent) { instance_double(Clusters::Agent) }
|
||||
let(:authorization) { instance_double(Clusters::Agents::GroupAuthorization, agent: agent) }
|
||||
let(:authorization) { instance_double(Clusters::Agents::GroupAuthorization) }
|
||||
let(:finder) { double(execute: [authorization]) }
|
||||
|
||||
it 'retrieves agent records from the finder and caches the result' do
|
||||
it 'retrieves authorization records from the finder and caches the result' do
|
||||
expect(Clusters::AgentAuthorizationsFinder).to receive(:new).once
|
||||
.with(pipeline.project)
|
||||
.and_return(finder)
|
||||
|
||||
expect(pipeline.authorized_cluster_agents).to contain_exactly(agent)
|
||||
expect(pipeline.authorized_cluster_agents).to contain_exactly(agent) # cached
|
||||
expect(pipeline.cluster_agent_authorizations).to contain_exactly(authorization)
|
||||
expect(pipeline.cluster_agent_authorizations).to contain_exactly(authorization) # cached
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ RSpec.describe Ci::Variable do
|
|||
|
||||
context 'loose foreign key on ci_variables.project_id' do
|
||||
it_behaves_like 'cleanup by a loose foreign key' do
|
||||
let!(:parent) { create(:project) }
|
||||
let!(:parent) { create(:project, namespace: create(:group)) }
|
||||
let!(:model) { create(:ci_variable, project: parent) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,5 +10,5 @@ RSpec.describe Clusters::Agents::ImplicitAuthorization do
|
|||
it { expect(subject.agent).to eq(agent) }
|
||||
it { expect(subject.agent_id).to eq(agent.id) }
|
||||
it { expect(subject.config_project).to eq(agent.project) }
|
||||
it { expect(subject.config).to be_nil }
|
||||
it { expect(subject.config).to eq({}) }
|
||||
end
|
||||
|
|
|
@ -45,6 +45,13 @@ RSpec.describe UserPreference do
|
|||
it { is_expected.not_to allow_value(color).for(:diffs_addition_color) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'use_legacy_web_ide' do
|
||||
it { is_expected.to allow_value(true).for(:use_legacy_web_ide) }
|
||||
it { is_expected.to allow_value(false).for(:use_legacy_web_ide) }
|
||||
it { is_expected.not_to allow_value(nil).for(:use_legacy_web_ide) }
|
||||
it { is_expected.not_to allow_value("").for(:use_legacy_web_ide) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'notes filters global keys' do
|
||||
|
|
|
@ -78,6 +78,9 @@ RSpec.describe User do
|
|||
it { is_expected.to delegate_method(:diffs_addition_color).to(:user_preference) }
|
||||
it { is_expected.to delegate_method(:diffs_addition_color=).to(:user_preference).with_arguments(:args) }
|
||||
|
||||
it { is_expected.to delegate_method(:use_legacy_web_ide).to(:user_preference) }
|
||||
it { is_expected.to delegate_method(:use_legacy_web_ide=).to(:user_preference).with_arguments(:args) }
|
||||
|
||||
it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil }
|
||||
it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil }
|
||||
|
||||
|
|
|
@ -350,6 +350,15 @@ RSpec.describe Ci::BuildRunnerPresenter do
|
|||
)
|
||||
end
|
||||
|
||||
it 'logs file_variable_is_referenced_in_another_variable' do
|
||||
expect(Gitlab::AppJsonLogger).to receive(:info).with(
|
||||
event: 'file_variable_is_referenced_in_another_variable',
|
||||
project_id: project.id
|
||||
).once
|
||||
|
||||
runner_variables
|
||||
end
|
||||
|
||||
context 'when the FF ci_stop_expanding_file_vars_for_runners is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_stop_expanding_file_vars_for_runners: false)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe IdeController do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:reporter) { create(:user) }
|
||||
|
||||
let_it_be(:project) do
|
||||
|
@ -237,21 +239,29 @@ RSpec.describe IdeController do
|
|||
end
|
||||
|
||||
# This indirectly tests that `minimal: true` was passed to the fullscreen layout
|
||||
it 'does not render top nav' do
|
||||
subject
|
||||
|
||||
expect(response).not_to render_template(top_nav_partial)
|
||||
end
|
||||
|
||||
context 'without vscode_web_ide feature flag' do
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: false)
|
||||
describe 'layout' do
|
||||
where(:ff_state, :use_legacy_web_ide, :expect_top_nav) do
|
||||
false | false | true
|
||||
false | true | true
|
||||
true | true | true
|
||||
true | false | false
|
||||
end
|
||||
|
||||
it 'renders top nav' do
|
||||
subject
|
||||
with_them do
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: ff_state)
|
||||
allow(user).to receive(:use_legacy_web_ide).and_return(use_legacy_web_ide)
|
||||
|
||||
expect(response).to render_template(top_nav_partial)
|
||||
subject
|
||||
end
|
||||
|
||||
it 'handles rendering top nav' do
|
||||
if expect_top_nav
|
||||
expect(response).to render_template(top_nav_partial)
|
||||
else
|
||||
expect(response).not_to render_template(top_nav_partial)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fast_spec_helper'
|
||||
require_relative '../../../../scripts/lib/glfm/verify_all_generated_files_are_up_to_date'
|
||||
|
||||
# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#verify-all-generated-files-are-up-to-daterb-script
|
||||
# for details on the implementation and usage of the `verify_all_generated_files_are_up_to_date.rb` script being tested.
|
||||
# This developers guide contains diagrams and documentation of the script,
|
||||
# including explanations and examples of all files it reads and writes.
|
||||
RSpec.describe Glfm::VerifyAllGeneratedFilesAreUpToDate, '#process' do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:output_path) { described_class::GLFM_SPEC_OUTPUT_PATH }
|
||||
let(:snapshots_path) { described_class::EXAMPLE_SNAPSHOTS_PATH }
|
||||
let(:verify_cmd) { "git status --porcelain #{output_path} #{snapshots_path}" }
|
||||
|
||||
before do
|
||||
# Prevent console output when running tests
|
||||
allow(subject).to receive(:output)
|
||||
end
|
||||
|
||||
context 'when repo is dirty' do
|
||||
before do
|
||||
# Simulate a dirty repo
|
||||
allow(subject).to receive(:run_external_cmd).with(verify_cmd).and_return(" M #{output_path}")
|
||||
end
|
||||
|
||||
it 'raises an error', :unlimited_max_formatted_output_length do
|
||||
expect { subject.process }.to raise_error(/Cannot run.*uncommitted changes.*#{output_path}/m)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repo is clean' do
|
||||
before do
|
||||
# Mock out all yarn install and script execution
|
||||
allow(subject).to receive(:run_external_cmd).with('yarn install --frozen-lockfile')
|
||||
allow(subject).to receive(:run_external_cmd).with(/update-specification.rb/)
|
||||
allow(subject).to receive(:run_external_cmd).with(/update-example-snapshots.rb/)
|
||||
end
|
||||
|
||||
context 'when all generated files are up to date' do
|
||||
before do
|
||||
# Simulate a clean repo, then simulate no changes to generated files
|
||||
allow(subject).to receive(:run_external_cmd).twice.with(verify_cmd).and_return('', '')
|
||||
end
|
||||
|
||||
it 'does not raise an error', :unlimited_max_formatted_output_length do
|
||||
expect { subject.process }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when generated file(s) are not up to date' do
|
||||
before do
|
||||
# Simulate a clean repo, then simulate changes to generated files
|
||||
allow(subject).to receive(:run_external_cmd).twice.with(verify_cmd).and_return('', "M #{snapshots_path}")
|
||||
end
|
||||
|
||||
it 'raises an error', :unlimited_max_formatted_output_length do
|
||||
expect { subject.process }.to raise_error(/following files were modified.*#{snapshots_path}/m)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,9 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::UploadsExportService do
|
||||
let_it_be(:project) { create(:project, avatar: fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png')) }
|
||||
let_it_be(:upload) { create(:upload, :with_file, :issuable_upload, uploader: FileUploader, model: project) }
|
||||
let_it_be(:export_path) { Dir.mktmpdir }
|
||||
let_it_be(:project) { create(:project, avatar: fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png')) }
|
||||
|
||||
let!(:upload) { create(:upload, :with_file, :issuable_upload, uploader: FileUploader, model: project) }
|
||||
let(:exported_filepath) { File.join(export_path, upload.secret, upload.retrieve_uploader.filename) }
|
||||
|
||||
subject(:service) { described_class.new(project, export_path) }
|
||||
|
||||
|
@ -15,10 +17,60 @@ RSpec.describe BulkImports::UploadsExportService do
|
|||
|
||||
describe '#execute' do
|
||||
it 'exports project uploads and avatar' do
|
||||
subject.execute
|
||||
service.execute
|
||||
|
||||
expect(File.exist?(File.join(export_path, 'avatar', 'rails_sample.png'))).to eq(true)
|
||||
expect(File.exist?(File.join(export_path, upload.secret, upload.retrieve_uploader.filename))).to eq(true)
|
||||
expect(File).to exist(File.join(export_path, 'avatar', 'rails_sample.png'))
|
||||
expect(File).to exist(exported_filepath)
|
||||
end
|
||||
|
||||
context 'when upload has underlying file missing' do
|
||||
context 'with an upload missing its file' do
|
||||
it 'does not cause errors' do
|
||||
File.delete(upload.absolute_path)
|
||||
|
||||
expect { service.execute }.not_to raise_error
|
||||
|
||||
expect(File).not_to exist(exported_filepath)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when upload is in object storage' do
|
||||
before do
|
||||
stub_uploads_object_storage(FileUploader)
|
||||
end
|
||||
|
||||
shared_examples 'export with invalid upload' do
|
||||
it 'ignores problematic upload and logs exception' do
|
||||
allow(service).to receive(:download_or_copy_upload).and_raise(exception)
|
||||
|
||||
expect(Gitlab::ErrorTracking)
|
||||
.to receive(:log_exception)
|
||||
.with(
|
||||
instance_of(exception), {
|
||||
portable_id: project.id,
|
||||
portable_class: 'Project',
|
||||
upload_id: upload.id
|
||||
}
|
||||
)
|
||||
|
||||
service.execute
|
||||
|
||||
expect(File).not_to exist(exported_filepath)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filename is too long' do
|
||||
let(:exception) { Errno::ENAMETOOLONG }
|
||||
|
||||
include_examples 'export with invalid upload'
|
||||
end
|
||||
|
||||
context 'when network exception occurs' do
|
||||
let(:exception) { Net::OpenTimeout }
|
||||
|
||||
include_examples 'export with invalid upload'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,8 @@ RSpec.describe Ci::GenerateKubeconfigService do
|
|||
let(:pipeline) { build.pipeline }
|
||||
let(:agent1) { create(:cluster_agent, project: project) }
|
||||
let(:agent2) { create(:cluster_agent) }
|
||||
let(:authorization1) { create(:agent_project_authorization, agent: agent1) }
|
||||
let(:authorization2) { create(:agent_project_authorization, agent: agent2) }
|
||||
|
||||
let(:template) { instance_double(Gitlab::Kubernetes::Kubeconfig::Template) }
|
||||
|
||||
|
@ -16,7 +18,7 @@ RSpec.describe Ci::GenerateKubeconfigService do
|
|||
|
||||
before do
|
||||
expect(Gitlab::Kubernetes::Kubeconfig::Template).to receive(:new).and_return(template)
|
||||
expect(pipeline).to receive(:authorized_cluster_agents).and_return([agent1, agent2])
|
||||
expect(pipeline).to receive(:cluster_agent_authorizations).and_return([authorization1, authorization2])
|
||||
end
|
||||
|
||||
it 'adds a cluster, and a user and context for each available agent' do
|
||||
|
@ -36,11 +38,13 @@ RSpec.describe Ci::GenerateKubeconfigService do
|
|||
|
||||
expect(template).to receive(:add_context).with(
|
||||
name: "#{project.full_path}:#{agent1.name}",
|
||||
namespace: 'production',
|
||||
cluster: 'gitlab',
|
||||
user: "agent:#{agent1.id}"
|
||||
)
|
||||
expect(template).to receive(:add_context).with(
|
||||
name: "#{agent2.project.full_path}:#{agent2.name}",
|
||||
namespace: 'production',
|
||||
cluster: 'gitlab',
|
||||
user: "agent:#{agent2.id}"
|
||||
)
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.after do |example|
|
||||
[::ApplicationRecord, ::Ci::ApplicationRecord].each do |base_class|
|
||||
base_class.gitlab_transactions_stack.clear if base_class.respond_to?(:gitlab_transactions_stack)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -27,5 +27,9 @@ RSpec.configure do |config|
|
|||
# Reset after execution to preferred state
|
||||
config.after do |example_file|
|
||||
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress_in_rspec = true
|
||||
|
||||
[::ApplicationRecord, ::Ci::ApplicationRecord].each do |base_class|
|
||||
base_class.gitlab_transactions_stack.clear if base_class.respond_to?(:gitlab_transactions_stack)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,8 +50,8 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
|
|||
allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
|
||||
end
|
||||
|
||||
let!(:label) { create(:label, project: project1) }
|
||||
let!(:label2) { create(:label, project: project1) }
|
||||
let_it_be(:label) { create(:label, project: project1) }
|
||||
let_it_be(:label2) { create(:label, project: project1) }
|
||||
|
||||
let!(:merge_request1) do
|
||||
create(:merge_request, assignees: [user], author: user, reviewers: [user2],
|
||||
|
@ -87,13 +87,16 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
|
|||
let!(:label_link) { create(:label_link, label: label, target: merge_request2) }
|
||||
let!(:label_link2) { create(:label_link, label: label2, target: merge_request3) }
|
||||
|
||||
before do
|
||||
before_all do
|
||||
project1.add_maintainer(user)
|
||||
project2.add_developer(user)
|
||||
project3.add_developer(user)
|
||||
project4.add_developer(user)
|
||||
project5.add_developer(user)
|
||||
project6.add_developer(user)
|
||||
end
|
||||
|
||||
before do
|
||||
project2.add_developer(user)
|
||||
project3.add_developer(user)
|
||||
|
||||
project2.add_developer(user2)
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'fast_spec_helper'
|
||||
|
||||
require_relative '../../../tooling/quality/test_level'
|
||||
|
||||
|
|
Loading…
Reference in New Issue