Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0afd7f1817
commit
f719944dee
|
@ -1 +1 @@
|
|||
6e58da454633a53c86d14a59587ee7a6a9d11031
|
||||
58261cb08bae1bae4fed28173ce9457bd9728c76
|
||||
|
|
|
@ -50,6 +50,9 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
issuableId() {
|
||||
return getIdFromGraphQLId(this.issuable.id);
|
||||
},
|
||||
createdInPastDay() {
|
||||
const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
|
||||
return createdSecondsAgo < SECONDS_IN_DAY;
|
||||
|
@ -61,7 +64,7 @@ export default {
|
|||
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
|
||||
},
|
||||
authorId() {
|
||||
return getIdFromGraphQLId(`${this.author.id}`);
|
||||
return getIdFromGraphQLId(this.author.id);
|
||||
},
|
||||
isIssuableUrlExternal() {
|
||||
return isExternal(this.webUrl);
|
||||
|
@ -70,10 +73,10 @@ export default {
|
|||
return this.issuable.labels?.nodes || this.issuable.labels || [];
|
||||
},
|
||||
labelIdsString() {
|
||||
return JSON.stringify(this.labels.map((label) => label.id));
|
||||
return JSON.stringify(this.labels.map((label) => getIdFromGraphQLId(label.id)));
|
||||
},
|
||||
assignees() {
|
||||
return this.issuable.assignees || [];
|
||||
return this.issuable.assignees?.nodes || this.issuable.assignees || [];
|
||||
},
|
||||
createdAt() {
|
||||
return sprintf(__('created %{timeAgo}'), {
|
||||
|
@ -81,6 +84,9 @@ export default {
|
|||
});
|
||||
},
|
||||
updatedAt() {
|
||||
if (!this.issuable.updatedAt) {
|
||||
return '';
|
||||
}
|
||||
return sprintf(__('updated %{timeAgo}'), {
|
||||
timeAgo: getTimeago().format(this.issuable.updatedAt),
|
||||
});
|
||||
|
@ -157,7 +163,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<li
|
||||
:id="`issuable_${issuable.id}`"
|
||||
:id="`issuable_${issuableId}`"
|
||||
class="issue gl-px-5!"
|
||||
:class="{ closed: issuable.closedAt, today: createdInPastDay }"
|
||||
:data-labels="labelIdsString"
|
||||
|
@ -167,7 +173,7 @@ export default {
|
|||
<gl-form-checkbox
|
||||
class="gl-mr-0"
|
||||
:checked="checked"
|
||||
:data-id="issuable.id"
|
||||
:data-id="issuableId"
|
||||
@input="$emit('checked-input', $event)"
|
||||
>
|
||||
<span class="gl-sr-only">{{ issuable.title }}</span>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
|
||||
|
@ -211,7 +212,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
issuableId(issuable) {
|
||||
return issuable.id || issuable.iid || uniqueId();
|
||||
return getIdFromGraphQLId(issuable.id) || issuable.iid || uniqueId();
|
||||
},
|
||||
issuableChecked(issuable) {
|
||||
return this.checkedIssuables[this.issuableId(issuable)]?.checked;
|
||||
|
|
|
@ -42,6 +42,9 @@ export default {
|
|||
}
|
||||
return __('Milestone');
|
||||
},
|
||||
milestoneLink() {
|
||||
return this.issue.milestone.webPath || this.issue.milestone.webUrl;
|
||||
},
|
||||
dueDate() {
|
||||
return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true);
|
||||
},
|
||||
|
@ -49,7 +52,7 @@ export default {
|
|||
return isInPast(new Date(this.issue.dueDate));
|
||||
},
|
||||
timeEstimate() {
|
||||
return this.issue.timeStats?.humanTimeEstimate;
|
||||
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
|
||||
},
|
||||
showHealthStatus() {
|
||||
return this.hasIssuableHealthStatusFeature && this.issue.healthStatus;
|
||||
|
@ -85,7 +88,7 @@ export default {
|
|||
class="issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3"
|
||||
data-testid="issuable-milestone"
|
||||
>
|
||||
<gl-link v-gl-tooltip :href="issue.milestone.webUrl" :title="milestoneDate">
|
||||
<gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate">
|
||||
<gl-icon name="clock" />
|
||||
{{ issue.milestone.title }}
|
||||
</gl-link>
|
||||
|
|
|
@ -9,24 +9,21 @@ import {
|
|||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import { toNumber } from 'lodash';
|
||||
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
|
||||
import createFlash from '~/flash';
|
||||
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
||||
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
|
||||
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
|
||||
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
|
||||
import {
|
||||
API_PARAM,
|
||||
apiSortParams,
|
||||
CREATED_DESC,
|
||||
i18n,
|
||||
MAX_LIST_SIZE,
|
||||
PAGE_SIZE,
|
||||
PARAM_DUE_DATE,
|
||||
PARAM_PAGE,
|
||||
PARAM_SORT,
|
||||
PARAM_STATE,
|
||||
RELATIVE_POSITION_DESC,
|
||||
RELATIVE_POSITION_ASC,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_AUTHOR,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
|
@ -37,19 +34,19 @@ import {
|
|||
TOKEN_TYPE_MILESTONE,
|
||||
TOKEN_TYPE_WEIGHT,
|
||||
UPDATED_DESC,
|
||||
URL_PARAM,
|
||||
urlSortParams,
|
||||
} from '~/issues_list/constants';
|
||||
import {
|
||||
convertToParams,
|
||||
convertToApiParams,
|
||||
convertToSearchQuery,
|
||||
convertToUrlParams,
|
||||
getDueDateValue,
|
||||
getFilterTokens,
|
||||
getSortKey,
|
||||
getSortOptions,
|
||||
} from '~/issues_list/utils';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
|
||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||
import {
|
||||
DEFAULT_NONE_ANY,
|
||||
OPERATOR_IS_ONLY,
|
||||
|
@ -107,9 +104,6 @@ export default {
|
|||
emptyStateSvgPath: {
|
||||
default: '',
|
||||
},
|
||||
endpoint: {
|
||||
default: '',
|
||||
},
|
||||
exportCsvPath: {
|
||||
default: '',
|
||||
},
|
||||
|
@ -173,15 +167,53 @@ export default {
|
|||
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
|
||||
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
|
||||
filterTokens: getFilterTokens(window.location.search),
|
||||
isLoading: false,
|
||||
issues: [],
|
||||
page: toNumber(getParameterByName(PARAM_PAGE)) || 1,
|
||||
page: 1,
|
||||
pageInfo: {},
|
||||
pageParams: {
|
||||
firstPageSize: PAGE_SIZE,
|
||||
},
|
||||
showBulkEditSidebar: false,
|
||||
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
|
||||
state: state || IssuableStates.Opened,
|
||||
totalIssues: 0,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
issues: {
|
||||
query: getIssuesQuery,
|
||||
variables() {
|
||||
const filterParams = {
|
||||
...this.apiFilterParams,
|
||||
};
|
||||
|
||||
if (filterParams.epicId) {
|
||||
filterParams.epicId = filterParams.epicId.split('::&').pop();
|
||||
} else if (filterParams.not?.epicId) {
|
||||
filterParams.not.epicId = filterParams.not.epicId.split('::&').pop();
|
||||
}
|
||||
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
search: this.searchQuery,
|
||||
sort: this.sortKey,
|
||||
state: this.state,
|
||||
...this.pageParams,
|
||||
...filterParams,
|
||||
};
|
||||
},
|
||||
update: ({ project }) => project.issues.nodes,
|
||||
result({ data }) {
|
||||
this.pageInfo = data.project.issues.pageInfo;
|
||||
this.totalIssues = data.project.issues.count;
|
||||
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
|
||||
},
|
||||
error() {
|
||||
createFlash({ message: this.$options.i18n.errorFetchingIssues });
|
||||
},
|
||||
debounce: 200,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasSearch() {
|
||||
return this.searchQuery || Object.keys(this.urlFilterParams).length;
|
||||
|
@ -190,16 +222,22 @@ export default {
|
|||
return this.showBulkEditSidebar || !this.issues.length;
|
||||
},
|
||||
isManualOrdering() {
|
||||
return this.sortKey === RELATIVE_POSITION_DESC;
|
||||
return this.sortKey === RELATIVE_POSITION_ASC;
|
||||
},
|
||||
isOpenTab() {
|
||||
return this.state === IssuableStates.Opened;
|
||||
},
|
||||
nextPage() {
|
||||
return Number(this.pageInfo.hasNextPage);
|
||||
},
|
||||
previousPage() {
|
||||
return Number(this.pageInfo.hasPreviousPage);
|
||||
},
|
||||
apiFilterParams() {
|
||||
return convertToParams(this.filterTokens, API_PARAM);
|
||||
return convertToApiParams(this.filterTokens);
|
||||
},
|
||||
urlFilterParams() {
|
||||
return convertToParams(this.filterTokens, URL_PARAM);
|
||||
return convertToUrlParams(this.filterTokens);
|
||||
},
|
||||
searchQuery() {
|
||||
return convertToSearchQuery(this.filterTokens) || undefined;
|
||||
|
@ -214,6 +252,7 @@ export default {
|
|||
dataType: 'user',
|
||||
unique: true,
|
||||
defaultAuthors: [],
|
||||
operators: OPERATOR_IS_ONLY,
|
||||
fetchAuthors: this.fetchUsers,
|
||||
},
|
||||
{
|
||||
|
@ -240,7 +279,7 @@ export default {
|
|||
title: TOKEN_TITLE_LABEL,
|
||||
icon: 'labels',
|
||||
token: LabelToken,
|
||||
defaultLabels: [],
|
||||
defaultLabels: DEFAULT_NONE_ANY,
|
||||
fetchLabels: this.fetchLabels,
|
||||
},
|
||||
];
|
||||
|
@ -333,10 +372,9 @@ export default {
|
|||
|
||||
return {
|
||||
due_date: this.dueDateFilter,
|
||||
page: this.page,
|
||||
search: this.searchQuery,
|
||||
sort: urlSortParams[this.sortKey],
|
||||
state: this.state,
|
||||
...urlSortParams[this.sortKey],
|
||||
...filterParams,
|
||||
};
|
||||
},
|
||||
|
@ -346,7 +384,6 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
|
||||
this.fetchIssues();
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
|
||||
|
@ -386,59 +423,19 @@ export default {
|
|||
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
|
||||
},
|
||||
fetchIterations(search) {
|
||||
return axios.get(this.projectIterationsPath, { params: { search } });
|
||||
const number = Number(search);
|
||||
return !search || Number.isNaN(number)
|
||||
? axios.get(this.projectIterationsPath, { params: { search } })
|
||||
: axios.get(this.projectIterationsPath, { params: { id: number } });
|
||||
},
|
||||
fetchUsers(search) {
|
||||
return axios.get(this.autocompleteUsersPath, { params: { search } });
|
||||
},
|
||||
fetchIssues() {
|
||||
if (!this.hasProjectIssues) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
const filterParams = {
|
||||
...this.apiFilterParams,
|
||||
};
|
||||
|
||||
if (filterParams.epic_id) {
|
||||
filterParams.epic_id = filterParams.epic_id.split('::&').pop();
|
||||
} else if (filterParams['not[epic_id]']) {
|
||||
filterParams['not[epic_id]'] = filterParams['not[epic_id]'].split('::&').pop();
|
||||
}
|
||||
|
||||
return axios
|
||||
.get(this.endpoint, {
|
||||
params: {
|
||||
due_date: this.dueDateFilter,
|
||||
page: this.page,
|
||||
per_page: PAGE_SIZE,
|
||||
search: this.searchQuery,
|
||||
state: this.state,
|
||||
with_labels_details: true,
|
||||
...apiSortParams[this.sortKey],
|
||||
...filterParams,
|
||||
},
|
||||
})
|
||||
.then(({ data, headers }) => {
|
||||
this.page = Number(headers['x-page']);
|
||||
this.totalIssues = Number(headers['x-total']);
|
||||
this.issues = data.map((issue) => convertObjectPropsToCamelCase(issue, { deep: true }));
|
||||
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
|
||||
})
|
||||
.catch(() => {
|
||||
createFlash({ message: this.$options.i18n.errorFetchingIssues });
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
getExportCsvPathWithQuery() {
|
||||
return `${this.exportCsvPath}${window.location.search}`;
|
||||
},
|
||||
getStatus(issue) {
|
||||
if (issue.closedAt && issue.movedToId) {
|
||||
if (issue.closedAt && issue.moved) {
|
||||
return this.$options.i18n.closedMoved;
|
||||
}
|
||||
if (issue.closedAt) {
|
||||
|
@ -469,18 +466,30 @@ export default {
|
|||
},
|
||||
handleClickTab(state) {
|
||||
if (this.state !== state) {
|
||||
this.pageParams = {
|
||||
firstPageSize: PAGE_SIZE,
|
||||
};
|
||||
this.page = 1;
|
||||
}
|
||||
this.state = state;
|
||||
this.fetchIssues();
|
||||
},
|
||||
handleFilter(filter) {
|
||||
this.filterTokens = filter;
|
||||
this.fetchIssues();
|
||||
},
|
||||
handlePageChange(page) {
|
||||
if (page > this.page) {
|
||||
this.pageParams = {
|
||||
afterCursor: this.pageInfo.endCursor,
|
||||
firstPageSize: PAGE_SIZE,
|
||||
};
|
||||
} else {
|
||||
this.pageParams = {
|
||||
beforeCursor: this.pageInfo.startCursor,
|
||||
lastPageSize: PAGE_SIZE,
|
||||
};
|
||||
}
|
||||
|
||||
this.page = page;
|
||||
this.fetchIssues();
|
||||
},
|
||||
handleReorder({ newIndex, oldIndex }) {
|
||||
const issueToMove = this.issues[oldIndex];
|
||||
|
@ -517,7 +526,6 @@ export default {
|
|||
},
|
||||
handleSort(value) {
|
||||
this.sortKey = value;
|
||||
this.fetchIssues();
|
||||
},
|
||||
toggleBulkEditSidebar(showBulkEditSidebar) {
|
||||
this.showBulkEditSidebar = showBulkEditSidebar;
|
||||
|
@ -541,14 +549,13 @@ export default {
|
|||
:tabs="$options.IssuableListTabs"
|
||||
:current-tab="state"
|
||||
:tab-counts="tabCounts"
|
||||
:issuables-loading="isLoading"
|
||||
:issuables-loading="$apollo.loading"
|
||||
:is-manual-ordering="isManualOrdering"
|
||||
:show-bulk-edit-sidebar="showBulkEditSidebar"
|
||||
:show-pagination-controls="showPaginationControls"
|
||||
:total-items="totalIssues"
|
||||
:current-page="page"
|
||||
:previous-page="page - 1"
|
||||
:next-page="page + 1"
|
||||
:previous-page="previousPage"
|
||||
:next-page="nextPage"
|
||||
:url-params="urlParams"
|
||||
@click-tab="handleClickTab"
|
||||
@filter="handleFilter"
|
||||
|
@ -631,7 +638,7 @@ export default {
|
|||
</li>
|
||||
<blocking-issues-count
|
||||
class="gl-display-none gl-sm-display-block"
|
||||
:blocking-issues-count="issuable.blockingIssuesCount"
|
||||
:blocking-issues-count="issuable.blockedByCount"
|
||||
:is-list-item="true"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -101,7 +101,6 @@ export const i18n = {
|
|||
export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map';
|
||||
|
||||
export const PARAM_DUE_DATE = 'due_date';
|
||||
export const PARAM_PAGE = 'page';
|
||||
export const PARAM_SORT = 'sort';
|
||||
export const PARAM_STATE = 'state';
|
||||
|
||||
|
@ -125,21 +124,21 @@ export const CREATED_ASC = 'CREATED_ASC';
|
|||
export const CREATED_DESC = 'CREATED_DESC';
|
||||
export const DUE_DATE_ASC = 'DUE_DATE_ASC';
|
||||
export const DUE_DATE_DESC = 'DUE_DATE_DESC';
|
||||
export const LABEL_PRIORITY_ASC = 'LABEL_PRIORITY_ASC';
|
||||
export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
|
||||
export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
|
||||
export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
|
||||
export const POPULARITY_ASC = 'POPULARITY_ASC';
|
||||
export const POPULARITY_DESC = 'POPULARITY_DESC';
|
||||
export const PRIORITY_ASC = 'PRIORITY_ASC';
|
||||
export const PRIORITY_DESC = 'PRIORITY_DESC';
|
||||
export const RELATIVE_POSITION_DESC = 'RELATIVE_POSITION_DESC';
|
||||
export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC';
|
||||
export const UPDATED_ASC = 'UPDATED_ASC';
|
||||
export const UPDATED_DESC = 'UPDATED_DESC';
|
||||
export const WEIGHT_ASC = 'WEIGHT_ASC';
|
||||
export const WEIGHT_DESC = 'WEIGHT_DESC';
|
||||
|
||||
const SORT_ASC = 'asc';
|
||||
const SORT_DESC = 'desc';
|
||||
|
||||
const PRIORITY_ASC_SORT = 'priority_asc';
|
||||
const CREATED_DATE_SORT = 'created_date';
|
||||
const CREATED_ASC_SORT = 'created_asc';
|
||||
const UPDATED_DESC_SORT = 'updated_desc';
|
||||
|
@ -147,129 +146,30 @@ const UPDATED_ASC_SORT = 'updated_asc';
|
|||
const MILESTONE_SORT = 'milestone';
|
||||
const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
|
||||
const DUE_DATE_DESC_SORT = 'due_date_desc';
|
||||
const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
|
||||
const POPULARITY_ASC_SORT = 'popularity_asc';
|
||||
const WEIGHT_DESC_SORT = 'weight_desc';
|
||||
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
|
||||
const BLOCKING_ISSUES = 'blocking_issues';
|
||||
|
||||
export const apiSortParams = {
|
||||
[PRIORITY_DESC]: {
|
||||
order_by: PRIORITY,
|
||||
sort: SORT_DESC,
|
||||
},
|
||||
[CREATED_ASC]: {
|
||||
order_by: CREATED_AT,
|
||||
sort: SORT_ASC,
|
||||
},
|
||||
[CREATED_DESC]: {
|
||||
order_by: CREATED_AT,
|
||||
sort: SORT_DESC,
|
||||
},
|
||||
[UPDATED_ASC]: {
|
||||
order_by: UPDATED_AT,
|
||||
sort: SORT_ASC,
|
||||
},
|
||||
[UPDATED_DESC]: {
|
||||
order_by: UPDATED_AT,
|
||||
sort: SORT_DESC,
|
||||
},
|
||||
[MILESTONE_DUE_ASC]: {
|
||||
order_by: MILESTONE_DUE,
|
||||
sort: SORT_ASC,
|
||||
},
|
||||
[MILESTONE_DUE_DESC]: {
|
||||
order_by: MILESTONE_DUE,
|
||||
sort: SORT_DESC,
|
||||
},
|
||||
[DUE_DATE_ASC]: {
|
||||
order_by: DUE_DATE,
|
||||
sort: SORT_ASC,
|
||||
},
|
||||
[DUE_DATE_DESC]: {
|
||||
order_by: DUE_DATE,
|
||||
sort: SORT_DESC,
|
||||
},
|
||||
[POPULARITY_ASC]: {
|
||||
order_by: POPULARITY,
|
||||
sort: SORT_ASC,
|
||||
},
|
||||
[POPULARITY_DESC]: {
|
||||
order_by: POPULARITY,
|
||||
sort: SORT_DESC,
|
||||
},
|
||||
[LABEL_PRIORITY_DESC]: {
|
||||
order_by: LABEL_PRIORITY,
|
||||
sort: SORT_DESC,
|
||||
},
|
||||
[RELATIVE_POSITION_DESC]: {
|
||||
order_by: RELATIVE_POSITION,
|
||||
per_page: 100,
|
||||
sort: SORT_ASC,
|
||||
},
|
||||
[WEIGHT_ASC]: {
|
||||
order_by: WEIGHT,
|
||||
sort: SORT_ASC,
|
||||
},
|
||||
[WEIGHT_DESC]: {
|
||||
order_by: WEIGHT,
|
||||
sort: SORT_DESC,
|
||||
},
|
||||
[BLOCKING_ISSUES_DESC]: {
|
||||
order_by: BLOCKING_ISSUES,
|
||||
sort: SORT_DESC,
|
||||
},
|
||||
};
|
||||
|
||||
export const urlSortParams = {
|
||||
[PRIORITY_DESC]: {
|
||||
sort: PRIORITY,
|
||||
},
|
||||
[CREATED_ASC]: {
|
||||
sort: CREATED_ASC_SORT,
|
||||
},
|
||||
[CREATED_DESC]: {
|
||||
sort: CREATED_DATE_SORT,
|
||||
},
|
||||
[UPDATED_ASC]: {
|
||||
sort: UPDATED_ASC_SORT,
|
||||
},
|
||||
[UPDATED_DESC]: {
|
||||
sort: UPDATED_DESC_SORT,
|
||||
},
|
||||
[MILESTONE_DUE_ASC]: {
|
||||
sort: MILESTONE_SORT,
|
||||
},
|
||||
[MILESTONE_DUE_DESC]: {
|
||||
sort: MILESTONE_DUE_DESC_SORT,
|
||||
},
|
||||
[DUE_DATE_ASC]: {
|
||||
sort: DUE_DATE,
|
||||
},
|
||||
[DUE_DATE_DESC]: {
|
||||
sort: DUE_DATE_DESC_SORT,
|
||||
},
|
||||
[POPULARITY_ASC]: {
|
||||
sort: POPULARITY_ASC_SORT,
|
||||
},
|
||||
[POPULARITY_DESC]: {
|
||||
sort: POPULARITY,
|
||||
},
|
||||
[LABEL_PRIORITY_DESC]: {
|
||||
sort: LABEL_PRIORITY,
|
||||
},
|
||||
[RELATIVE_POSITION_DESC]: {
|
||||
sort: RELATIVE_POSITION,
|
||||
per_page: 100,
|
||||
},
|
||||
[WEIGHT_ASC]: {
|
||||
sort: WEIGHT,
|
||||
},
|
||||
[WEIGHT_DESC]: {
|
||||
sort: WEIGHT_DESC_SORT,
|
||||
},
|
||||
[BLOCKING_ISSUES_DESC]: {
|
||||
sort: BLOCKING_ISSUES_DESC_SORT,
|
||||
},
|
||||
[PRIORITY_ASC]: PRIORITY_ASC_SORT,
|
||||
[PRIORITY_DESC]: PRIORITY,
|
||||
[CREATED_ASC]: CREATED_ASC_SORT,
|
||||
[CREATED_DESC]: CREATED_DATE_SORT,
|
||||
[UPDATED_ASC]: UPDATED_ASC_SORT,
|
||||
[UPDATED_DESC]: UPDATED_DESC_SORT,
|
||||
[MILESTONE_DUE_ASC]: MILESTONE_SORT,
|
||||
[MILESTONE_DUE_DESC]: MILESTONE_DUE_DESC_SORT,
|
||||
[DUE_DATE_ASC]: DUE_DATE,
|
||||
[DUE_DATE_DESC]: DUE_DATE_DESC_SORT,
|
||||
[POPULARITY_ASC]: POPULARITY_ASC_SORT,
|
||||
[POPULARITY_DESC]: POPULARITY,
|
||||
[LABEL_PRIORITY_ASC]: LABEL_PRIORITY_ASC_SORT,
|
||||
[LABEL_PRIORITY_DESC]: LABEL_PRIORITY,
|
||||
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
|
||||
[WEIGHT_ASC]: WEIGHT,
|
||||
[WEIGHT_DESC]: WEIGHT_DESC_SORT,
|
||||
[BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT,
|
||||
};
|
||||
|
||||
export const MAX_LIST_SIZE = 10;
|
||||
|
@ -294,12 +194,7 @@ export const TOKEN_TYPE_WEIGHT = 'weight';
|
|||
export const filters = {
|
||||
[TOKEN_TYPE_AUTHOR]: {
|
||||
[API_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'author_username',
|
||||
},
|
||||
[OPERATOR_IS_NOT]: {
|
||||
[NORMAL_FILTER]: 'not[author_username]',
|
||||
},
|
||||
[NORMAL_FILTER]: 'authorUsername',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
|
@ -312,13 +207,8 @@ export const filters = {
|
|||
},
|
||||
[TOKEN_TYPE_ASSIGNEE]: {
|
||||
[API_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'assignee_username',
|
||||
[SPECIAL_FILTER]: 'assignee_id',
|
||||
},
|
||||
[OPERATOR_IS_NOT]: {
|
||||
[NORMAL_FILTER]: 'not[assignee_username]',
|
||||
},
|
||||
[NORMAL_FILTER]: 'assigneeUsernames',
|
||||
[SPECIAL_FILTER]: 'assigneeId',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
|
@ -333,12 +223,7 @@ export const filters = {
|
|||
},
|
||||
[TOKEN_TYPE_MILESTONE]: {
|
||||
[API_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'milestone',
|
||||
},
|
||||
[OPERATOR_IS_NOT]: {
|
||||
[NORMAL_FILTER]: 'not[milestone]',
|
||||
},
|
||||
[NORMAL_FILTER]: 'milestoneTitle',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
|
@ -351,16 +236,13 @@ export const filters = {
|
|||
},
|
||||
[TOKEN_TYPE_LABEL]: {
|
||||
[API_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'labels',
|
||||
},
|
||||
[OPERATOR_IS_NOT]: {
|
||||
[NORMAL_FILTER]: 'not[labels]',
|
||||
},
|
||||
[NORMAL_FILTER]: 'labelName',
|
||||
[SPECIAL_FILTER]: 'labelName',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'label_name[]',
|
||||
[SPECIAL_FILTER]: 'label_name[]',
|
||||
},
|
||||
[OPERATOR_IS_NOT]: {
|
||||
[NORMAL_FILTER]: 'not[label_name][]',
|
||||
|
@ -369,10 +251,8 @@ export const filters = {
|
|||
},
|
||||
[TOKEN_TYPE_MY_REACTION]: {
|
||||
[API_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'my_reaction_emoji',
|
||||
[SPECIAL_FILTER]: 'my_reaction_emoji',
|
||||
},
|
||||
[NORMAL_FILTER]: 'myReactionEmoji',
|
||||
[SPECIAL_FILTER]: 'myReactionEmoji',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
|
@ -383,9 +263,7 @@ export const filters = {
|
|||
},
|
||||
[TOKEN_TYPE_CONFIDENTIAL]: {
|
||||
[API_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'confidential',
|
||||
},
|
||||
[NORMAL_FILTER]: 'confidential',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
|
@ -395,33 +273,23 @@ export const filters = {
|
|||
},
|
||||
[TOKEN_TYPE_ITERATION]: {
|
||||
[API_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'iteration_title',
|
||||
[SPECIAL_FILTER]: 'iteration_id',
|
||||
},
|
||||
[OPERATOR_IS_NOT]: {
|
||||
[NORMAL_FILTER]: 'not[iteration_title]',
|
||||
},
|
||||
[NORMAL_FILTER]: 'iterationId',
|
||||
[SPECIAL_FILTER]: 'iterationWildcardId',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'iteration_title',
|
||||
[NORMAL_FILTER]: 'iteration_id',
|
||||
[SPECIAL_FILTER]: 'iteration_id',
|
||||
},
|
||||
[OPERATOR_IS_NOT]: {
|
||||
[NORMAL_FILTER]: 'not[iteration_title]',
|
||||
[NORMAL_FILTER]: 'not[iteration_id]',
|
||||
},
|
||||
},
|
||||
},
|
||||
[TOKEN_TYPE_EPIC]: {
|
||||
[API_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'epic_id',
|
||||
[SPECIAL_FILTER]: 'epic_id',
|
||||
},
|
||||
[OPERATOR_IS_NOT]: {
|
||||
[NORMAL_FILTER]: 'not[epic_id]',
|
||||
},
|
||||
[NORMAL_FILTER]: 'epicId',
|
||||
[SPECIAL_FILTER]: 'epicId',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
|
@ -435,13 +303,8 @@ export const filters = {
|
|||
},
|
||||
[TOKEN_TYPE_WEIGHT]: {
|
||||
[API_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'weight',
|
||||
[SPECIAL_FILTER]: 'weight',
|
||||
},
|
||||
[OPERATOR_IS_NOT]: {
|
||||
[NORMAL_FILTER]: 'not[weight]',
|
||||
},
|
||||
[NORMAL_FILTER]: 'weight',
|
||||
[SPECIAL_FILTER]: 'weight',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
|
|
|
@ -73,6 +73,13 @@ export function mountIssuesListApp() {
|
|||
return false;
|
||||
}
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient,
|
||||
});
|
||||
|
||||
const {
|
||||
autocompleteAwardEmojisPath,
|
||||
autocompleteUsersPath,
|
||||
|
@ -83,7 +90,6 @@ export function mountIssuesListApp() {
|
|||
email,
|
||||
emailsHelpPagePath,
|
||||
emptyStateSvgPath,
|
||||
endpoint,
|
||||
exportCsvPath,
|
||||
groupEpicsPath,
|
||||
hasBlockedIssuesFeature,
|
||||
|
@ -115,14 +121,13 @@ export function mountIssuesListApp() {
|
|||
el,
|
||||
// Currently does not use Vue Apollo, but need to provide {} for now until the
|
||||
// issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
|
||||
apolloProvider: {},
|
||||
apolloProvider,
|
||||
provide: {
|
||||
autocompleteAwardEmojisPath,
|
||||
autocompleteUsersPath,
|
||||
calendarPath,
|
||||
canBulkUpdate: parseBoolean(canBulkUpdate),
|
||||
emptyStateSvgPath,
|
||||
endpoint,
|
||||
groupEpicsPath,
|
||||
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
|
||||
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
|
||||
#import "./issue_info.fragment.graphql"
|
||||
|
||||
query getProjectIssues(
|
||||
$projectPath: ID!
|
||||
$search: String
|
||||
$sort: IssueSort
|
||||
$state: IssuableState
|
||||
$assigneeId: String
|
||||
$authorUsername: String
|
||||
$assigneeUsernames: [String!]
|
||||
$milestoneTitle: [String]
|
||||
$labelName: [String]
|
||||
$not: NegatedIssueFilterInput
|
||||
$beforeCursor: String
|
||||
$afterCursor: String
|
||||
$firstPageSize: Int
|
||||
$lastPageSize: Int
|
||||
) {
|
||||
project(fullPath: $projectPath) {
|
||||
issues(
|
||||
search: $search
|
||||
sort: $sort
|
||||
state: $state
|
||||
assigneeId: $assigneeId
|
||||
authorUsername: $authorUsername
|
||||
assigneeUsernames: $assigneeUsernames
|
||||
milestoneTitle: $milestoneTitle
|
||||
labelName: $labelName
|
||||
not: $not
|
||||
before: $beforeCursor
|
||||
after: $afterCursor
|
||||
first: $firstPageSize
|
||||
last: $lastPageSize
|
||||
) {
|
||||
count
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
}
|
||||
nodes {
|
||||
...IssueInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
fragment IssueInfo on Issue {
|
||||
id
|
||||
iid
|
||||
closedAt
|
||||
confidential
|
||||
createdAt
|
||||
downvotes
|
||||
dueDate
|
||||
humanTimeEstimate
|
||||
moved
|
||||
title
|
||||
updatedAt
|
||||
upvotes
|
||||
userDiscussionsCount
|
||||
webUrl
|
||||
assignees {
|
||||
nodes {
|
||||
id
|
||||
avatarUrl
|
||||
name
|
||||
username
|
||||
webUrl
|
||||
}
|
||||
}
|
||||
author {
|
||||
id
|
||||
avatarUrl
|
||||
name
|
||||
username
|
||||
webUrl
|
||||
}
|
||||
labels {
|
||||
nodes {
|
||||
id
|
||||
color
|
||||
title
|
||||
description
|
||||
}
|
||||
}
|
||||
milestone {
|
||||
id
|
||||
dueDate
|
||||
startDate
|
||||
webPath
|
||||
title
|
||||
}
|
||||
taskCompletionStatus {
|
||||
completedCount
|
||||
count
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
API_PARAM,
|
||||
BLOCKING_ISSUES_DESC,
|
||||
CREATED_ASC,
|
||||
CREATED_DESC,
|
||||
|
@ -6,29 +7,36 @@ import {
|
|||
DUE_DATE_DESC,
|
||||
DUE_DATE_VALUES,
|
||||
filters,
|
||||
LABEL_PRIORITY_ASC,
|
||||
LABEL_PRIORITY_DESC,
|
||||
MILESTONE_DUE_ASC,
|
||||
MILESTONE_DUE_DESC,
|
||||
NORMAL_FILTER,
|
||||
POPULARITY_ASC,
|
||||
POPULARITY_DESC,
|
||||
PRIORITY_ASC,
|
||||
PRIORITY_DESC,
|
||||
RELATIVE_POSITION_DESC,
|
||||
RELATIVE_POSITION_ASC,
|
||||
SPECIAL_FILTER,
|
||||
SPECIAL_FILTER_VALUES,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_ITERATION,
|
||||
UPDATED_ASC,
|
||||
UPDATED_DESC,
|
||||
URL_PARAM,
|
||||
urlSortParams,
|
||||
WEIGHT_ASC,
|
||||
WEIGHT_DESC,
|
||||
} from '~/issues_list/constants';
|
||||
import { isPositiveInteger } from '~/lib/utils/number_utils';
|
||||
import { __ } from '~/locale';
|
||||
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import {
|
||||
FILTERED_SEARCH_TERM,
|
||||
OPERATOR_IS_NOT,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
|
||||
export const getSortKey = (sort) =>
|
||||
Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort);
|
||||
Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort);
|
||||
|
||||
export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
|
||||
|
||||
|
@ -38,7 +46,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
|
|||
id: 1,
|
||||
title: __('Priority'),
|
||||
sortDirection: {
|
||||
ascending: PRIORITY_DESC,
|
||||
ascending: PRIORITY_ASC,
|
||||
descending: PRIORITY_DESC,
|
||||
},
|
||||
},
|
||||
|
@ -86,7 +94,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
|
|||
id: 7,
|
||||
title: __('Label priority'),
|
||||
sortDirection: {
|
||||
ascending: LABEL_PRIORITY_DESC,
|
||||
ascending: LABEL_PRIORITY_ASC,
|
||||
descending: LABEL_PRIORITY_DESC,
|
||||
},
|
||||
},
|
||||
|
@ -94,8 +102,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
|
|||
id: 8,
|
||||
title: __('Manual'),
|
||||
sortDirection: {
|
||||
ascending: RELATIVE_POSITION_DESC,
|
||||
descending: RELATIVE_POSITION_DESC,
|
||||
ascending: RELATIVE_POSITION_ASC,
|
||||
descending: RELATIVE_POSITION_ASC,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -178,12 +186,36 @@ const getFilterType = (data, tokenType = '') =>
|
|||
? SPECIAL_FILTER
|
||||
: NORMAL_FILTER;
|
||||
|
||||
export const convertToParams = (filterTokens, paramType) =>
|
||||
const isIterationSpecialValue = (tokenType, value) =>
|
||||
tokenType === TOKEN_TYPE_ITERATION && SPECIAL_FILTER_VALUES.includes(value);
|
||||
|
||||
export const convertToApiParams = (filterTokens) => {
|
||||
const params = {};
|
||||
const not = {};
|
||||
|
||||
filterTokens
|
||||
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
|
||||
.forEach((token) => {
|
||||
const filterType = getFilterType(token.value.data, token.type);
|
||||
const field = filters[token.type][API_PARAM][filterType];
|
||||
const obj = token.value.operator === OPERATOR_IS_NOT ? not : params;
|
||||
const data = isIterationSpecialValue(token.type, token.value.data)
|
||||
? token.value.data.toUpperCase()
|
||||
: token.value.data;
|
||||
Object.assign(obj, {
|
||||
[field]: obj[field] ? [obj[field], data].flat() : data,
|
||||
});
|
||||
});
|
||||
|
||||
return Object.keys(not).length ? Object.assign(params, { not }) : params;
|
||||
};
|
||||
|
||||
export const convertToUrlParams = (filterTokens) =>
|
||||
filterTokens
|
||||
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
|
||||
.reduce((acc, token) => {
|
||||
const filterType = getFilterType(token.value.data, token.type);
|
||||
const param = filters[token.type][paramType][token.value.operator]?.[filterType];
|
||||
const param = filters[token.type][URL_PARAM][token.value.operator]?.[filterType];
|
||||
return Object.assign(acc, {
|
||||
[param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
|
||||
});
|
||||
|
|
|
@ -214,7 +214,7 @@ export default {
|
|||
<div></div>
|
||||
</template>
|
||||
</gl-modal>
|
||||
<span class="gl-text-white">{{ title }}</span>
|
||||
{{ title }}
|
||||
<request-warning :html-id="htmlId" :warnings="warnings" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -141,7 +141,7 @@ export default {
|
|||
<div id="peek-view-host" class="view">
|
||||
<span
|
||||
v-if="hasHost"
|
||||
class="current-host gl-text-white"
|
||||
class="current-host"
|
||||
:class="{ canary: currentRequest.details.host.canary }"
|
||||
>
|
||||
<span v-html="birdEmoji"></span>
|
||||
|
|
|
@ -229,7 +229,7 @@ export default {
|
|||
@expand-widget="expandWidget"
|
||||
/>
|
||||
</template>
|
||||
<template #default>
|
||||
<template #default="{ edit }">
|
||||
<user-select
|
||||
ref="userSelect"
|
||||
v-model="selected"
|
||||
|
@ -240,6 +240,7 @@ export default {
|
|||
:allow-multiple-assignees="allowMultipleAssignees"
|
||||
:current-user="currentUser"
|
||||
:issuable-type="issuableType"
|
||||
:is-editing="edit"
|
||||
class="gl-w-full dropdown-menu-user"
|
||||
@toggle="collapseWidget"
|
||||
@error="showError"
|
||||
|
|
|
@ -39,7 +39,7 @@ export default {
|
|||
return this.value.data;
|
||||
},
|
||||
activeIteration() {
|
||||
return this.iterations.find((iteration) => iteration.title === this.currentValue);
|
||||
return this.iterations.find((iteration) => iteration.id === Number(this.currentValue));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -99,8 +99,8 @@ export default {
|
|||
<template v-else>
|
||||
<gl-filtered-search-suggestion
|
||||
v-for="iteration in iterations"
|
||||
:key="iteration.title"
|
||||
:value="iteration.title"
|
||||
:key="iteration.id"
|
||||
:value="String(iteration.id)"
|
||||
>
|
||||
{{ iteration.title }}
|
||||
</gl-filtered-search-suggestion>
|
||||
|
|
|
@ -60,6 +60,11 @@ export default {
|
|||
required: false,
|
||||
default: 'issue',
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -75,7 +80,7 @@ export default {
|
|||
return participantsQueries[this.issuableType].query;
|
||||
},
|
||||
skip() {
|
||||
return Boolean(participantsQueries[this.issuableType].skipQuery);
|
||||
return Boolean(participantsQueries[this.issuableType].skipQuery) || !this.isEditing;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
|
@ -102,6 +107,9 @@ export default {
|
|||
first: 20,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.isEditing;
|
||||
},
|
||||
update(data) {
|
||||
// TODO Remove null filter (BE fix required)
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/329750
|
||||
|
|
|
@ -11,47 +11,40 @@
|
|||
height: $performance-bar-height;
|
||||
background: $black;
|
||||
line-height: $performance-bar-height;
|
||||
color: $gray-300;
|
||||
color: $gray-100;
|
||||
|
||||
select {
|
||||
color: $white;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
input {
|
||||
color: $gray-300;
|
||||
width: $input-short-width - 60px;
|
||||
}
|
||||
|
||||
select,
|
||||
input {
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
option {
|
||||
color: initial;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.production {
|
||||
background-color: $perf-bar-production;
|
||||
|
||||
select,
|
||||
input {
|
||||
background: $perf-bar-production;
|
||||
}
|
||||
}
|
||||
|
||||
&.staging {
|
||||
background-color: $perf-bar-staging;
|
||||
|
||||
select,
|
||||
input {
|
||||
background: $perf-bar-staging;
|
||||
}
|
||||
}
|
||||
|
||||
&.development {
|
||||
background-color: $perf-bar-development;
|
||||
|
||||
select,
|
||||
input {
|
||||
background: $perf-bar-development;
|
||||
}
|
||||
}
|
||||
|
||||
// UI Elements
|
||||
|
@ -61,7 +54,6 @@
|
|||
padding: 4px 6px;
|
||||
font-family: Consolas, 'Liberation Mono', Courier, monospace;
|
||||
line-height: 1;
|
||||
color: $gray-100;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from,
|
||||
inset 0 1px 2px $perf-bar-bucket-box-shadow-to;
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
class Projects::AlertManagementController < Projects::ApplicationController
|
||||
before_action :authorize_read_alert_management_alert!
|
||||
|
||||
before_action(only: [:index]) do
|
||||
push_frontend_feature_flag(:managed_alerts_deprecation, @project)
|
||||
end
|
||||
|
||||
feature_category :incident_management
|
||||
|
||||
def index
|
||||
|
|
|
@ -6,7 +6,7 @@ module Types
|
|||
graphql_name 'PackageMetadata'
|
||||
description 'Represents metadata associated with a Package'
|
||||
|
||||
possible_types ::Types::Packages::Composer::MetadatumType, ::Types::Packages::Conan::MetadatumType, ::Types::Packages::Maven::MetadatumType, ::Types::Packages::Nuget::MetadatumType
|
||||
possible_types ::Types::Packages::Composer::MetadatumType, ::Types::Packages::Conan::MetadatumType, ::Types::Packages::Maven::MetadatumType, ::Types::Packages::Nuget::MetadatumType, ::Types::Packages::Pypi::MetadatumType
|
||||
|
||||
def self.resolve_type(object, context)
|
||||
case object
|
||||
|
@ -18,6 +18,8 @@ module Types
|
|||
::Types::Packages::Maven::MetadatumType
|
||||
when ::Packages::Nuget::Metadatum
|
||||
::Types::Packages::Nuget::MetadatumType
|
||||
when ::Packages::Pypi::Metadatum
|
||||
::Types::Packages::Pypi::MetadatumType
|
||||
else
|
||||
# NOTE: This method must be kept in sync with `PackageWithoutVersionsType#metadata`,
|
||||
# which must never produce data that this discriminator cannot handle.
|
||||
|
|
|
@ -49,6 +49,8 @@ module Types
|
|||
object.maven_metadatum
|
||||
when 'nuget'
|
||||
object.nuget_metadatum
|
||||
when 'pypi'
|
||||
object.pypi_metadatum
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Packages
|
||||
module Pypi
|
||||
class MetadatumType < BaseObject
|
||||
graphql_name 'PypiMetadata'
|
||||
description 'Pypi metadata'
|
||||
|
||||
authorize :read_package
|
||||
|
||||
field :id, ::Types::GlobalIDType[::Packages::Pypi::Metadatum], null: false, description: 'ID of the metadatum.'
|
||||
field :required_python, GraphQL::STRING_TYPE, null: true, description: 'Required Python version of the Pypi package.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -190,7 +190,6 @@ module IssuesHelper
|
|||
email: current_user&.notification_email,
|
||||
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
|
||||
empty_state_svg_path: image_path('illustrations/issues.svg'),
|
||||
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
|
||||
export_csv_path: export_csv_project_issues_path(project),
|
||||
has_project_issues: project_issues(project).exists?.to_s,
|
||||
import_csv_issues_path: import_csv_namespace_project_issues_path,
|
||||
|
|
|
@ -18,6 +18,8 @@ class LfsObject < ApplicationRecord
|
|||
|
||||
mount_file_store_uploader LfsObjectUploader
|
||||
|
||||
BATCH_SIZE = 3000
|
||||
|
||||
def self.not_linked_to_project(project)
|
||||
where('NOT EXISTS (?)',
|
||||
project.lfs_objects_projects.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
|
||||
|
@ -37,13 +39,14 @@ class LfsObject < ApplicationRecord
|
|||
file_store == LfsObjectUploader::Store::LOCAL
|
||||
end
|
||||
|
||||
# rubocop: disable Cop/DestroyAll
|
||||
def self.destroy_unreferenced
|
||||
joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
|
||||
.where(lfs_objects_projects: { id: nil })
|
||||
.destroy_all
|
||||
def self.unreferenced_in_batches
|
||||
each_batch(of: BATCH_SIZE, order: :desc) do |lfs_objects|
|
||||
relation = lfs_objects.where('NOT EXISTS (?)',
|
||||
LfsObjectsProject.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
|
||||
|
||||
yield relation if relation.any?
|
||||
end
|
||||
end
|
||||
# rubocop: enable Cop/DestroyAll
|
||||
|
||||
def self.calculate_oid(path)
|
||||
self.hexdigest(path)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
module Packages
|
||||
module Pypi
|
||||
class MetadatumPolicy < BasePolicy
|
||||
delegate { @subject.package }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ module Commits
|
|||
super
|
||||
|
||||
@commit = params[:commit]
|
||||
@message = params[:message]
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -14,7 +15,9 @@ module Commits
|
|||
raise NotImplementedError unless repository.respond_to?(action)
|
||||
|
||||
# rubocop:disable GitlabSecurity/PublicSend
|
||||
message = @commit.public_send(:"#{action}_message", current_user)
|
||||
message =
|
||||
@message || @commit.public_send(:"#{action}_message", current_user)
|
||||
|
||||
repository.public_send(
|
||||
action,
|
||||
current_user,
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Manage Web IDE features')
|
||||
= _('Manage Web IDE features.')
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form', id: 'web-ide-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
@ -100,7 +100,8 @@
|
|||
= f.label :web_ide_clientside_preview_enabled, class: 'form-check-label' do
|
||||
= s_('IDE|Live Preview')
|
||||
%span.form-text.text-muted
|
||||
= s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview.')
|
||||
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('user/project/web_ide/index', anchor: 'enable-live-preview') }
|
||||
= s_('Preview JavaScript projects in the Web IDE with CodeSandbox Live Preview. %{link_start}Learn more.%{link_end} ').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
|
||||
|
||||
= render_if_exists 'admin/application_settings/maintenance_mode_settings_form'
|
||||
|
|
|
@ -467,7 +467,7 @@
|
|||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: cronjob:repository_archive_cache
|
||||
:worker_name: RepositoryArchiveCacheWorker
|
||||
|
@ -1078,15 +1078,6 @@
|
|||
:weight: 2
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: incident_management:incident_management_process_alert
|
||||
:worker_name: IncidentManagement::ProcessAlertWorker
|
||||
:feature_category: :incident_management
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 2
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: incident_management:incident_management_process_alert_worker_v2
|
||||
:worker_name: IncidentManagement::ProcessAlertWorkerV2
|
||||
:feature_category: :incident_management
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module IncidentManagement
|
||||
class ProcessAlertWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
|
||||
sidekiq_options retry: 3
|
||||
|
||||
queue_namespace :incident_management
|
||||
feature_category :incident_management
|
||||
|
||||
# `project_id` and `alert_payload` are deprecated and can be removed
|
||||
# starting from 14.0 release
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/224500
|
||||
#
|
||||
# This worker is not scheduled anymore since
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60285
|
||||
# and will be removed completely via
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/224500
|
||||
# in 14.0.
|
||||
def perform(_project_id = nil, _alert_payload = nil, alert_id = nil)
|
||||
return unless alert_id
|
||||
|
||||
alert = find_alert(alert_id)
|
||||
return unless alert
|
||||
|
||||
result = create_issue_for(alert)
|
||||
return if result.success?
|
||||
|
||||
log_warning(alert, result)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_alert(alert_id)
|
||||
AlertManagement::Alert.find_by_id(alert_id)
|
||||
end
|
||||
|
||||
def create_issue_for(alert)
|
||||
AlertManagement::CreateAlertIssueService
|
||||
.new(alert, User.alert_bot)
|
||||
.execute
|
||||
end
|
||||
|
||||
def log_warning(alert, result)
|
||||
issue_id = result.payload[:issue]&.id
|
||||
|
||||
Gitlab::AppLogger.warn(
|
||||
message: 'Cannot process an Incident',
|
||||
issue_id: issue_id,
|
||||
alert_id: alert.id,
|
||||
errors: result.message
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveUnreferencedLfsObjectsWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
class RemoveUnreferencedLfsObjectsWorker
|
||||
include ApplicationWorker
|
||||
|
||||
sidekiq_options retry: 3
|
||||
|
@ -10,8 +10,16 @@ class RemoveUnreferencedLfsObjectsWorker # rubocop:disable Scalability/Idempoten
|
|||
# rubocop:enable Scalability/CronWorkerContext
|
||||
|
||||
feature_category :git_lfs
|
||||
deduplicate :until_executed
|
||||
idempotent!
|
||||
|
||||
def perform
|
||||
LfsObject.destroy_unreferenced
|
||||
number_of_removed_files = 0
|
||||
|
||||
LfsObject.unreferenced_in_batches do |lfs_objects_without_projects|
|
||||
number_of_removed_files += lfs_objects_without_projects.destroy_all.count # rubocop: disable Cop/DestroyAll
|
||||
end
|
||||
|
||||
number_of_removed_files
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: managed_alerts_deprecation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62528
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331863
|
||||
milestone: '14.0'
|
||||
type: development
|
||||
group: group::monitor
|
||||
default_enabled: false
|
|
@ -208,6 +208,7 @@ formatters
|
|||
Fugit
|
||||
fuzzer
|
||||
Gantt
|
||||
Gemfile
|
||||
Gemnasium
|
||||
Gemojione
|
||||
Getter
|
||||
|
|
|
@ -305,6 +305,7 @@ Parameters:
|
|||
| `sha` | string | yes | The commit hash |
|
||||
| `branch` | string | yes | The name of the branch |
|
||||
| `dry_run` | boolean | no | Does not commit any changes. Default is false. [Introduced in GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/issues/231032) |
|
||||
| `message` | string | no | A custom commit message to use for the new commit. [Introduced in GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62481)
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "branch=master" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/cherry_pick"
|
||||
|
|
|
@ -11933,6 +11933,17 @@ Represents rules that commit pushes must follow.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="pushrulesrejectunsignedcommits"></a>`rejectUnsignedCommits` | [`Boolean!`](#boolean) | Indicates whether commits not signed through GPG will be rejected. |
|
||||
|
||||
### `PypiMetadata`
|
||||
|
||||
Pypi metadata.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="pypimetadataid"></a>`id` | [`PackagesPypiMetadatumID!`](#packagespypimetadatumid) | ID of the metadatum. |
|
||||
| <a id="pypimetadatarequiredpython"></a>`requiredPython` | [`String`](#string) | Required Python version of the Pypi package. |
|
||||
|
||||
### `RecentFailures`
|
||||
|
||||
Recent failure history of a test case.
|
||||
|
@ -15298,6 +15309,12 @@ A `PackagesPackageID` is a global ID. It is encoded as a string.
|
|||
|
||||
An example `PackagesPackageID` is: `"gid://gitlab/Packages::Package/1"`.
|
||||
|
||||
### `PackagesPypiMetadatumID`
|
||||
|
||||
A `PackagesPypiMetadatumID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `PackagesPypiMetadatumID` is: `"gid://gitlab/Packages::Pypi::Metadatum/1"`.
|
||||
|
||||
### `PathLockID`
|
||||
|
||||
A `PathLockID` is a global ID. It is encoded as a string.
|
||||
|
@ -15429,6 +15446,7 @@ One of:
|
|||
- [`ConanMetadata`](#conanmetadata)
|
||||
- [`MavenMetadata`](#mavenmetadata)
|
||||
- [`NugetMetadata`](#nugetmetadata)
|
||||
- [`PypiMetadata`](#pypimetadata)
|
||||
|
||||
#### `VulnerabilityDetail`
|
||||
|
||||
|
|
|
@ -63,11 +63,11 @@ Example of response
|
|||
"pipeline": {
|
||||
"id": 6,
|
||||
"project_id": 1,
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
|
||||
"status": "pending"
|
||||
},
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
"status": "failed",
|
||||
|
@ -117,11 +117,11 @@ Example of response
|
|||
"pipeline": {
|
||||
"id": 6,
|
||||
"project_id": 1,
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
|
||||
"status": "pending"
|
||||
},
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
|
@ -198,11 +198,11 @@ Example of response
|
|||
"pipeline": {
|
||||
"id": 6,
|
||||
"project_id": 1,
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
|
||||
"status": "pending"
|
||||
},
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
|
@ -263,11 +263,11 @@ Example of response
|
|||
"pipeline": {
|
||||
"id": 6,
|
||||
"project_id": 1,
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
|
||||
"status": "pending"
|
||||
},
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
"status": "failed",
|
||||
|
@ -348,14 +348,14 @@ Example of response
|
|||
"pipeline": {
|
||||
"id": 6,
|
||||
"project_id": 1,
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
|
||||
"status": "pending",
|
||||
"created_at": "2015-12-24T15:50:16.123Z",
|
||||
"updated_at": "2015-12-24T18:00:44.432Z",
|
||||
"web_url": "https://example.com/foo/bar/pipelines/6"
|
||||
},
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"stage": "test",
|
||||
"status": "pending",
|
||||
"tag": false,
|
||||
|
@ -380,7 +380,7 @@ Example of response
|
|||
"downstream_pipeline": {
|
||||
"id": 5,
|
||||
"sha": "f62a4b2fb89754372a346f24659212eb8da13601",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"status": "pending",
|
||||
"created_at": "2015-12-24T17:54:27.722Z",
|
||||
"updated_at": "2015-12-24T17:58:27.896Z",
|
||||
|
@ -433,11 +433,11 @@ Example of response
|
|||
"pipeline": {
|
||||
"id": 6,
|
||||
"project_id": 1,
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
|
||||
"status": "pending"
|
||||
},
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
|
@ -518,7 +518,7 @@ Example response:
|
|||
"id": 1,
|
||||
"project_id": 1,
|
||||
"sha": "b83d6e391c22777fca1ed3012fce84f633d7fed0",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"status": "pending",
|
||||
"created_at": "2021-03-26T14:51:51.107Z",
|
||||
"updated_at": "2021-03-26T14:51:51.107Z",
|
||||
|
@ -590,11 +590,11 @@ Example of response
|
|||
"pipeline": {
|
||||
"id": 6,
|
||||
"project_id": 1,
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
|
||||
"status": "pending"
|
||||
},
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
|
@ -684,7 +684,7 @@ Example of response
|
|||
"queued_duration": 0.010,
|
||||
"id": 42,
|
||||
"name": "rubocop",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
|
@ -734,7 +734,7 @@ Example of response
|
|||
"queued_duration": 0.010,
|
||||
"id": 42,
|
||||
"name": "rubocop",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
|
@ -784,7 +784,7 @@ Example of response
|
|||
"download_url": null,
|
||||
"id": 42,
|
||||
"name": "rubocop",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
|
@ -839,7 +839,7 @@ Example of response
|
|||
"queued_duration": 0.010,
|
||||
"id": 42,
|
||||
"name": "rubocop",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
|
|
|
@ -30,7 +30,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
|
|||
{
|
||||
"id": 13,
|
||||
"description": "Test schedule pipeline",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"cron": "* * * * *",
|
||||
"cron_timezone": "Asia/Tokyo",
|
||||
"next_run_at": "2017-05-19T13:41:00.000Z",
|
||||
|
@ -70,7 +70,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
|
|||
{
|
||||
"id": 13,
|
||||
"description": "Test schedule pipeline",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"cron": "* * * * *",
|
||||
"cron_timezone": "Asia/Tokyo",
|
||||
"next_run_at": "2017-05-19T13:41:00.000Z",
|
||||
|
@ -80,7 +80,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
|
|||
"last_pipeline": {
|
||||
"id": 332,
|
||||
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"status": "pending"
|
||||
},
|
||||
"owner": {
|
||||
|
@ -119,14 +119,14 @@ POST /projects/:id/pipeline_schedules
|
|||
| `active` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule is initially deactivated (default: `true`). |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form description="Build packages" --form ref="master" --form cron="0 1 * * 5" --form cron_timezone="UTC" --form active="true" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form description="Build packages" --form ref="main" --form cron="0 1 * * 5" --form cron_timezone="UTC" --form active="true" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 14,
|
||||
"description": "Build packages",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"cron": "0 1 * * 5",
|
||||
"cron_timezone": "UTC",
|
||||
"next_run_at": "2017-05-26T01:00:00.000Z",
|
||||
|
@ -171,7 +171,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form cron="0
|
|||
{
|
||||
"id": 13,
|
||||
"description": "Test schedule pipeline",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"cron": "0 2 * * *",
|
||||
"cron_timezone": "Asia/Tokyo",
|
||||
"next_run_at": "2017-05-19T17:00:00.000Z",
|
||||
|
@ -181,7 +181,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form cron="0
|
|||
"last_pipeline": {
|
||||
"id": 332,
|
||||
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"status": "pending"
|
||||
},
|
||||
"owner": {
|
||||
|
@ -216,7 +216,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
|
|||
{
|
||||
"id": 13,
|
||||
"description": "Test schedule pipeline",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"cron": "0 2 * * *",
|
||||
"cron_timezone": "Asia/Tokyo",
|
||||
"next_run_at": "2017-05-19T17:00:00.000Z",
|
||||
|
@ -226,7 +226,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
|
|||
"last_pipeline": {
|
||||
"id": 332,
|
||||
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"status": "pending"
|
||||
},
|
||||
"owner": {
|
||||
|
@ -261,7 +261,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
|
|||
{
|
||||
"id": 13,
|
||||
"description": "Test schedule pipeline",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"cron": "0 2 * * *",
|
||||
"cron_timezone": "Asia/Tokyo",
|
||||
"next_run_at": "2017-05-19T17:00:00.000Z",
|
||||
|
@ -271,7 +271,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
|
|||
"last_pipeline": {
|
||||
"id": 332,
|
||||
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"status": "pending"
|
||||
},
|
||||
"owner": {
|
||||
|
|
|
@ -99,7 +99,7 @@ Example of response
|
|||
"id": 46,
|
||||
"project_id": 1,
|
||||
"status": "success",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"tag": false,
|
||||
|
@ -226,7 +226,7 @@ POST /projects/:id/pipeline
|
|||
| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure `[{ 'key': 'UPLOAD_TO_S3', 'variable_type': 'file', 'value': 'true' }]` |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master"
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=main"
|
||||
```
|
||||
|
||||
Example of response
|
||||
|
@ -236,7 +236,7 @@ Example of response
|
|||
"id": 61,
|
||||
"project_id": 1,
|
||||
"sha": "384c444e840a515b23f21915ee5766b87068a70d",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"status": "pending",
|
||||
"before_sha": "0000000000000000000000000000000000000000",
|
||||
"tag": false,
|
||||
|
@ -285,7 +285,7 @@ Response:
|
|||
"id": 46,
|
||||
"project_id": 1,
|
||||
"status": "pending",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"tag": false,
|
||||
|
@ -334,7 +334,7 @@ Response:
|
|||
"id": 46,
|
||||
"project_id": 1,
|
||||
"status": "canceled",
|
||||
"ref": "master",
|
||||
"ref": "main",
|
||||
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"tag": false,
|
||||
|
|
|
@ -30,7 +30,7 @@ and allow us to have much better resiliency and much easier way to scale applica
|
|||
|
||||
The actual split was tested with the usage of [Rails Engines](https://guides.rubyonrails.org/engines.html)
|
||||
implementing separate gems in a single repository. The [Rails Engines](https://guides.rubyonrails.org/engines.html)
|
||||
allowed us to well describe the indivdual components with its dependencies and run an application
|
||||
allowed us to well describe the individual components with its dependencies and run an application
|
||||
consisting of many Rails Engines.
|
||||
|
||||
The blueprint aims to retain all key aspects of GitLab success: single and monolithic codebase (with a [single data-store](https://about.gitlab.com/handbook/product/single-application/#single-data-store)),
|
||||
|
@ -52,7 +52,7 @@ codebase without clear boundaries results in a number of problems and inefficien
|
|||
- The high memory usage results in slowing the whole application as it increases GC cycles duration
|
||||
creating significantly longer latency for processing requests or worse cache usage of CPUs
|
||||
- Increased application boot-up times as we need to load and parse significantly more files
|
||||
- Longer boot-up times slows down the development, as running application or tests takes singificantly longer
|
||||
- Longer boot-up times slows down the development, as running application or tests takes significantly longer
|
||||
reducing velocity and amount of iterations
|
||||
|
||||
## Composable codebase dimensions
|
||||
|
@ -123,8 +123,8 @@ application layers. This list is not exhaustive, but shows a general list of the
|
|||
- Services/Models/DB: all code required to maintain our database structure, data validation, business logic and policies models that needs to be shared with other components
|
||||
|
||||
The best way to likely describe how the actual GitLab Rails split would look like. It is a satellite model.
|
||||
Where we have a single core, that is shared across all satelitte components. The design of that implies
|
||||
that satelitte components have a limited way to communicate with each other. In a single monolithic application
|
||||
Where we have a single core, that is shared across all satellite components. The design of that implies
|
||||
that satellite components have a limited way to communicate with each other. In a single monolithic application
|
||||
in most of cases application would communicate with a code. In a satellite model the communication needs
|
||||
to be performed externally to the component. This can be via Database, Redis or using a well defined exposed API.
|
||||
|
||||
|
@ -293,7 +293,7 @@ This allowed us to run Web Nodes with all dependencies, but measure the impact o
|
|||
All work can be found in these merge requests:
|
||||
|
||||
- [Provide mechanism to load GraphQL with all dependencies only when needed](https://gitlab.com/gitlab-org/gitlab/-/issues/288044)
|
||||
- [Draft: PoC - Move Graphql to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
|
||||
- [Draft: PoC - Move GraphQL to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
|
||||
- [Draft: PoC - Move Controllers and Grape API:API to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53720)
|
||||
- [Draft: PoC - Move only Grape API:API to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53982)
|
||||
- [Measure performance impact for proposed web_engine](https://gitlab.com/gitlab-org/gitlab/-/issues/300548)
|
||||
|
@ -310,20 +310,20 @@ What was done?
|
|||
|
||||
#### Implementation details for proposed solution
|
||||
|
||||
1. Introduce new Rails Engine for each application layer.
|
||||
1. Introduce new Rails Engine for each application layer.
|
||||
|
||||
We created `engines` folder, which could contain different engines for each application layer we introduce in the future.
|
||||
|
||||
In the above PoCs we introuced the new Web Application Layer, located in `engines/web_engine` folder.
|
||||
In the above PoCs we introduced the new Web Application Layer, located in `engines/web_engine` folder.
|
||||
|
||||
1. Move all code and specs into `engines/web_engine/`
|
||||
|
||||
- We moved all Graphql code and specs into `engines/web_engine/` without changing files itself
|
||||
- We moved all GraphQL code and specs into `engines/web_engine/` without changing files itself
|
||||
- We moved all Grape API and Controllers code into `engines/web_engine/` without changing files itself
|
||||
|
||||
1. Move gems to the `engines/web_engine/`
|
||||
|
||||
- We moved all Graphql gems to the actual web_engine Gemfile
|
||||
- We moved all GraphQL gems to the actual web_engine Gemfile
|
||||
- We moved Grape API gem to the actual web_engine Gemfile
|
||||
|
||||
```ruby
|
||||
|
@ -331,7 +331,7 @@ What was done?
|
|||
spec.add_dependency 'apollo_upload_server'
|
||||
spec.add_dependency 'graphql'
|
||||
spec.add_dependency 'graphiql-rails'
|
||||
|
||||
|
||||
spec.add_dependency 'graphql-docs'
|
||||
spec.add_dependency 'grape'
|
||||
end
|
||||
|
@ -339,31 +339,31 @@ What was done?
|
|||
|
||||
1. Move routes to the `engines/web_engine/config/routes.rb` file
|
||||
|
||||
- We moved Graphql routes to the web_engine routes.
|
||||
- We moved GraphQL routes to the web_engine routes.
|
||||
- We moved API routes to the web_engine routes.
|
||||
- We moved most of the controller routes to the web_engine routes.
|
||||
|
||||
```ruby
|
||||
Rails.application.routes.draw do
|
||||
post '/api/graphql', to: 'graphql#execute'
|
||||
mount GraphiQL::Rails::Engine, at: '/-/graphql-explorer', graphql_path:
|
||||
mount GraphiQL::Rails::Engine, at: '/-/graphql-explorer', graphql_path:
|
||||
Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/api/graphql')
|
||||
|
||||
|
||||
draw :api
|
||||
|
||||
|
||||
#...
|
||||
end
|
||||
```
|
||||
|
||||
1. Move initializers to the `engines/web_engine/config/initializers` folder
|
||||
|
||||
- We moved graphql.rb initializer to the `web_engine` initializers folder
|
||||
- We moved grape_patch.rb and graphe_validators to the `web_engine` initializers folder
|
||||
- We moved `graphql.rb` initializer to the `web_engine` initializers folder
|
||||
- We moved `grape_patch.rb` and `graphe_validators` to the `web_engine` initializers folder
|
||||
|
||||
1. Connect GitLab application with the WebEngine
|
||||
|
||||
In GitLab Gemfile.rb, add web_engine to the engines group
|
||||
|
||||
|
||||
```ruby
|
||||
# Gemfile
|
||||
group :engines, :test do
|
||||
|
@ -376,7 +376,7 @@ What was done?
|
|||
1. Configure GitLab when to load the engine.
|
||||
|
||||
In GitLab `config/engines.rb`, we can configure when do we want to load our engines by relying on our `Gitlab::Runtime`
|
||||
|
||||
|
||||
```ruby
|
||||
# config/engines.rb
|
||||
# Load only in case we are running web_server or rails console
|
||||
|
@ -392,11 +392,11 @@ What was done?
|
|||
the application, performing tasks such as adding the app directory of the engine to
|
||||
the load path for models, mailers, controllers, and views.
|
||||
A file at `lib/web_engine/engine.rb`, is identical in function to a standard Rails
|
||||
application's `config/application.rb` file. This way engines can access a config
|
||||
application's `config/application.rb` file. This way engines can access a configuration
|
||||
object which contains configuration shared by all railties and the application.
|
||||
Additionally, each engine can access autoload_paths, eager_load_paths, and autoload_once_paths
|
||||
Additionally, each engine can access `autoload_paths`, `eager_load_paths`, and `autoload_once_paths`
|
||||
settings which are scoped to that engine.
|
||||
|
||||
|
||||
```ruby
|
||||
module WebEngine
|
||||
class Engine < ::Rails::Engine
|
||||
|
@ -404,7 +404,7 @@ What was done?
|
|||
#{config.root}/app/graphql/resolvers/concerns
|
||||
#{config.root}/app/graphql/mutations/concerns
|
||||
#{config.root}/app/graphql/types/concerns])
|
||||
|
||||
|
||||
if Gitlab.ee?
|
||||
ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
|
||||
ee_path = config.root
|
||||
|
@ -422,11 +422,11 @@ What was done?
|
|||
|
||||
We adapted CI to test `engines/web_engine/` as a self-sufficient component of stack.
|
||||
|
||||
- We moved spec as-is files to the `engines/web_engine/spec` folder
|
||||
- We moved ee/spec as-is files to the `engines/web_engine/ee/spec` folder
|
||||
- We moved `spec` as-is files to the `engines/web_engine/spec` folder
|
||||
- We moved `ee/spec` as-is files to the `engines/web_engine/ee/spec` folder
|
||||
- We control specs from main application using environment variable `TEST_WEB_ENGINE`
|
||||
- We added new CI job that will run `engines/web_engine/spec` tests separately using `TEST_WEB_ENGINE` env variable.
|
||||
- We added new CI job that will run `engines/web_engine/ee/spec` tests separately using `TEST_WEB_ENGINE` env variable.
|
||||
- We added new CI job that will run `engines/web_engine/spec` tests separately using `TEST_WEB_ENGINE` environment variable.
|
||||
- We added new CI job that will run `engines/web_engine/ee/spec` tests separately using `TEST_WEB_ENGINE` environment variable.
|
||||
- We are running all whitebox frontend tests with `TEST_WEB_ENGINE=true`
|
||||
|
||||
#### Results
|
||||
|
@ -482,12 +482,12 @@ Pros:
|
|||
|
||||
- Significantly lower memory usage
|
||||
- Significantly shorter application load time for Sidekiq
|
||||
- Significantly improved responsivness of Sidekiq service due to much shorter GC cycles
|
||||
- Significantly improved responsiveness of Sidekiq service due to much shorter GC cycles
|
||||
- Significantly easier testing of a portion of application, ex. changing `web_engines/` does require
|
||||
re-running test only for this application layer
|
||||
- We retained a monolithic architecture of the codebase, but sharing database and application models
|
||||
- A significant saving from the infrastracture side
|
||||
- Ability to comfortably run on constrainted environments by reducing application footprint
|
||||
- A significant saving from the infrastructure side
|
||||
- Ability to comfortably run on constrained environments by reducing application footprint
|
||||
|
||||
Cons:
|
||||
|
||||
|
@ -497,7 +497,7 @@ Cons:
|
|||
|
||||
#### Example: GraphQL
|
||||
|
||||
[Draft: PoC - Move Graphql to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
|
||||
[Draft: PoC - Move GraphQL to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
|
||||
|
||||
- The [99% of changes](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180/diffs?commit_id=49c9881c6696eb620dccac71532a3173f5702ea8) as visible in the above MRs is moving files as-is.
|
||||
- The [actual work](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180/diffs?commit_id=1d9a9edfa29ea6638e7d8a6712ddf09f5be77a44) on fixing cross-dependencies, specs, and configuring web_engine
|
||||
|
@ -506,8 +506,8 @@ Cons:
|
|||
Today, loading GraphQL requires a bunch of [dependencies](https://gitlab.com/gitlab-org/gitlab/-/issues/288044):
|
||||
|
||||
> We also discovered that we load/require 14480 files, [memory-team-2gb-week#9](https://gitlab.com/gitlab-org/memory-team/memory-team-2gb-week/-/issues/9#note_452530513)
|
||||
> when we start GitLab. 1274 files belong to Graphql. This means that if we don't load 1274 application files
|
||||
> and all related Graphql gems when we don't need them (Sidekiq), we could save a lot of memory.
|
||||
> when we start GitLab. 1274 files belong to GraphQL. This means that if we don't load 1274 application files
|
||||
> and all related GraphQL gems when we don't need them (Sidekiq), we could save a lot of memory.
|
||||
|
||||
GraphQL only needs to run in a specific context. If we could limit when it is being loaded we could effectively improve application efficiency, by reducing application load time and required memory. This, for example, is applicable for every size installation.
|
||||
|
||||
|
@ -524,9 +524,9 @@ An alternative way is to use a notification system that would always make an `Ac
|
|||
|
||||
Grape::API is another example that only needs to run only in a web server context.
|
||||
|
||||
Potential chalenges with Grape API:
|
||||
Potential challenges with Grape API:
|
||||
|
||||
- Currently there are some API::API dependencies in the models (e.g. `API::Helpers::Version` dependency in [project model](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/project.rb#L2019) or API::API dependency in GeoNode model for [geo_retrieve_url](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/geo_node.rb#L183))
|
||||
- Currently there are some API::API dependencies in the models (e.g. `API::Helpers::Version` dependency in [project model](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/project.rb#L2019) or API::API dependency in GeoNode model for [`geo_retrieve_url`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/geo_node.rb#L183))
|
||||
- `api_v4` paths are used in helpers, presenters, and views (e.g. `api_v4_projects_path` in [PackagesHelper](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/helpers/packages_helper.rb#L17))
|
||||
|
||||
#### Example: Controllers
|
||||
|
@ -538,7 +538,7 @@ Potential chalenges with Grape API:
|
|||
|
||||
Controllers, Serializers, some presenters and some of the Grape:Entities are also good examples that only need to be run in web server context.
|
||||
|
||||
Potential chalenges with moving Controllers:
|
||||
Potential challenges with moving Controllers:
|
||||
|
||||
- We needed to extend `Gitlab::Patch::DrawRoute` in order to support `engines/web_engine/config/routes` and `engines/web_engine/ee/config/routes` in case when `web_engine` is loaded. Here is potential [solution](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53720#note_506957398).
|
||||
- `Gitlab::Routing.url_helpers` paths are used in models and services, that could be used by Sidekiq (e.g. `Gitlab::Routing.url_helpers.project_pipelines_path` is used by [ExpirePipelineCacheService](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/ci/expire_pipeline_cache_service.rb#L20) in [ExpirePipelineCacheWorker](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/workers/expire_pipeline_cache_worker.rb#L18)))
|
||||
|
@ -552,7 +552,7 @@ Packwerk is currently accepting bug fixes only, and it is not being actively dev
|
|||
|
||||
**Application Layers** and this proposal currently defines only `web_engine`. Following the same pattern we could easily introduce
|
||||
additional engines dedicated for supporting that would allow us to maintain much better separation, lower memory usage
|
||||
and much better maintanability of GitLab Rails into the future.
|
||||
and much better maintainability of GitLab Rails into the future.
|
||||
|
||||
This would be a framework for introducing all new interfaces for features that do not need to be part of the core codebase,
|
||||
like support for additional Package services. Allowing us to better scale application in the future, but retaining a single codebase
|
||||
|
@ -564,7 +564,7 @@ As of today, it seems reasonable to define three **application layers**:
|
|||
- `gitlab-web`: a Controllers/API/GraphQL/ActionCable functionality needed to run in a web server context (depends on `gitlab-core`)
|
||||
- `gitlab-sidekiq`: a background jobs functionality needed to run Sidekiq Workers (depends on `gitlab-core`)
|
||||
|
||||
This model is best described today as a shared core with satellite. The shared core defines data access layer, where as satellites define a way to present and process this data. Satelites can only talk with Core. They cannot directly load or talk to another satelitte unless they use a well defined interface in form of API, GraphQL or Redis (as for scheduling Sidekiq jobs).
|
||||
This model is best described today as a shared core with satellite. The shared core defines data access layer, where as satellites define a way to present and process this data. Satellites can only talk with Core. They cannot directly load or talk to another satellite unless they use a well defined interface in form of API, GraphQL or Redis (as for scheduling Sidekiq jobs).
|
||||
|
||||
It is reasonable to assume that we limit how many `engines` we allow. Initial proposal is to allow up to 5 engines
|
||||
to be created to ensure that we do not have explosion of engines.
|
||||
|
@ -573,7 +573,7 @@ to be created to ensure that we do not have explosion of engines.
|
|||
|
||||
- [Split application into functional parts to ensure that only needed code is loaded with all dependencies](https://gitlab.com/gitlab-org/gitlab/-/issues/290935)
|
||||
- [Provide mechanism to load GraphQL with all dependencies only when needed](https://gitlab.com/gitlab-org/gitlab/-/issues/288044)
|
||||
- [Draft: PoC - Move Graphql to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
|
||||
- [Draft: PoC - Move GraphQL to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
|
||||
- [Draft: PoC - Move Controllers and Grape API:API to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53720)
|
||||
- [Draft: PoC - Move only Grape API:API to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53982)
|
||||
- [Measure performance impact for proposed web_engine](https://gitlab.com/gitlab-org/gitlab/-/issues/300548)
|
||||
|
|
|
@ -78,7 +78,7 @@ git init
|
|||
git remote add origin git@gitlab.example.com:<USERNAME>/laravel-sample.git
|
||||
git add .
|
||||
git commit -m 'Initial Commit'
|
||||
git push -u origin master
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
## Configure the production server
|
||||
|
@ -260,7 +260,7 @@ Let's create these three tasks one by one.
|
|||
|
||||
#### Clone the repository
|
||||
|
||||
The first task will create the `releases` directory (if it doesn't exist), and then clone the `master` branch of the repository (by default) into the new release directory, given by the `$new_release_dir` variable.
|
||||
The first task will create the `releases` directory (if it doesn't exist), and then clone the `main` branch of the repository (by default) into the new release directory, given by the `$new_release_dir` variable.
|
||||
The `releases` directory will hold all our deployments:
|
||||
|
||||
```php
|
||||
|
@ -378,14 +378,14 @@ These are persistent data and will be shared to every new release.
|
|||
|
||||
Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../environments/index.md), which will be described [later](#setting-up-gitlab-cicd) in this tutorial.
|
||||
|
||||
Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `master` branch.
|
||||
To keep things simple, we commit directly to `master`, without using [feature-branches](../../../topics/gitlab_flow.md#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial.
|
||||
Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `main` branch.
|
||||
To keep things simple, we commit directly to `main`, without using [feature-branches](../../../topics/gitlab_flow.md#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial.
|
||||
In a real world project, teams may use [Issue Tracker](../../../user/project/issues/index.md) and [Merge Requests](../../../user/project/merge_requests/index.md) to move their code across branches:
|
||||
|
||||
```shell
|
||||
git add Envoy.blade.php
|
||||
git commit -m 'Add Envoy'
|
||||
git push origin master
|
||||
git push origin main
|
||||
```
|
||||
|
||||
## Continuous Integration with GitLab
|
||||
|
@ -474,7 +474,7 @@ Let's commit the `Dockerfile` file.
|
|||
```shell
|
||||
git add Dockerfile
|
||||
git commit -m 'Add Dockerfile'
|
||||
git push origin master
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### Setting up GitLab CI/CD
|
||||
|
@ -523,7 +523,7 @@ deploy_production:
|
|||
url: http://192.168.1.1
|
||||
when: manual
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
```
|
||||
|
||||
That's a lot to take in, isn't it? Let's run through it step by step.
|
||||
|
@ -595,7 +595,7 @@ If the SSH keys have added successfully, we can run Envoy.
|
|||
As mentioned before, GitLab supports [Continuous Delivery](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) methods as well.
|
||||
The [environment](../../yaml/README.md#environment) keyword tells GitLab that this job deploys to the `production` environment.
|
||||
The `url` keyword is used to generate a link to our application on the GitLab Environments page.
|
||||
The `only` keyword tells GitLab CI/CD that the job should be executed only when the pipeline is building the `master` branch.
|
||||
The `only` keyword tells GitLab CI/CD that the job should be executed only when the pipeline is building the `main` branch.
|
||||
Lastly, `when: manual` is used to turn the job from running automatically to a manual action.
|
||||
|
||||
```yaml
|
||||
|
@ -616,7 +616,7 @@ deploy_production:
|
|||
url: http://192.168.1.1
|
||||
when: manual
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
```
|
||||
|
||||
You may also want to add another job for [staging environment](https://about.gitlab.com/blog/2021/02/05/ci-deployment-and-environments/), to final test your application before deploying to production.
|
||||
|
@ -624,7 +624,7 @@ You may also want to add another job for [staging environment](https://about.git
|
|||
### Turn on GitLab CI/CD
|
||||
|
||||
We have prepared everything we need to test and deploy our app with GitLab CI/CD.
|
||||
To do that, commit and push `.gitlab-ci.yml` to the `master` branch. It will trigger a pipeline, which you can watch live under your project's **Pipelines**.
|
||||
To do that, commit and push `.gitlab-ci.yml` to the `main` branch. It will trigger a pipeline, which you can watch live under your project's **Pipelines**.
|
||||
|
||||
![pipelines page](img/pipelines_page.png)
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ job1:
|
|||
- echo This rule uses parentheses.
|
||||
only:
|
||||
variables:
|
||||
- ($CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "develop") && $MY_VARIABLE
|
||||
- ($CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop") && $MY_VARIABLE
|
||||
```
|
||||
|
||||
### `only:changes` / `except:changes` examples
|
||||
|
|
|
@ -70,7 +70,7 @@ build:
|
|||
stage: build
|
||||
script: ./build
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
|
||||
test:
|
||||
stage: test
|
||||
|
@ -82,7 +82,7 @@ deploy:
|
|||
stage: deploy
|
||||
script: ./deploy
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
```
|
||||
|
||||
#### Excluding certain jobs
|
||||
|
@ -103,7 +103,7 @@ To achieve this, you can configure your `.gitlab-ci.yml` file as follows:
|
|||
``` yaml
|
||||
.only-default: &only-default
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
- merge_requests
|
||||
- tags
|
||||
|
||||
|
|
|
@ -209,7 +209,7 @@ jobs:
|
|||
deploy:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
- /rc-.*/
|
||||
```
|
||||
|
||||
|
@ -221,7 +221,7 @@ deploy_prod:
|
|||
script:
|
||||
- echo "Deploy to production server"
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
|
|
@ -318,7 +318,7 @@ Markdown code embeds the test coverage report badge of the `coverage` job
|
|||
into your `README.md`:
|
||||
|
||||
```markdown
|
||||
![coverage](https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=coverage)
|
||||
![coverage](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=coverage)
|
||||
```
|
||||
|
||||
### Badge styles
|
||||
|
@ -331,7 +331,7 @@ Pipeline badges can be rendered in different styles by adding the `style=style_n
|
|||
https://gitlab.example.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat
|
||||
```
|
||||
|
||||
![Badge flat style](https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=coverage&style=flat)
|
||||
![Badge flat style](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=coverage&style=flat)
|
||||
|
||||
- Flat square ([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30120) in GitLab 11.8):
|
||||
|
||||
|
@ -339,7 +339,7 @@ Pipeline badges can be rendered in different styles by adding the `style=style_n
|
|||
https://gitlab.example.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat-square
|
||||
```
|
||||
|
||||
![Badge flat square style](https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=coverage&style=flat-square)
|
||||
![Badge flat square style](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=coverage&style=flat-square)
|
||||
|
||||
### Custom badge text
|
||||
|
||||
|
@ -348,10 +348,10 @@ Pipeline badges can be rendered in different styles by adding the `style=style_n
|
|||
The text for a badge can be customized to differentiate between multiple coverage jobs that run in the same pipeline. Customize the badge text and width by adding the `key_text=custom_text` and `key_width=custom_key_width` parameters to the URL:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=karma&key_text=Frontend+Coverage&key_width=130
|
||||
https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=karma&key_text=Frontend+Coverage&key_width=130
|
||||
```
|
||||
|
||||
![Badge with custom text and width](https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=karma&key_text=Frontend+Coverage&key_width=130)
|
||||
![Badge with custom text and width](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=karma&key_text=Frontend+Coverage&key_width=130)
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ and it creates a dependent pipeline relation visible on the
|
|||
trigger_pipeline:
|
||||
stage: deploy
|
||||
script:
|
||||
- curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=master "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
|
||||
- curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=main "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
|
||||
only:
|
||||
- tags
|
||||
```
|
||||
|
@ -81,7 +81,7 @@ build_submodule:
|
|||
stage: test
|
||||
script:
|
||||
- apt update && apt install -y unzip
|
||||
- curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test&job_token=$CI_JOB_TOKEN"
|
||||
- curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/main/download?job=test&job_token=$CI_JOB_TOKEN"
|
||||
- unzip artifacts.zip
|
||||
only:
|
||||
- tags
|
||||
|
@ -178,7 +178,7 @@ To trigger a job from a webhook of another project you need to add the following
|
|||
webhook URL for Push and Tag events (change the project ID, ref and token):
|
||||
|
||||
```plaintext
|
||||
https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN
|
||||
https://gitlab.example.com/api/v4/projects/9/ref/main/trigger/pipeline?token=TOKEN
|
||||
```
|
||||
|
||||
You should pass `ref` as part of the URL, to take precedence over `ref` from
|
||||
|
@ -250,7 +250,7 @@ and the script of the `upload_package` job is run:
|
|||
```shell
|
||||
curl --request POST \
|
||||
--form token=TOKEN \
|
||||
--form ref=master \
|
||||
--form ref=main \
|
||||
--form "variables[UPLOAD_TO_S3]=true" \
|
||||
"https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
|
||||
```
|
||||
|
|
|
@ -434,7 +434,7 @@ Example job log output:
|
|||
export CI_JOB_ID="50"
|
||||
export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"
|
||||
export CI_COMMIT_SHORT_SHA="1ecfd275"
|
||||
export CI_COMMIT_REF_NAME="master"
|
||||
export CI_COMMIT_REF_NAME="main"
|
||||
export CI_REPOSITORY_URL="https://gitlab-ci-token:[masked]@example.com/gitlab-org/gitlab-foss.git"
|
||||
export CI_COMMIT_TAG="1.0.0"
|
||||
export CI_JOB_NAME="spec:other"
|
||||
|
@ -815,7 +815,7 @@ as well as from in a variable:
|
|||
|
||||
```yaml
|
||||
variables:
|
||||
MYSTRING: 'master'
|
||||
MYSTRING: 'main'
|
||||
MYREGEX: '/^mast.*/'
|
||||
|
||||
testdirect:
|
||||
|
@ -943,8 +943,8 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
|
|||
++ CI_PROJECT_VISIBILITY=public
|
||||
++ export CI_PROJECT_REPOSITORY_LANGUAGES=
|
||||
++ CI_PROJECT_REPOSITORY_LANGUAGES=
|
||||
++ export CI_DEFAULT_BRANCH=master
|
||||
++ CI_DEFAULT_BRANCH=master
|
||||
++ export CI_DEFAULT_BRANCH=main
|
||||
++ CI_DEFAULT_BRANCH=main
|
||||
++ export CI_REGISTRY=registry.gitlab.com
|
||||
++ CI_REGISTRY=registry.gitlab.com
|
||||
++ export CI_API_V4_URL=https://gitlab.com/api/v4
|
||||
|
@ -961,10 +961,10 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
|
|||
++ CI_COMMIT_SHORT_SHA=dd648b2e
|
||||
++ export CI_COMMIT_BEFORE_SHA=0000000000000000000000000000000000000000
|
||||
++ CI_COMMIT_BEFORE_SHA=0000000000000000000000000000000000000000
|
||||
++ export CI_COMMIT_REF_NAME=master
|
||||
++ CI_COMMIT_REF_NAME=master
|
||||
++ export CI_COMMIT_REF_SLUG=master
|
||||
++ CI_COMMIT_REF_SLUG=master
|
||||
++ export CI_COMMIT_REF_NAME=main
|
||||
++ CI_COMMIT_REF_NAME=main
|
||||
++ export CI_COMMIT_REF_SLUG=main
|
||||
++ CI_COMMIT_REF_SLUG=main
|
||||
...
|
||||
```
|
||||
|
||||
|
|
|
@ -1029,7 +1029,7 @@ levels. For example:
|
|||
URL: "http://my-url.internal"
|
||||
IMPORTANT_VAR: "the details"
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
- stable
|
||||
tags:
|
||||
- production
|
||||
|
@ -1062,7 +1062,7 @@ rspec:
|
|||
IMPORTANT_VAR: "the details"
|
||||
GITLAB: "is-awesome"
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
- stable
|
||||
tags:
|
||||
- docker
|
||||
|
@ -2390,7 +2390,7 @@ For example:
|
|||
```yaml
|
||||
deploy to production:
|
||||
stage: deploy
|
||||
script: git push production HEAD:master
|
||||
script: git push production HEAD:main
|
||||
environment: production
|
||||
```
|
||||
|
||||
|
@ -2412,7 +2412,7 @@ Set a name for an [environment](../environments/index.md). For example:
|
|||
```yaml
|
||||
deploy to production:
|
||||
stage: deploy
|
||||
script: git push production HEAD:master
|
||||
script: git push production HEAD:main
|
||||
environment:
|
||||
name: production
|
||||
```
|
||||
|
@ -2447,7 +2447,7 @@ Set a URL for an [environment](../environments/index.md). For example:
|
|||
```yaml
|
||||
deploy to production:
|
||||
stage: deploy
|
||||
script: git push production HEAD:master
|
||||
script: git push production HEAD:main
|
||||
environment:
|
||||
name: production
|
||||
url: https://prod.example.com
|
||||
|
|
|
@ -26,7 +26,7 @@ Array with `include` method implied:
|
|||
|
||||
```yaml
|
||||
include:
|
||||
- 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
|
||||
- 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
|
||||
- '/templates/.after-script-template.yml'
|
||||
```
|
||||
|
||||
|
@ -34,21 +34,21 @@ Single string with `include` method specified explicitly:
|
|||
|
||||
```yaml
|
||||
include:
|
||||
remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
|
||||
remote: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
|
||||
```
|
||||
|
||||
Array with `include:remote` being the single item:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
|
||||
- remote: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
|
||||
```
|
||||
|
||||
Array with multiple `include` methods specified explicitly:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
|
||||
- remote: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
|
||||
- local: '/templates/.after-script-template.yml'
|
||||
- template: Auto-DevOps.gitlab-ci.yml
|
||||
```
|
||||
|
@ -57,11 +57,11 @@ Array mixed syntax:
|
|||
|
||||
```yaml
|
||||
include:
|
||||
- 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
|
||||
- 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
|
||||
- '/templates/.after-script-template.yml'
|
||||
- template: Auto-DevOps.gitlab-ci.yml
|
||||
- project: 'my-group/my-project'
|
||||
ref: master
|
||||
ref: main
|
||||
file: '/templates/.gitlab-ci-template.yml'
|
||||
```
|
||||
|
||||
|
@ -70,7 +70,7 @@ include:
|
|||
In the following example, the content of `.before-script-template.yml` is
|
||||
automatically fetched and evaluated along with the content of `.gitlab-ci.yml`.
|
||||
|
||||
Content of `https://gitlab.com/awesome-project/raw/master/.before-script-template.yml`:
|
||||
Content of `https://gitlab.com/awesome-project/raw/main/.before-script-template.yml`:
|
||||
|
||||
```yaml
|
||||
default:
|
||||
|
@ -83,7 +83,7 @@ default:
|
|||
Content of `.gitlab-ci.yml`:
|
||||
|
||||
```yaml
|
||||
include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
|
||||
include: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
|
||||
|
||||
rspec:
|
||||
script:
|
||||
|
@ -112,7 +112,7 @@ production:
|
|||
name: production
|
||||
url: https://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
|
||||
only:
|
||||
- master
|
||||
- main
|
||||
```
|
||||
|
||||
Content of `.gitlab-ci.yml`:
|
||||
|
|
|
@ -16778,15 +16778,15 @@ Tiers: `free`
|
|||
|
||||
### `usage_activity_by_stage.manage.value_stream_management_customized_group_stages`
|
||||
|
||||
Missing description
|
||||
Number of custom value stream analytics stages.
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_all/20210216180803_value_stream_management_customized_group_stages.yml)
|
||||
|
||||
Group: `group::manage`
|
||||
Group: `group::optimize`
|
||||
|
||||
Status: `data_available`
|
||||
|
||||
Tiers:
|
||||
Tiers: `premium`, `ultimate`
|
||||
|
||||
### `usage_activity_by_stage.monitor.clusters`
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ documentation for all current settings and limits on the GitLab.com instance.
|
|||
|
||||
## General
|
||||
|
||||
Access the default page for admin area settings by navigating to **Admin Area > Settings > General**:
|
||||
Access the default page for Admin Area settings by navigating to **Admin Area > Settings > General**:
|
||||
|
||||
| Option | Description |
|
||||
| ------ | ----------- |
|
||||
|
@ -27,7 +27,7 @@ Access the default page for admin area settings by navigating to **Admin Area >
|
|||
| [Terms of Service and Privacy Policy](terms.md) | Include a Terms of Service agreement and Privacy Policy that all users must accept. |
|
||||
| [External Authentication](external_authorization.md#configuration) | External Classification Policy Authorization |
|
||||
| [Web terminal](../../../administration/integration/terminal.md#limiting-websocket-connection-time) | Set max session time for web terminal. |
|
||||
| [Web IDE](../../project/web_ide/index.md#enabling-live-preview) | Manage Web IDE Features. |
|
||||
| [Web IDE](../../project/web_ide/index.md#enable-live-preview) | Manage Web IDE features. |
|
||||
| [FLoC](floc.md) | Enable or disable [Federated Learning of Cohorts (FLoC)](https://en.wikipedia.org/wiki/Federated_Learning_of_Cohorts) tracking. |
|
||||
|
||||
## Integrations
|
||||
|
|
|
@ -100,6 +100,11 @@ To disable it:
|
|||
Feature.disable(:pick_into_project)
|
||||
```
|
||||
|
||||
## Related links
|
||||
|
||||
- The [Commits API](../../../api/commits.md) enables you to add custom messages
|
||||
to changes you cherry-pick through the API.
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
@ -259,19 +259,27 @@ Additionally, for public projects an **Open in CodeSandbox** button is available
|
|||
to transfer the contents of the project into a public CodeSandbox project to
|
||||
quickly share your project with others.
|
||||
|
||||
### Enabling Live Preview
|
||||
### Enable Live Preview
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/268288) in GitLab 12.9, third-party assets and libraries required for Live Preview are hosted at `https://sandbox-prod.gitlab-static.net` when it is enabled. However, some libraries are still served from other third-party services which may or may not be desirable in your environment.
|
||||
With Live Preview enabled, you can preview projects with a `package.json` file and
|
||||
a `main` entry point inside the Web IDE.
|
||||
|
||||
The Live Preview feature needs to be enabled in the GitLab instance's
|
||||
Admin Area. Live Preview is enabled for all projects on
|
||||
GitLab.com
|
||||
Live Preview is enabled for all projects on GitLab.com. If you are an administrator
|
||||
of a self-managed GitLab instance, and you want to enable Live Preview:
|
||||
|
||||
![Administrator Live Preview setting](img/admin_live_preview_v13_0.png)
|
||||
1. In the top navigation bar, go to **Admin Area**.
|
||||
1. In the left sidebar, select **Settings > General**.
|
||||
1. Scroll to **Web IDE** and select **Expand**:
|
||||
![Administrator Live Preview setting](img/admin_live_preview_v13_0.png)
|
||||
1. Select **Enable Live Preview** and select **Save changes**.
|
||||
|
||||
After you have done that, you can preview projects with a `package.json` file and
|
||||
a `main` entry point inside the Web IDE. An example `package.json` is shown
|
||||
below.
|
||||
[In GitLab 12.9 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/268288),
|
||||
third-party assets and libraries required for Live Preview are hosted at
|
||||
`https://sandbox-prod.gitlab-static.net` when it is enabled. However, some libraries
|
||||
are still served from other third-party services, which may or may not be desirable
|
||||
in your environment.
|
||||
|
||||
An example `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
@ -203,6 +203,7 @@ module API
|
|||
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked'
|
||||
requires :branch, type: String, desc: 'The name of the branch', allow_blank: false
|
||||
optional :dry_run, type: Boolean, default: false, desc: "Does not commit any changes"
|
||||
optional :message, type: String, desc: 'A custom commit message to use for the picked commit'
|
||||
end
|
||||
post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
|
||||
authorize_push_to_branch!(params[:branch])
|
||||
|
@ -216,7 +217,8 @@ module API
|
|||
commit: commit,
|
||||
start_branch: params[:branch],
|
||||
branch_name: params[:branch],
|
||||
dry_run: params[:dry_run]
|
||||
dry_run: params[:dry_run],
|
||||
message: params[:message]
|
||||
}
|
||||
|
||||
result = ::Commits::CherryPickService
|
||||
|
|
|
@ -136,7 +136,7 @@ module API
|
|||
.preload(project_group_links: { group: :route },
|
||||
fork_network: :root_project,
|
||||
fork_network_member: :forked_from_project,
|
||||
forked_from_project: [:route, :forks, :tags, :group, :project_feature, namespace: [:route, :owner]])
|
||||
forked_from_project: [:route, :tags, :group, :project_feature, namespace: [:route, :owner]])
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
@ -92,9 +92,9 @@ namespace :gitlab do
|
|||
task orphan_lfs_files: :gitlab_environment do
|
||||
warn_user_is_not_gitlab
|
||||
|
||||
removed_files = RemoveUnreferencedLfsObjectsWorker.new.perform
|
||||
number_of_removed_files = RemoveUnreferencedLfsObjectsWorker.new.perform
|
||||
|
||||
logger.info "Removed unreferenced LFS files: #{removed_files.count}".color(:green)
|
||||
logger.info "Removed unreferenced LFS files: #{number_of_removed_files}".color(:green)
|
||||
end
|
||||
|
||||
namespace :sessions do
|
||||
|
|
|
@ -13157,7 +13157,9 @@ msgid "Every two weeks"
|
|||
msgstr ""
|
||||
|
||||
msgid "Every week"
|
||||
msgstr ""
|
||||
msgid_plural "Every %d weeks"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Every week (%{weekday} at %{time})"
|
||||
msgstr ""
|
||||
|
@ -16533,9 +16535,6 @@ msgstr ""
|
|||
msgid "IDE"
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Back"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18536,6 +18535,9 @@ msgstr ""
|
|||
msgid "Iterations|No iteration cadences to show."
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|No iterations in cadence."
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Number of future iterations you would like to have scheduled"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20034,7 +20036,7 @@ msgstr ""
|
|||
msgid "Manage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage Web IDE features"
|
||||
msgid "Manage Web IDE features."
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage access"
|
||||
|
@ -24936,6 +24938,9 @@ msgstr ""
|
|||
msgid "Preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preview JavaScript projects in the Web IDE with CodeSandbox Live Preview. %{link_start}Learn more.%{link_end} "
|
||||
msgstr ""
|
||||
|
||||
msgid "Preview Markdown"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -55,4 +55,28 @@ RSpec.describe 'Alert Management index', :js do
|
|||
it_behaves_like 'alert page with title, filtered search, and table'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'managed_alerts_deprecation feature flag' do
|
||||
subject { page }
|
||||
|
||||
before do
|
||||
stub_feature_flags(managed_alerts_deprecation: feature_flag_value)
|
||||
sign_in(developer)
|
||||
|
||||
visit project_alert_management_index_path(project)
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
context 'feature flag on' do
|
||||
let(:feature_flag_value) { true }
|
||||
|
||||
it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: true) }
|
||||
end
|
||||
|
||||
context 'feature flag off' do
|
||||
let(:feature_flag_value) { false }
|
||||
|
||||
it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
{ "$ref": "./package_conan_metadata.json" },
|
||||
{ "$ref": "./package_maven_metadata.json" },
|
||||
{ "$ref": "./package_nuget_metadata.json" },
|
||||
{ "$ref": "./package_pypi_metadata.json" },
|
||||
{ "type": "null" }
|
||||
]
|
||||
},
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"requiredPython": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,12 +13,10 @@ describe('IssuesListApp component', () => {
|
|||
dueDate: '2020-12-17',
|
||||
startDate: '2020-12-10',
|
||||
title: 'My milestone',
|
||||
webUrl: '/milestone/webUrl',
|
||||
webPath: '/milestone/webPath',
|
||||
},
|
||||
dueDate: '2020-12-12',
|
||||
timeStats: {
|
||||
humanTimeEstimate: '1w',
|
||||
},
|
||||
humanTimeEstimate: '1w',
|
||||
};
|
||||
|
||||
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
|
||||
|
@ -56,7 +54,7 @@ describe('IssuesListApp component', () => {
|
|||
|
||||
expect(milestone.text()).toBe(issue.milestone.title);
|
||||
expect(milestone.find(GlIcon).props('name')).toBe('clock');
|
||||
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webUrl);
|
||||
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath);
|
||||
});
|
||||
|
||||
describe.each`
|
||||
|
@ -102,7 +100,7 @@ describe('IssuesListApp component', () => {
|
|||
|
||||
const timeEstimate = wrapper.find('[data-testid="time-estimate"]');
|
||||
|
||||
expect(timeEstimate.text()).toBe(issue.timeStats.humanTimeEstimate);
|
||||
expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
|
||||
expect(timeEstimate.attributes('title')).toBe('Estimate');
|
||||
expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
|
||||
});
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
|
||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { apiParams, filteredTokens, locationSearch, urlParams } from 'jest/issues_list/mock_data';
|
||||
import {
|
||||
filteredTokens,
|
||||
getIssuesQueryResponse,
|
||||
locationSearch,
|
||||
urlParams,
|
||||
} from 'jest/issues_list/mock_data';
|
||||
import createFlash from '~/flash';
|
||||
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
||||
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
|
||||
|
@ -11,13 +20,9 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
|
|||
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
|
||||
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
|
||||
import {
|
||||
apiSortParams,
|
||||
CREATED_DESC,
|
||||
DUE_DATE_OVERDUE,
|
||||
PAGE_SIZE,
|
||||
PAGE_SIZE_MANUAL,
|
||||
PARAM_DUE_DATE,
|
||||
RELATIVE_POSITION_DESC,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_AUTHOR,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
|
@ -40,12 +45,14 @@ describe('IssuesListApp component', () => {
|
|||
let axiosMock;
|
||||
let wrapper;
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
const defaultProvide = {
|
||||
autocompleteUsersPath: 'autocomplete/users/path',
|
||||
calendarPath: 'calendar/path',
|
||||
canBulkUpdate: false,
|
||||
emptyStateSvgPath: 'empty-state.svg',
|
||||
endpoint: 'api/endpoint',
|
||||
exportCsvPath: 'export/csv/path',
|
||||
hasBlockedIssuesFeature: true,
|
||||
hasIssueWeightsFeature: true,
|
||||
|
@ -61,22 +68,6 @@ describe('IssuesListApp component', () => {
|
|||
signInPath: 'sign/in/path',
|
||||
};
|
||||
|
||||
const state = 'opened';
|
||||
const xPage = 1;
|
||||
const xTotal = 25;
|
||||
const tabCounts = {
|
||||
opened: xTotal,
|
||||
closed: undefined,
|
||||
all: undefined,
|
||||
};
|
||||
const fetchIssuesResponse = {
|
||||
data: [],
|
||||
headers: {
|
||||
'x-page': xPage,
|
||||
'x-total': xTotal,
|
||||
},
|
||||
};
|
||||
|
||||
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
|
||||
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
|
||||
const findGlButton = () => wrapper.findComponent(GlButton);
|
||||
|
@ -86,19 +77,26 @@ describe('IssuesListApp component', () => {
|
|||
const findGlLink = () => wrapper.findComponent(GlLink);
|
||||
const findIssuableList = () => wrapper.findComponent(IssuableList);
|
||||
|
||||
const mountComponent = ({ provide = {}, mountFn = shallowMount } = {}) =>
|
||||
mountFn(IssuesListApp, {
|
||||
const mountComponent = ({
|
||||
provide = {},
|
||||
response = getIssuesQueryResponse,
|
||||
mountFn = shallowMount,
|
||||
} = {}) => {
|
||||
const requestHandlers = [[getIssuesQuery, jest.fn().mockResolvedValue(response)]];
|
||||
const apolloProvider = createMockApollo(requestHandlers);
|
||||
|
||||
return mountFn(IssuesListApp, {
|
||||
localVue,
|
||||
apolloProvider,
|
||||
provide: {
|
||||
...defaultProvide,
|
||||
...provide,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new AxiosMockAdapter(axios);
|
||||
axiosMock
|
||||
.onGet(defaultProvide.endpoint)
|
||||
.reply(200, fetchIssuesResponse.data, fetchIssuesResponse.headers);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -108,28 +106,37 @@ describe('IssuesListApp component', () => {
|
|||
});
|
||||
|
||||
describe('IssuableList', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent();
|
||||
await waitForPromises();
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(findIssuableList().props()).toMatchObject({
|
||||
namespace: defaultProvide.projectPath,
|
||||
recentSearchesStorageKey: 'issues',
|
||||
searchInputPlaceholder: 'Search or filter results…',
|
||||
searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
|
||||
sortOptions: getSortOptions(true, true),
|
||||
initialSortBy: CREATED_DESC,
|
||||
issuables: getIssuesQueryResponse.data.project.issues.nodes,
|
||||
tabs: IssuableListTabs,
|
||||
currentTab: IssuableStates.Opened,
|
||||
tabCounts,
|
||||
showPaginationControls: false,
|
||||
issuables: [],
|
||||
totalItems: xTotal,
|
||||
currentPage: xPage,
|
||||
previousPage: xPage - 1,
|
||||
nextPage: xPage + 1,
|
||||
urlParams: { page: xPage, state },
|
||||
tabCounts: {
|
||||
opened: 1,
|
||||
closed: undefined,
|
||||
all: undefined,
|
||||
},
|
||||
issuablesLoading: false,
|
||||
isManualOrdering: false,
|
||||
showBulkEditSidebar: false,
|
||||
showPaginationControls: true,
|
||||
currentPage: 1,
|
||||
previousPage: Number(getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage),
|
||||
nextPage: Number(getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage),
|
||||
urlParams: {
|
||||
sort: urlSortParams[CREATED_DESC],
|
||||
state: IssuableStates.Opened,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -157,9 +164,9 @@ describe('IssuesListApp component', () => {
|
|||
|
||||
describe('csv import/export component', () => {
|
||||
describe('when user is signed in', () => {
|
||||
it('renders', async () => {
|
||||
const search = '?page=1&search=refactor&state=opened&sort=created_date';
|
||||
const search = '?search=refactor&sort=created_date&state=opened';
|
||||
|
||||
beforeEach(() => {
|
||||
global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` });
|
||||
|
||||
wrapper = mountComponent({
|
||||
|
@ -167,11 +174,13 @@ describe('IssuesListApp component', () => {
|
|||
mountFn: mount,
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(findCsvImportExportButtons().props()).toMatchObject({
|
||||
exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
|
||||
issuableCount: xTotal,
|
||||
issuableCount: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -189,7 +198,7 @@ describe('IssuesListApp component', () => {
|
|||
it('renders when user has permissions', () => {
|
||||
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
|
||||
|
||||
expect(findGlButtonAt(2).text()).toBe('Edit issues');
|
||||
expect(findGlButtonAt(2).text()).toBe(IssuesListApp.i18n.editIssues);
|
||||
});
|
||||
|
||||
it('does not render when user does not have permissions', () => {
|
||||
|
@ -215,7 +224,7 @@ describe('IssuesListApp component', () => {
|
|||
it('renders when user has permissions', () => {
|
||||
wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount });
|
||||
|
||||
expect(findGlButtonAt(2).text()).toBe('New issue');
|
||||
expect(findGlButtonAt(2).text()).toBe(IssuesListApp.i18n.newIssueLabel);
|
||||
expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath);
|
||||
});
|
||||
|
||||
|
@ -238,18 +247,6 @@ describe('IssuesListApp component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('page', () => {
|
||||
it('is set from the url params', () => {
|
||||
const page = 5;
|
||||
|
||||
global.jsdom.reconfigure({ url: setUrlParams({ page }, TEST_HOST) });
|
||||
|
||||
wrapper = mountComponent();
|
||||
|
||||
expect(findIssuableList().props('currentPage')).toBe(page);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('is set from the url params', () => {
|
||||
global.jsdom.reconfigure({ url: `${TEST_HOST}${locationSearch}` });
|
||||
|
@ -262,13 +259,15 @@ describe('IssuesListApp component', () => {
|
|||
|
||||
describe('sort', () => {
|
||||
it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => {
|
||||
global.jsdom.reconfigure({ url: setUrlParams(urlSortParams[sortKey], TEST_HOST) });
|
||||
global.jsdom.reconfigure({
|
||||
url: setUrlParams({ sort: urlSortParams[sortKey] }, TEST_HOST),
|
||||
});
|
||||
|
||||
wrapper = mountComponent();
|
||||
|
||||
expect(findIssuableList().props()).toMatchObject({
|
||||
initialSortBy: sortKey,
|
||||
urlParams: urlSortParams[sortKey],
|
||||
urlParams: { sort: urlSortParams[sortKey] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -326,12 +325,10 @@ describe('IssuesListApp component', () => {
|
|||
describe('empty states', () => {
|
||||
describe('when there are issues', () => {
|
||||
describe('when search returns no results', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
global.jsdom.reconfigure({ url: `${TEST_HOST}?search=no+results` });
|
||||
|
||||
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows empty state', () => {
|
||||
|
@ -344,10 +341,8 @@ describe('IssuesListApp component', () => {
|
|||
});
|
||||
|
||||
describe('when "Open" tab has no issues', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows empty state', () => {
|
||||
|
@ -360,14 +355,12 @@ describe('IssuesListApp component', () => {
|
|||
});
|
||||
|
||||
describe('when "Closed" tab has no issues', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
global.jsdom.reconfigure({
|
||||
url: setUrlParams({ state: IssuableStates.Closed }, TEST_HOST),
|
||||
});
|
||||
|
||||
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows empty state', () => {
|
||||
|
@ -536,74 +529,67 @@ describe('IssuesListApp component', () => {
|
|||
describe('events', () => {
|
||||
describe('when "click-tab" event is emitted by IssuableList', () => {
|
||||
beforeEach(() => {
|
||||
axiosMock.onGet(defaultProvide.endpoint).reply(200, fetchIssuesResponse.data, {
|
||||
'x-page': 2,
|
||||
'x-total': xTotal,
|
||||
});
|
||||
|
||||
wrapper = mountComponent();
|
||||
|
||||
findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
|
||||
});
|
||||
|
||||
it('makes API call to filter the list by the new state and resets the page to 1', () => {
|
||||
expect(axiosMock.history.get[1].params).toMatchObject({
|
||||
page: 1,
|
||||
state: IssuableStates.Closed,
|
||||
});
|
||||
it('updates to the new tab', () => {
|
||||
expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "page-change" event is emitted by IssuableList', () => {
|
||||
const data = [{ id: 10, title: 'title', state }];
|
||||
const page = 2;
|
||||
const totalItems = 21;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock.onGet(defaultProvide.endpoint).reply(200, data, {
|
||||
'x-page': page,
|
||||
'x-total': totalItems,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent();
|
||||
|
||||
findIssuableList().vm.$emit('page-change', page);
|
||||
|
||||
await waitForPromises();
|
||||
findIssuableList().vm.$emit('page-change', 2);
|
||||
});
|
||||
|
||||
it('fetches issues with expected params', () => {
|
||||
expect(axiosMock.history.get[1].params).toMatchObject({
|
||||
page,
|
||||
per_page: PAGE_SIZE,
|
||||
state,
|
||||
with_labels_details: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates IssuableList with response data', () => {
|
||||
expect(findIssuableList().props()).toMatchObject({
|
||||
issuables: data,
|
||||
totalItems,
|
||||
currentPage: page,
|
||||
previousPage: page - 1,
|
||||
nextPage: page + 1,
|
||||
urlParams: { page, state },
|
||||
});
|
||||
it('updates to the new page', () => {
|
||||
expect(findIssuableList().props('currentPage')).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "reorder" event is emitted by IssuableList', () => {
|
||||
const issueOne = { id: 1, iid: 101, title: 'Issue one' };
|
||||
const issueTwo = { id: 2, iid: 102, title: 'Issue two' };
|
||||
const issueThree = { id: 3, iid: 103, title: 'Issue three' };
|
||||
const issueFour = { id: 4, iid: 104, title: 'Issue four' };
|
||||
const issues = [issueOne, issueTwo, issueThree, issueFour];
|
||||
const issueOne = {
|
||||
...getIssuesQueryResponse.data.project.issues.nodes[0],
|
||||
id: 1,
|
||||
iid: 101,
|
||||
title: 'Issue one',
|
||||
};
|
||||
const issueTwo = {
|
||||
...getIssuesQueryResponse.data.project.issues.nodes[0],
|
||||
id: 2,
|
||||
iid: 102,
|
||||
title: 'Issue two',
|
||||
};
|
||||
const issueThree = {
|
||||
...getIssuesQueryResponse.data.project.issues.nodes[0],
|
||||
id: 3,
|
||||
iid: 103,
|
||||
title: 'Issue three',
|
||||
};
|
||||
const issueFour = {
|
||||
...getIssuesQueryResponse.data.project.issues.nodes[0],
|
||||
id: 4,
|
||||
iid: 104,
|
||||
title: 'Issue four',
|
||||
};
|
||||
const response = {
|
||||
data: {
|
||||
project: {
|
||||
issues: {
|
||||
...getIssuesQueryResponse.data.project.issues,
|
||||
nodes: [issueOne, issueTwo, issueThree, issueFour],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock.onGet(defaultProvide.endpoint).reply(200, issues, fetchIssuesResponse.headers);
|
||||
wrapper = mountComponent();
|
||||
await waitForPromises();
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({ response });
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
describe('when successful', () => {
|
||||
|
@ -644,21 +630,18 @@ describe('IssuesListApp component', () => {
|
|||
});
|
||||
|
||||
describe('when "sort" event is emitted by IssuableList', () => {
|
||||
it.each(Object.keys(apiSortParams))(
|
||||
'fetches issues with correct params with payload `%s`',
|
||||
it.each(Object.keys(urlSortParams))(
|
||||
'updates to the new sort when payload is `%s`',
|
||||
async (sortKey) => {
|
||||
wrapper = mountComponent();
|
||||
|
||||
findIssuableList().vm.$emit('sort', sortKey);
|
||||
|
||||
await waitForPromises();
|
||||
jest.runOnlyPendingTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(axiosMock.history.get[1].params).toEqual({
|
||||
page: xPage,
|
||||
per_page: sortKey === RELATIVE_POSITION_DESC ? PAGE_SIZE_MANUAL : PAGE_SIZE,
|
||||
state,
|
||||
with_labels_details: true,
|
||||
...apiSortParams[sortKey],
|
||||
expect(findIssuableList().props('urlParams')).toMatchObject({
|
||||
sort: urlSortParams[sortKey],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -668,13 +651,11 @@ describe('IssuesListApp component', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = mountComponent();
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
|
||||
findIssuableList().vm.$emit('update-legacy-bulk-edit');
|
||||
});
|
||||
|
||||
it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', async () => {
|
||||
findIssuableList().vm.$emit('update-legacy-bulk-edit');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', () => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
|
||||
});
|
||||
});
|
||||
|
@ -686,10 +667,6 @@ describe('IssuesListApp component', () => {
|
|||
findIssuableList().vm.$emit('filter', filteredTokens);
|
||||
});
|
||||
|
||||
it('makes an API call to search for issues with the search term', () => {
|
||||
expect(axiosMock.history.get[1].params).toMatchObject(apiParams);
|
||||
});
|
||||
|
||||
it('updates IssuableList with url params', () => {
|
||||
expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
|
||||
});
|
||||
|
|
|
@ -3,6 +3,76 @@ import {
|
|||
OPERATOR_IS_NOT,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
|
||||
export const getIssuesQueryResponse = {
|
||||
data: {
|
||||
project: {
|
||||
issues: {
|
||||
count: 1,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'startcursor',
|
||||
endCursor: 'endcursor',
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Issue/123456',
|
||||
iid: '789',
|
||||
blockedByCount: 1,
|
||||
closedAt: null,
|
||||
confidential: false,
|
||||
createdAt: '2021-05-22T04:08:01Z',
|
||||
downvotes: 2,
|
||||
dueDate: '2021-05-29',
|
||||
healthStatus: null,
|
||||
humanTimeEstimate: null,
|
||||
moved: false,
|
||||
title: 'Issue title',
|
||||
updatedAt: '2021-05-22T04:08:01Z',
|
||||
upvotes: 3,
|
||||
userDiscussionsCount: 4,
|
||||
webUrl: 'project/-/issues/789',
|
||||
weight: 5,
|
||||
assignees: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/User/234',
|
||||
avatarUrl: 'avatar/url',
|
||||
name: 'Marge Simpson',
|
||||
username: 'msimpson',
|
||||
webUrl: 'url/msimpson',
|
||||
},
|
||||
],
|
||||
},
|
||||
author: {
|
||||
id: 'gid://gitlab/User/456',
|
||||
avatarUrl: 'avatar/url',
|
||||
name: 'Homer Simpson',
|
||||
username: 'hsimpson',
|
||||
webUrl: 'url/hsimpson',
|
||||
},
|
||||
labels: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/ProjectLabel/456',
|
||||
color: '#333',
|
||||
title: 'Label title',
|
||||
description: 'Label description',
|
||||
},
|
||||
],
|
||||
},
|
||||
milestone: null,
|
||||
taskCompletionStatus: {
|
||||
completedCount: 1,
|
||||
count: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const locationSearch = [
|
||||
'?search=find+issues',
|
||||
'author_username=homer',
|
||||
|
@ -19,8 +89,8 @@ export const locationSearch = [
|
|||
'not[label_name][]=drama',
|
||||
'my_reaction_emoji=thumbsup',
|
||||
'confidential=no',
|
||||
'iteration_title=season:+%234',
|
||||
'not[iteration_title]=season:+%2320',
|
||||
'iteration_id=4',
|
||||
'not[iteration_id]=20',
|
||||
'epic_id=gitlab-org%3A%3A%2612',
|
||||
'not[epic_id]=gitlab-org%3A%3A%2634',
|
||||
'weight=1',
|
||||
|
@ -51,8 +121,8 @@ export const filteredTokens = [
|
|||
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
|
||||
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
|
||||
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
|
||||
{ type: 'iteration', value: { data: 'season: #4', operator: OPERATOR_IS } },
|
||||
{ type: 'iteration', value: { data: 'season: #20', operator: OPERATOR_IS_NOT } },
|
||||
{ type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
|
||||
{ type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
|
||||
{ type: 'epic_id', value: { data: 'gitlab-org::&12', operator: OPERATOR_IS } },
|
||||
{ type: 'epic_id', value: { data: 'gitlab-org::&34', operator: OPERATOR_IS_NOT } },
|
||||
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
|
||||
|
@ -71,30 +141,32 @@ export const filteredTokensWithSpecialValues = [
|
|||
];
|
||||
|
||||
export const apiParams = {
|
||||
author_username: 'homer',
|
||||
'not[author_username]': 'marge',
|
||||
assignee_username: ['bart', 'lisa'],
|
||||
'not[assignee_username]': ['patty', 'selma'],
|
||||
milestone: 'season 4',
|
||||
'not[milestone]': 'season 20',
|
||||
labels: ['cartoon', 'tv'],
|
||||
'not[labels]': ['live action', 'drama'],
|
||||
my_reaction_emoji: 'thumbsup',
|
||||
authorUsername: 'homer',
|
||||
assigneeUsernames: ['bart', 'lisa'],
|
||||
milestoneTitle: 'season 4',
|
||||
labelName: ['cartoon', 'tv'],
|
||||
myReactionEmoji: 'thumbsup',
|
||||
confidential: 'no',
|
||||
iteration_title: 'season: #4',
|
||||
'not[iteration_title]': 'season: #20',
|
||||
epic_id: '12',
|
||||
'not[epic_id]': 'gitlab-org::&34',
|
||||
iterationId: '4',
|
||||
epicId: 'gitlab-org::&12',
|
||||
weight: '1',
|
||||
'not[weight]': '3',
|
||||
not: {
|
||||
authorUsername: 'marge',
|
||||
assigneeUsernames: ['patty', 'selma'],
|
||||
milestoneTitle: 'season 20',
|
||||
labelName: ['live action', 'drama'],
|
||||
iterationId: '20',
|
||||
epicId: 'gitlab-org::&34',
|
||||
weight: '3',
|
||||
},
|
||||
};
|
||||
|
||||
export const apiParamsWithSpecialValues = {
|
||||
assignee_id: '123',
|
||||
assignee_username: 'bart',
|
||||
my_reaction_emoji: 'None',
|
||||
iteration_id: 'Current',
|
||||
epic_id: 'None',
|
||||
assigneeId: '123',
|
||||
assigneeUsernames: 'bart',
|
||||
myReactionEmoji: 'None',
|
||||
iterationWildcardId: 'CURRENT',
|
||||
epicId: 'None',
|
||||
weight: 'None',
|
||||
};
|
||||
|
||||
|
@ -109,8 +181,8 @@ export const urlParams = {
|
|||
'not[label_name][]': ['live action', 'drama'],
|
||||
my_reaction_emoji: 'thumbsup',
|
||||
confidential: 'no',
|
||||
iteration_title: 'season: #4',
|
||||
'not[iteration_title]': 'season: #20',
|
||||
iteration_id: '4',
|
||||
'not[iteration_id]': '20',
|
||||
epic_id: 'gitlab-org%3A%3A%2612',
|
||||
'not[epic_id]': 'gitlab-org::&34',
|
||||
weight: '1',
|
||||
|
|
|
@ -8,19 +8,20 @@ import {
|
|||
urlParams,
|
||||
urlParamsWithSpecialValues,
|
||||
} from 'jest/issues_list/mock_data';
|
||||
import { API_PARAM, DUE_DATE_VALUES, URL_PARAM, urlSortParams } from '~/issues_list/constants';
|
||||
import { DUE_DATE_VALUES, urlSortParams } from '~/issues_list/constants';
|
||||
import {
|
||||
convertToParams,
|
||||
convertToUrlParams,
|
||||
convertToSearchQuery,
|
||||
getDueDateValue,
|
||||
getFilterTokens,
|
||||
getSortKey,
|
||||
getSortOptions,
|
||||
convertToApiParams,
|
||||
} from '~/issues_list/utils';
|
||||
|
||||
describe('getSortKey', () => {
|
||||
it.each(Object.keys(urlSortParams))('returns %s given the correct inputs', (sortKey) => {
|
||||
const { sort } = urlSortParams[sortKey];
|
||||
const sort = urlSortParams[sortKey];
|
||||
expect(getSortKey(sort)).toBe(sortKey);
|
||||
});
|
||||
});
|
||||
|
@ -80,31 +81,26 @@ describe('getFilterTokens', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('convertToParams', () => {
|
||||
describe('convertToApiParams', () => {
|
||||
it('returns api params given filtered tokens', () => {
|
||||
expect(convertToParams(filteredTokens, API_PARAM)).toEqual({
|
||||
...apiParams,
|
||||
epic_id: 'gitlab-org::&12',
|
||||
});
|
||||
expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
|
||||
});
|
||||
|
||||
it('returns api params given filtered tokens with special values', () => {
|
||||
expect(convertToParams(filteredTokensWithSpecialValues, API_PARAM)).toEqual(
|
||||
apiParamsWithSpecialValues,
|
||||
);
|
||||
expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToUrlParams', () => {
|
||||
it('returns url params given filtered tokens', () => {
|
||||
expect(convertToParams(filteredTokens, URL_PARAM)).toEqual({
|
||||
expect(convertToUrlParams(filteredTokens)).toEqual({
|
||||
...urlParams,
|
||||
epic_id: 'gitlab-org::&12',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns url params given filtered tokens with special values', () => {
|
||||
expect(convertToParams(filteredTokensWithSpecialValues, URL_PARAM)).toEqual(
|
||||
urlParamsWithSpecialValues,
|
||||
);
|
||||
expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -49,10 +49,13 @@ describe('User select dropdown', () => {
|
|||
const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
|
||||
const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
|
||||
|
||||
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
|
||||
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
|
||||
|
||||
const createComponent = ({
|
||||
props = {},
|
||||
searchQueryHandler = jest.fn().mockResolvedValue(projectMembersResponse),
|
||||
participantsQueryHandler = jest.fn().mockResolvedValue(participantsQueryResponse),
|
||||
searchQueryHandler = searchQueryHandlerSuccess,
|
||||
participantsQueryHandler = participantsQueryHandlerSuccess,
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo([
|
||||
[searchUsersQuery, searchQueryHandler],
|
||||
|
@ -91,6 +94,14 @@ describe('User select dropdown', () => {
|
|||
expect(findParticipantsLoading().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('skips the queries if `isEditing` prop is false', () => {
|
||||
createComponent({ props: { isEditing: false } });
|
||||
|
||||
expect(findParticipantsLoading().exists()).toBe(false);
|
||||
expect(searchQueryHandlerSuccess).not.toHaveBeenCalled();
|
||||
expect(participantsQueryHandlerSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits an `error` event if participants query was rejected', async () => {
|
||||
createComponent({ participantsQueryHandler: mockError });
|
||||
await waitForPromises();
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['PypiMetadata'] do
|
||||
it 'includes pypi metadatum fields' do
|
||||
expected_fields = %w[
|
||||
id required_python
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
end
|
||||
end
|
|
@ -302,7 +302,6 @@ RSpec.describe IssuesHelper do
|
|||
email: current_user&.notification_email,
|
||||
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
|
||||
empty_state_svg_path: '#',
|
||||
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
|
||||
export_csv_path: export_csv_project_issues_path(project),
|
||||
has_project_issues: project_issues(project).exists?.to_s,
|
||||
import_csv_issues_path: '#',
|
||||
|
|
|
@ -191,4 +191,21 @@ RSpec.describe LfsObject do
|
|||
expect { lfs_object.destroy! }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe '.unreferenced_in_batches' do
|
||||
let!(:unreferenced_lfs_object1) { create(:lfs_object, oid: '1') }
|
||||
let!(:referenced_lfs_object) { create(:lfs_objects_project).lfs_object }
|
||||
let!(:unreferenced_lfs_object2) { create(:lfs_object, oid: '2') }
|
||||
|
||||
it 'returns lfs objects in batches' do
|
||||
stub_const('LfsObject::BATCH_SIZE', 1)
|
||||
|
||||
batches = []
|
||||
described_class.unreferenced_in_batches { |batch| batches << batch }
|
||||
|
||||
expect(batches.size).to eq(2)
|
||||
expect(batches.first).to eq([unreferenced_lfs_object2])
|
||||
expect(batches.last).to eq([unreferenced_lfs_object1])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1503,6 +1503,13 @@ RSpec.describe API::Commits do
|
|||
expect(json_response).to eq("dry_run" => "success")
|
||||
expect(project.commit(branch)).to eq(head)
|
||||
end
|
||||
|
||||
it 'supports the use of a custom commit message' do
|
||||
post api(route, user), params: { branch: branch, message: 'foo' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response["message"]).to eq('foo')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repository is disabled' do
|
||||
|
|
|
@ -3,62 +3,35 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'package details' do
|
||||
include GraphqlHelpers
|
||||
include_context 'package details setup'
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:composer_package) { create(:composer_package, project: project) }
|
||||
let_it_be(:package) { create(:composer_package, project: project) }
|
||||
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
|
||||
let_it_be(:composer_metadatum) do
|
||||
# we are forced to manually create the metadatum, without using the factory to force the sha to be a string
|
||||
# and avoid an error where gitaly can't find the repository
|
||||
create(:composer_metadatum, package: composer_package, target_sha: 'foo_sha', composer_json: composer_json)
|
||||
create(:composer_metadatum, package: package, target_sha: 'foo_sha', composer_json: composer_json)
|
||||
end
|
||||
|
||||
let(:depth) { 3 }
|
||||
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
|
||||
let(:metadata) { query_graphql_fragment('ComposerMetadata') }
|
||||
let(:package_files) { all_graphql_fields_for('PackageFile') }
|
||||
let(:user) { project.owner }
|
||||
let(:package_global_id) { global_id_of(composer_package) }
|
||||
let(:package_details) { graphql_data_at(:package) }
|
||||
let(:metadata_response) { graphql_data_at(:package, :metadata) }
|
||||
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
|
||||
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
|
||||
metadata {
|
||||
#{metadata}
|
||||
}
|
||||
packageFiles {
|
||||
nodes {
|
||||
#{package_files}
|
||||
}
|
||||
}
|
||||
FIELDS
|
||||
end
|
||||
|
||||
subject { post_graphql(query, current_user: user) }
|
||||
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
it 'matches the JSON schema' do
|
||||
expect(package_details).to match_schema('graphql/packages/package_details')
|
||||
end
|
||||
it_behaves_like 'a package detail'
|
||||
|
||||
it 'has the correct metadata' do
|
||||
expect(metadata_response).to include(
|
||||
'targetSha' => 'foo_sha',
|
||||
'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
|
||||
)
|
||||
end
|
||||
|
||||
describe 'Composer' do
|
||||
it 'has the correct metadata' do
|
||||
expect(metadata_response).to include(
|
||||
'targetSha' => 'foo_sha',
|
||||
'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not have files' do
|
||||
expect(package_files_response).to be_empty
|
||||
end
|
||||
it 'does not have files' do
|
||||
expect(package_files_response).to be_empty
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,26 +3,13 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'conan package details' do
|
||||
include GraphqlHelpers
|
||||
include_context 'package details setup'
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:conan_package) { create(:conan_package, project: project) }
|
||||
let_it_be(:package) { create(:conan_package, project: project) }
|
||||
|
||||
let(:package_global_id) { global_id_of(conan_package) }
|
||||
let(:metadata) { query_graphql_fragment('ConanMetadata') }
|
||||
let(:first_file) { conan_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
|
||||
|
||||
let(:depth) { 3 }
|
||||
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
|
||||
let(:package_files) { all_graphql_fields_for('PackageFile') }
|
||||
let(:package_files_metadata) {query_graphql_fragment('ConanFileMetadata')}
|
||||
|
||||
let(:user) { project.owner }
|
||||
let(:package_details) { graphql_data_at(:package) }
|
||||
let(:metadata_response) { graphql_data_at(:package, :metadata) }
|
||||
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
|
||||
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
|
||||
let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
|
||||
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
|
||||
|
@ -46,35 +33,16 @@ RSpec.describe 'conan package details' do
|
|||
subject
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
it 'matches the JSON schema' do
|
||||
expect(package_details).to match_schema('graphql/packages/package_details')
|
||||
end
|
||||
end
|
||||
it_behaves_like 'a package detail'
|
||||
it_behaves_like 'a package with files'
|
||||
|
||||
it 'has the correct metadata' do
|
||||
expect(metadata_response).to include(
|
||||
'id' => global_id_of(conan_package.conan_metadatum),
|
||||
'recipe' => conan_package.conan_metadatum.recipe,
|
||||
'packageChannel' => conan_package.conan_metadatum.package_channel,
|
||||
'packageUsername' => conan_package.conan_metadatum.package_username,
|
||||
'recipePath' => conan_package.conan_metadatum.recipe_path
|
||||
)
|
||||
end
|
||||
|
||||
it 'has the right amount of files' do
|
||||
expect(package_files_response.length).to be(conan_package.package_files.length)
|
||||
end
|
||||
|
||||
it 'has the basic package files data' do
|
||||
expect(first_file_response).to include(
|
||||
'id' => global_id_of(first_file),
|
||||
'fileName' => first_file.file_name,
|
||||
'size' => first_file.size.to_s,
|
||||
'downloadPath' => first_file.download_path,
|
||||
'fileSha1' => first_file.file_sha1,
|
||||
'fileMd5' => first_file.file_md5,
|
||||
'fileSha256' => first_file.file_sha256
|
||||
'id' => global_id_of(package.conan_metadatum),
|
||||
'recipe' => package.conan_metadatum.recipe,
|
||||
'packageChannel' => package.conan_metadatum.package_channel,
|
||||
'packageUsername' => package.conan_metadatum.package_username,
|
||||
'recipePath' => package.conan_metadatum.recipe_path
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -3,89 +3,51 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'maven package details' do
|
||||
include GraphqlHelpers
|
||||
include_context 'package details setup'
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:maven_package) { create(:maven_package, project: project) }
|
||||
let_it_be(:package) { create(:maven_package, project: project) }
|
||||
|
||||
let(:package_global_id) { global_id_of(maven_package) }
|
||||
let(:metadata) { query_graphql_fragment('MavenMetadata') }
|
||||
let(:first_file) { maven_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
|
||||
|
||||
let(:depth) { 3 }
|
||||
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
|
||||
let(:package_files) { all_graphql_fields_for('PackageFile') }
|
||||
|
||||
let(:user) { project.owner }
|
||||
let(:package_details) { graphql_data_at(:package) }
|
||||
let(:metadata_response) { graphql_data_at(:package, :metadata) }
|
||||
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
|
||||
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
|
||||
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
|
||||
metadata {
|
||||
#{metadata}
|
||||
}
|
||||
packageFiles {
|
||||
nodes {
|
||||
#{package_files}
|
||||
}
|
||||
}
|
||||
FIELDS
|
||||
end
|
||||
|
||||
subject { post_graphql(query, current_user: user) }
|
||||
|
||||
shared_examples 'a working maven package' do
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
it 'matches the JSON schema' do
|
||||
expect(package_details).to match_schema('graphql/packages/package_details')
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'correct maven metadata' do
|
||||
it 'has the correct metadata' do
|
||||
expect(metadata_response).to include(
|
||||
'id' => global_id_of(maven_package.maven_metadatum),
|
||||
'path' => maven_package.maven_metadatum.path,
|
||||
'appGroup' => maven_package.maven_metadatum.app_group,
|
||||
'appVersion' => maven_package.maven_metadatum.app_version,
|
||||
'appName' => maven_package.maven_metadatum.app_name
|
||||
)
|
||||
end
|
||||
|
||||
it 'has the right amount of files' do
|
||||
expect(package_files_response.length).to be(maven_package.package_files.length)
|
||||
end
|
||||
|
||||
it 'has the basic package files data' do
|
||||
expect(first_file_response).to include(
|
||||
'id' => global_id_of(first_file),
|
||||
'fileName' => first_file.file_name,
|
||||
'size' => first_file.size.to_s,
|
||||
'downloadPath' => first_file.download_path,
|
||||
'fileSha1' => first_file.file_sha1,
|
||||
'fileMd5' => first_file.file_md5,
|
||||
'fileSha256' => first_file.file_sha256
|
||||
'id' => global_id_of(package.maven_metadatum),
|
||||
'path' => package.maven_metadatum.path,
|
||||
'appGroup' => package.maven_metadatum.app_group,
|
||||
'appVersion' => package.maven_metadatum.app_version,
|
||||
'appName' => package.maven_metadatum.app_name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'a maven package with version' do
|
||||
it_behaves_like "a working maven package"
|
||||
subject { post_graphql(query, current_user: user) }
|
||||
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it_behaves_like 'a package detail'
|
||||
it_behaves_like 'correct maven metadata'
|
||||
it_behaves_like 'a package with files'
|
||||
end
|
||||
|
||||
context 'a versionless maven package' do
|
||||
let_it_be(:maven_metadatum) { create(:maven_metadatum, app_version: nil) }
|
||||
let_it_be(:maven_package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) }
|
||||
let_it_be(:package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) }
|
||||
|
||||
it_behaves_like "a working maven package"
|
||||
subject { post_graphql(query, current_user: user) }
|
||||
|
||||
it "has an empty version" do
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it_behaves_like 'a package detail'
|
||||
it_behaves_like 'correct maven metadata'
|
||||
it_behaves_like 'a package with files'
|
||||
|
||||
it 'has an empty version' do
|
||||
subject
|
||||
|
||||
expect(metadata_response['appVersion']).to eq(nil)
|
||||
|
|
|
@ -3,37 +3,11 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'nuget package details' do
|
||||
include GraphqlHelpers
|
||||
include_context 'package details setup'
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:nuget_package) { create(:nuget_package, :with_metadatum, project: project) }
|
||||
let_it_be(:package) { create(:nuget_package, :with_metadatum, project: project) }
|
||||
|
||||
let(:package_global_id) { global_id_of(nuget_package) }
|
||||
let(:metadata) { query_graphql_fragment('NugetMetadata') }
|
||||
let(:first_file) { nuget_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
|
||||
|
||||
let(:depth) { 3 }
|
||||
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
|
||||
let(:package_files) { all_graphql_fields_for('PackageFile') }
|
||||
|
||||
let(:user) { project.owner }
|
||||
let(:package_details) { graphql_data_at(:package) }
|
||||
let(:metadata_response) { graphql_data_at(:package, :metadata) }
|
||||
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
|
||||
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
|
||||
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
|
||||
metadata {
|
||||
#{metadata}
|
||||
}
|
||||
packageFiles {
|
||||
nodes {
|
||||
#{package_files}
|
||||
}
|
||||
}
|
||||
FIELDS
|
||||
end
|
||||
|
||||
subject { post_graphql(query, current_user: user) }
|
||||
|
||||
|
@ -41,34 +15,15 @@ RSpec.describe 'nuget package details' do
|
|||
subject
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
it 'matches the JSON schema' do
|
||||
expect(package_details).to match_schema('graphql/packages/package_details')
|
||||
end
|
||||
end
|
||||
it_behaves_like 'a package detail'
|
||||
it_behaves_like 'a package with files'
|
||||
|
||||
it 'has the correct metadata' do
|
||||
expect(metadata_response).to include(
|
||||
'id' => global_id_of(nuget_package.nuget_metadatum),
|
||||
'licenseUrl' => nuget_package.nuget_metadatum.license_url,
|
||||
'projectUrl' => nuget_package.nuget_metadatum.project_url,
|
||||
'iconUrl' => nuget_package.nuget_metadatum.icon_url
|
||||
)
|
||||
end
|
||||
|
||||
it 'has the right amount of files' do
|
||||
expect(package_files_response.length).to be(nuget_package.package_files.length)
|
||||
end
|
||||
|
||||
it 'has the basic package files data' do
|
||||
expect(first_file_response).to include(
|
||||
'id' => global_id_of(first_file),
|
||||
'fileName' => first_file.file_name,
|
||||
'size' => first_file.size.to_s,
|
||||
'downloadPath' => first_file.download_path,
|
||||
'fileSha1' => first_file.file_sha1,
|
||||
'fileMd5' => first_file.file_md5,
|
||||
'fileSha256' => first_file.file_sha256
|
||||
'id' => global_id_of(package.nuget_metadatum),
|
||||
'licenseUrl' => package.nuget_metadatum.license_url,
|
||||
'projectUrl' => package.nuget_metadatum.project_url,
|
||||
'iconUrl' => package.nuget_metadatum.icon_url
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'pypi package details' do
|
||||
include GraphqlHelpers
|
||||
include_context 'package details setup'
|
||||
|
||||
let_it_be(:package) { create(:pypi_package, project: project) }
|
||||
|
||||
let(:metadata) { query_graphql_fragment('PypiMetadata') }
|
||||
|
||||
subject { post_graphql(query, current_user: user) }
|
||||
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it_behaves_like 'a package detail'
|
||||
it_behaves_like 'a package with files'
|
||||
|
||||
it 'has the correct metadata' do
|
||||
expect(metadata_response).to include(
|
||||
'id' => global_id_of(package.pypi_metadatum),
|
||||
'requiredPython' => package.pypi_metadatum.required_python
|
||||
)
|
||||
end
|
||||
end
|
|
@ -24,7 +24,7 @@ RSpec.describe Commits::CherryPickService do
|
|||
repository.add_branch(user, branch_name, merge_base_sha)
|
||||
end
|
||||
|
||||
def cherry_pick(sha, branch_name)
|
||||
def cherry_pick(sha, branch_name, message: nil)
|
||||
commit = project.commit(sha)
|
||||
|
||||
described_class.new(
|
||||
|
@ -32,7 +32,8 @@ RSpec.describe Commits::CherryPickService do
|
|||
user,
|
||||
commit: commit,
|
||||
start_branch: branch_name,
|
||||
branch_name: branch_name
|
||||
branch_name: branch_name,
|
||||
message: message
|
||||
).execute
|
||||
end
|
||||
|
||||
|
@ -45,6 +46,14 @@ RSpec.describe Commits::CherryPickService do
|
|||
head = repository.find_branch(branch_name).target
|
||||
expect(head).not_to eq(merge_base_sha)
|
||||
end
|
||||
|
||||
it 'supports a custom commit message' do
|
||||
result = cherry_pick(merge_commit_sha, branch_name, message: 'foo')
|
||||
branch = repository.find_branch(branch_name)
|
||||
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(branch.dereferenced_target.message).to eq('foo')
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'successful cherry-pick'
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'package details setup' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:package) { create(:package, project: project) }
|
||||
|
||||
let(:package_global_id) { global_id_of(package) }
|
||||
|
||||
let(:depth) { 3 }
|
||||
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
|
||||
let(:package_files) { all_graphql_fields_for('PackageFile') }
|
||||
let(:user) { project.owner }
|
||||
let(:package_details) { graphql_data_at(:package) }
|
||||
let(:metadata_response) { graphql_data_at(:package, :metadata) }
|
||||
let(:first_file) { package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
|
||||
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
|
||||
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
|
||||
let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
|
||||
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
|
||||
metadata {
|
||||
#{metadata}
|
||||
}
|
||||
packageFiles {
|
||||
nodes {
|
||||
#{package_files}
|
||||
}
|
||||
}
|
||||
FIELDS
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'a package detail' do
|
||||
it_behaves_like 'a working graphql query' do
|
||||
it 'matches the JSON schema' do
|
||||
expect(package_details).to match_schema('graphql/packages/package_details')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'a package with files' do
|
||||
it 'has the right amount of files' do
|
||||
expect(package_files_response.length).to be(package.package_files.length)
|
||||
end
|
||||
|
||||
it 'has the basic package files data' do
|
||||
expect(first_file_response).to include(
|
||||
'id' => global_id_of(first_file),
|
||||
'fileName' => first_file.file_name,
|
||||
'size' => first_file.size.to_s,
|
||||
'downloadPath' => first_file.download_path,
|
||||
'fileSha1' => first_file.file_sha1,
|
||||
'fileMd5' => first_file.file_md5,
|
||||
'fileSha256' => first_file.file_sha256
|
||||
)
|
||||
end
|
||||
end
|
|
@ -306,7 +306,6 @@ RSpec.describe 'Every Sidekiq worker' do
|
|||
'IncidentManagement::OncallRotations::PersistAllRotationsShiftsJob' => 3,
|
||||
'IncidentManagement::OncallRotations::PersistShiftsJob' => 3,
|
||||
'IncidentManagement::PagerDuty::ProcessIncidentWorker' => 3,
|
||||
'IncidentManagement::ProcessAlertWorker' => 3,
|
||||
'IncidentManagement::ProcessPrometheusAlertWorker' => 3,
|
||||
'InvalidGpgSignatureUpdateWorker' => 3,
|
||||
'IrkerWorker' => 3,
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe IncidentManagement::ProcessAlertWorker do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:settings) { create(:project_incident_management_setting, project: project, create_issue: true) }
|
||||
|
||||
describe '#perform' do
|
||||
let_it_be(:started_at) { Time.now.rfc3339 }
|
||||
let_it_be(:payload) { { 'title' => 'title', 'start_time' => started_at } }
|
||||
let_it_be(:alert) { create(:alert_management_alert, project: project, payload: payload, started_at: started_at) }
|
||||
|
||||
let(:created_issue) { Issue.last! }
|
||||
|
||||
subject { described_class.new.perform(nil, nil, alert.id) }
|
||||
|
||||
before do
|
||||
allow(Gitlab::AppLogger).to receive(:warn).and_call_original
|
||||
|
||||
allow(AlertManagement::CreateAlertIssueService)
|
||||
.to receive(:new).with(alert, User.alert_bot)
|
||||
.and_call_original
|
||||
end
|
||||
|
||||
shared_examples 'creates issue successfully' do
|
||||
it 'creates an issue' do
|
||||
expect(AlertManagement::CreateAlertIssueService)
|
||||
.to receive(:new).with(alert, User.alert_bot)
|
||||
|
||||
expect { subject }.to change { Issue.count }.by(1)
|
||||
end
|
||||
|
||||
it 'updates AlertManagement::Alert#issue_id' do
|
||||
subject
|
||||
|
||||
expect(alert.reload.issue_id).to eq(created_issue.id)
|
||||
end
|
||||
|
||||
it 'does not write a warning to log' do
|
||||
subject
|
||||
|
||||
expect(Gitlab::AppLogger).not_to have_received(:warn)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid alert' do
|
||||
it_behaves_like 'creates issue successfully'
|
||||
|
||||
context 'when alert cannot be updated' do
|
||||
let_it_be(:alert) { create(:alert_management_alert, :with_validation_errors, project: project, payload: payload) }
|
||||
|
||||
it 'updates AlertManagement::Alert#issue_id' do
|
||||
expect { subject }.not_to change { alert.reload.issue_id }
|
||||
end
|
||||
|
||||
it 'logs a warning' do
|
||||
subject
|
||||
|
||||
expect(Gitlab::AppLogger).to have_received(:warn).with(
|
||||
message: 'Cannot process an Incident',
|
||||
issue_id: created_issue.id,
|
||||
alert_id: alert.id,
|
||||
errors: 'Hosts hosts array is over 255 chars'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'prometheus alert' do
|
||||
let_it_be(:alert) { create(:alert_management_alert, :prometheus, project: project, started_at: started_at) }
|
||||
|
||||
it_behaves_like 'creates issue successfully'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid alert' do
|
||||
let(:invalid_alert_id) { non_existing_record_id }
|
||||
|
||||
subject { described_class.new.perform(nil, nil, invalid_alert_id) }
|
||||
|
||||
it 'does not create issues' do
|
||||
expect(AlertManagement::CreateAlertIssueService).not_to receive(:new)
|
||||
|
||||
expect { subject }.not_to change { Issue.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -34,14 +34,14 @@ RSpec.describe RemoveUnreferencedLfsObjectsWorker do
|
|||
end
|
||||
|
||||
it 'removes unreferenced lfs objects' do
|
||||
worker.perform
|
||||
expect(worker.perform).to eq(2)
|
||||
|
||||
expect(LfsObject.where(id: unreferenced_lfs_object1.id)).to be_empty
|
||||
expect(LfsObject.where(id: unreferenced_lfs_object2.id)).to be_empty
|
||||
end
|
||||
|
||||
it 'leaves referenced lfs objects' do
|
||||
worker.perform
|
||||
expect(worker.perform).to eq(2)
|
||||
|
||||
expect(referenced_lfs_object1.reload).to be_present
|
||||
expect(referenced_lfs_object2.reload).to be_present
|
||||
|
@ -50,10 +50,12 @@ RSpec.describe RemoveUnreferencedLfsObjectsWorker do
|
|||
it 'removes unreferenced lfs objects after project removal' do
|
||||
project1.destroy!
|
||||
|
||||
worker.perform
|
||||
expect(worker.perform).to eq(3)
|
||||
|
||||
expect(referenced_lfs_object1.reload).to be_present
|
||||
expect(LfsObject.where(id: referenced_lfs_object2.id)).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'an idempotent worker'
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue