Add latest changes from gitlab-org/gitlab@master
|
@ -50,9 +50,6 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
issuableId() {
|
|
||||||
return getIdFromGraphQLId(this.issuable.id);
|
|
||||||
},
|
|
||||||
createdInPastDay() {
|
createdInPastDay() {
|
||||||
const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
|
const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
|
||||||
return createdSecondsAgo < SECONDS_IN_DAY;
|
return createdSecondsAgo < SECONDS_IN_DAY;
|
||||||
|
@ -64,7 +61,7 @@ export default {
|
||||||
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
|
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
|
||||||
},
|
},
|
||||||
authorId() {
|
authorId() {
|
||||||
return getIdFromGraphQLId(this.author.id);
|
return getIdFromGraphQLId(`${this.author.id}`);
|
||||||
},
|
},
|
||||||
isIssuableUrlExternal() {
|
isIssuableUrlExternal() {
|
||||||
return isExternal(this.webUrl);
|
return isExternal(this.webUrl);
|
||||||
|
@ -73,10 +70,10 @@ export default {
|
||||||
return this.issuable.labels?.nodes || this.issuable.labels || [];
|
return this.issuable.labels?.nodes || this.issuable.labels || [];
|
||||||
},
|
},
|
||||||
labelIdsString() {
|
labelIdsString() {
|
||||||
return JSON.stringify(this.labels.map((label) => getIdFromGraphQLId(label.id)));
|
return JSON.stringify(this.labels.map((label) => label.id));
|
||||||
},
|
},
|
||||||
assignees() {
|
assignees() {
|
||||||
return this.issuable.assignees?.nodes || this.issuable.assignees || [];
|
return this.issuable.assignees || [];
|
||||||
},
|
},
|
||||||
createdAt() {
|
createdAt() {
|
||||||
return sprintf(__('created %{timeAgo}'), {
|
return sprintf(__('created %{timeAgo}'), {
|
||||||
|
@ -84,9 +81,6 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updatedAt() {
|
updatedAt() {
|
||||||
if (!this.issuable.updatedAt) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return sprintf(__('updated %{timeAgo}'), {
|
return sprintf(__('updated %{timeAgo}'), {
|
||||||
timeAgo: getTimeago().format(this.issuable.updatedAt),
|
timeAgo: getTimeago().format(this.issuable.updatedAt),
|
||||||
});
|
});
|
||||||
|
@ -163,7 +157,7 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li
|
<li
|
||||||
:id="`issuable_${issuableId}`"
|
:id="`issuable_${issuable.id}`"
|
||||||
class="issue gl-px-5!"
|
class="issue gl-px-5!"
|
||||||
:class="{ closed: issuable.closedAt, today: createdInPastDay }"
|
:class="{ closed: issuable.closedAt, today: createdInPastDay }"
|
||||||
:data-labels="labelIdsString"
|
:data-labels="labelIdsString"
|
||||||
|
@ -173,7 +167,7 @@ export default {
|
||||||
<gl-form-checkbox
|
<gl-form-checkbox
|
||||||
class="gl-mr-0"
|
class="gl-mr-0"
|
||||||
:checked="checked"
|
:checked="checked"
|
||||||
:data-id="issuableId"
|
:data-id="issuable.id"
|
||||||
@input="$emit('checked-input', $event)"
|
@input="$emit('checked-input', $event)"
|
||||||
>
|
>
|
||||||
<span class="gl-sr-only">{{ issuable.title }}</span>
|
<span class="gl-sr-only">{{ issuable.title }}</span>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
|
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|
||||||
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
||||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||||
|
|
||||||
|
@ -212,7 +211,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
issuableId(issuable) {
|
issuableId(issuable) {
|
||||||
return getIdFromGraphQLId(issuable.id) || issuable.iid || uniqueId();
|
return issuable.id || issuable.iid || uniqueId();
|
||||||
},
|
},
|
||||||
issuableChecked(issuable) {
|
issuableChecked(issuable) {
|
||||||
return this.checkedIssuables[this.issuableId(issuable)]?.checked;
|
return this.checkedIssuables[this.issuableId(issuable)]?.checked;
|
||||||
|
|
|
@ -42,9 +42,6 @@ export default {
|
||||||
}
|
}
|
||||||
return __('Milestone');
|
return __('Milestone');
|
||||||
},
|
},
|
||||||
milestoneLink() {
|
|
||||||
return this.issue.milestone.webPath || this.issue.milestone.webUrl;
|
|
||||||
},
|
|
||||||
dueDate() {
|
dueDate() {
|
||||||
return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true);
|
return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true);
|
||||||
},
|
},
|
||||||
|
@ -52,7 +49,7 @@ export default {
|
||||||
return isInPast(new Date(this.issue.dueDate));
|
return isInPast(new Date(this.issue.dueDate));
|
||||||
},
|
},
|
||||||
timeEstimate() {
|
timeEstimate() {
|
||||||
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
|
return this.issue.timeStats?.humanTimeEstimate;
|
||||||
},
|
},
|
||||||
showHealthStatus() {
|
showHealthStatus() {
|
||||||
return this.hasIssuableHealthStatusFeature && this.issue.healthStatus;
|
return this.hasIssuableHealthStatusFeature && this.issue.healthStatus;
|
||||||
|
@ -88,7 +85,7 @@ export default {
|
||||||
class="issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3"
|
class="issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3"
|
||||||
data-testid="issuable-milestone"
|
data-testid="issuable-milestone"
|
||||||
>
|
>
|
||||||
<gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate">
|
<gl-link v-gl-tooltip :href="issue.milestone.webUrl" :title="milestoneDate">
|
||||||
<gl-icon name="clock" />
|
<gl-icon name="clock" />
|
||||||
{{ issue.milestone.title }}
|
{{ issue.milestone.title }}
|
||||||
</gl-link>
|
</gl-link>
|
||||||
|
|
|
@ -9,21 +9,24 @@ import {
|
||||||
GlTooltipDirective,
|
GlTooltipDirective,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||||
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
|
import { toNumber } from 'lodash';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
||||||
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
|
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
|
||||||
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
|
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
|
||||||
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
|
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
|
||||||
import {
|
import {
|
||||||
|
API_PARAM,
|
||||||
|
apiSortParams,
|
||||||
CREATED_DESC,
|
CREATED_DESC,
|
||||||
i18n,
|
i18n,
|
||||||
MAX_LIST_SIZE,
|
MAX_LIST_SIZE,
|
||||||
PAGE_SIZE,
|
PAGE_SIZE,
|
||||||
PARAM_DUE_DATE,
|
PARAM_DUE_DATE,
|
||||||
|
PARAM_PAGE,
|
||||||
PARAM_SORT,
|
PARAM_SORT,
|
||||||
PARAM_STATE,
|
PARAM_STATE,
|
||||||
RELATIVE_POSITION_ASC,
|
RELATIVE_POSITION_DESC,
|
||||||
TOKEN_TYPE_ASSIGNEE,
|
TOKEN_TYPE_ASSIGNEE,
|
||||||
TOKEN_TYPE_AUTHOR,
|
TOKEN_TYPE_AUTHOR,
|
||||||
TOKEN_TYPE_CONFIDENTIAL,
|
TOKEN_TYPE_CONFIDENTIAL,
|
||||||
|
@ -34,19 +37,19 @@ import {
|
||||||
TOKEN_TYPE_MILESTONE,
|
TOKEN_TYPE_MILESTONE,
|
||||||
TOKEN_TYPE_WEIGHT,
|
TOKEN_TYPE_WEIGHT,
|
||||||
UPDATED_DESC,
|
UPDATED_DESC,
|
||||||
|
URL_PARAM,
|
||||||
urlSortParams,
|
urlSortParams,
|
||||||
} from '~/issues_list/constants';
|
} from '~/issues_list/constants';
|
||||||
import {
|
import {
|
||||||
convertToApiParams,
|
convertToParams,
|
||||||
convertToSearchQuery,
|
convertToSearchQuery,
|
||||||
convertToUrlParams,
|
|
||||||
getDueDateValue,
|
getDueDateValue,
|
||||||
getFilterTokens,
|
getFilterTokens,
|
||||||
getSortKey,
|
getSortKey,
|
||||||
getSortOptions,
|
getSortOptions,
|
||||||
} from '~/issues_list/utils';
|
} from '~/issues_list/utils';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
|
||||||
import {
|
import {
|
||||||
DEFAULT_NONE_ANY,
|
DEFAULT_NONE_ANY,
|
||||||
OPERATOR_IS_ONLY,
|
OPERATOR_IS_ONLY,
|
||||||
|
@ -104,6 +107,9 @@ export default {
|
||||||
emptyStateSvgPath: {
|
emptyStateSvgPath: {
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
endpoint: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
exportCsvPath: {
|
exportCsvPath: {
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
@ -167,53 +173,15 @@ export default {
|
||||||
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
|
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
|
||||||
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
|
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
|
||||||
filterTokens: getFilterTokens(window.location.search),
|
filterTokens: getFilterTokens(window.location.search),
|
||||||
|
isLoading: false,
|
||||||
issues: [],
|
issues: [],
|
||||||
page: 1,
|
page: toNumber(getParameterByName(PARAM_PAGE)) || 1,
|
||||||
pageInfo: {},
|
|
||||||
pageParams: {
|
|
||||||
firstPageSize: PAGE_SIZE,
|
|
||||||
},
|
|
||||||
showBulkEditSidebar: false,
|
showBulkEditSidebar: false,
|
||||||
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
|
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
|
||||||
state: state || IssuableStates.Opened,
|
state: state || IssuableStates.Opened,
|
||||||
totalIssues: 0,
|
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: {
|
computed: {
|
||||||
hasSearch() {
|
hasSearch() {
|
||||||
return this.searchQuery || Object.keys(this.urlFilterParams).length;
|
return this.searchQuery || Object.keys(this.urlFilterParams).length;
|
||||||
|
@ -222,22 +190,16 @@ export default {
|
||||||
return this.showBulkEditSidebar || !this.issues.length;
|
return this.showBulkEditSidebar || !this.issues.length;
|
||||||
},
|
},
|
||||||
isManualOrdering() {
|
isManualOrdering() {
|
||||||
return this.sortKey === RELATIVE_POSITION_ASC;
|
return this.sortKey === RELATIVE_POSITION_DESC;
|
||||||
},
|
},
|
||||||
isOpenTab() {
|
isOpenTab() {
|
||||||
return this.state === IssuableStates.Opened;
|
return this.state === IssuableStates.Opened;
|
||||||
},
|
},
|
||||||
nextPage() {
|
|
||||||
return Number(this.pageInfo.hasNextPage);
|
|
||||||
},
|
|
||||||
previousPage() {
|
|
||||||
return Number(this.pageInfo.hasPreviousPage);
|
|
||||||
},
|
|
||||||
apiFilterParams() {
|
apiFilterParams() {
|
||||||
return convertToApiParams(this.filterTokens);
|
return convertToParams(this.filterTokens, API_PARAM);
|
||||||
},
|
},
|
||||||
urlFilterParams() {
|
urlFilterParams() {
|
||||||
return convertToUrlParams(this.filterTokens);
|
return convertToParams(this.filterTokens, URL_PARAM);
|
||||||
},
|
},
|
||||||
searchQuery() {
|
searchQuery() {
|
||||||
return convertToSearchQuery(this.filterTokens) || undefined;
|
return convertToSearchQuery(this.filterTokens) || undefined;
|
||||||
|
@ -252,7 +214,6 @@ export default {
|
||||||
dataType: 'user',
|
dataType: 'user',
|
||||||
unique: true,
|
unique: true,
|
||||||
defaultAuthors: [],
|
defaultAuthors: [],
|
||||||
operators: OPERATOR_IS_ONLY,
|
|
||||||
fetchAuthors: this.fetchUsers,
|
fetchAuthors: this.fetchUsers,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -279,7 +240,7 @@ export default {
|
||||||
title: TOKEN_TITLE_LABEL,
|
title: TOKEN_TITLE_LABEL,
|
||||||
icon: 'labels',
|
icon: 'labels',
|
||||||
token: LabelToken,
|
token: LabelToken,
|
||||||
defaultLabels: DEFAULT_NONE_ANY,
|
defaultLabels: [],
|
||||||
fetchLabels: this.fetchLabels,
|
fetchLabels: this.fetchLabels,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -372,9 +333,10 @@ export default {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
due_date: this.dueDateFilter,
|
due_date: this.dueDateFilter,
|
||||||
|
page: this.page,
|
||||||
search: this.searchQuery,
|
search: this.searchQuery,
|
||||||
sort: urlSortParams[this.sortKey],
|
|
||||||
state: this.state,
|
state: this.state,
|
||||||
|
...urlSortParams[this.sortKey],
|
||||||
...filterParams,
|
...filterParams,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -384,6 +346,7 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
|
eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
|
||||||
|
this.fetchIssues();
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
|
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
|
||||||
|
@ -423,19 +386,59 @@ export default {
|
||||||
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
|
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
|
||||||
},
|
},
|
||||||
fetchIterations(search) {
|
fetchIterations(search) {
|
||||||
const number = Number(search);
|
return axios.get(this.projectIterationsPath, { params: { search } });
|
||||||
return !search || Number.isNaN(number)
|
|
||||||
? axios.get(this.projectIterationsPath, { params: { search } })
|
|
||||||
: axios.get(this.projectIterationsPath, { params: { id: number } });
|
|
||||||
},
|
},
|
||||||
fetchUsers(search) {
|
fetchUsers(search) {
|
||||||
return axios.get(this.autocompleteUsersPath, { params: { 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() {
|
getExportCsvPathWithQuery() {
|
||||||
return `${this.exportCsvPath}${window.location.search}`;
|
return `${this.exportCsvPath}${window.location.search}`;
|
||||||
},
|
},
|
||||||
getStatus(issue) {
|
getStatus(issue) {
|
||||||
if (issue.closedAt && issue.moved) {
|
if (issue.closedAt && issue.movedToId) {
|
||||||
return this.$options.i18n.closedMoved;
|
return this.$options.i18n.closedMoved;
|
||||||
}
|
}
|
||||||
if (issue.closedAt) {
|
if (issue.closedAt) {
|
||||||
|
@ -466,30 +469,18 @@ export default {
|
||||||
},
|
},
|
||||||
handleClickTab(state) {
|
handleClickTab(state) {
|
||||||
if (this.state !== state) {
|
if (this.state !== state) {
|
||||||
this.pageParams = {
|
|
||||||
firstPageSize: PAGE_SIZE,
|
|
||||||
};
|
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
}
|
}
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
this.fetchIssues();
|
||||||
},
|
},
|
||||||
handleFilter(filter) {
|
handleFilter(filter) {
|
||||||
this.filterTokens = filter;
|
this.filterTokens = filter;
|
||||||
|
this.fetchIssues();
|
||||||
},
|
},
|
||||||
handlePageChange(page) {
|
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.page = page;
|
||||||
|
this.fetchIssues();
|
||||||
},
|
},
|
||||||
handleReorder({ newIndex, oldIndex }) {
|
handleReorder({ newIndex, oldIndex }) {
|
||||||
const issueToMove = this.issues[oldIndex];
|
const issueToMove = this.issues[oldIndex];
|
||||||
|
@ -526,6 +517,7 @@ export default {
|
||||||
},
|
},
|
||||||
handleSort(value) {
|
handleSort(value) {
|
||||||
this.sortKey = value;
|
this.sortKey = value;
|
||||||
|
this.fetchIssues();
|
||||||
},
|
},
|
||||||
toggleBulkEditSidebar(showBulkEditSidebar) {
|
toggleBulkEditSidebar(showBulkEditSidebar) {
|
||||||
this.showBulkEditSidebar = showBulkEditSidebar;
|
this.showBulkEditSidebar = showBulkEditSidebar;
|
||||||
|
@ -549,13 +541,14 @@ export default {
|
||||||
:tabs="$options.IssuableListTabs"
|
:tabs="$options.IssuableListTabs"
|
||||||
:current-tab="state"
|
:current-tab="state"
|
||||||
:tab-counts="tabCounts"
|
:tab-counts="tabCounts"
|
||||||
:issuables-loading="$apollo.loading"
|
:issuables-loading="isLoading"
|
||||||
:is-manual-ordering="isManualOrdering"
|
:is-manual-ordering="isManualOrdering"
|
||||||
:show-bulk-edit-sidebar="showBulkEditSidebar"
|
:show-bulk-edit-sidebar="showBulkEditSidebar"
|
||||||
:show-pagination-controls="showPaginationControls"
|
:show-pagination-controls="showPaginationControls"
|
||||||
|
:total-items="totalIssues"
|
||||||
:current-page="page"
|
:current-page="page"
|
||||||
:previous-page="previousPage"
|
:previous-page="page - 1"
|
||||||
:next-page="nextPage"
|
:next-page="page + 1"
|
||||||
:url-params="urlParams"
|
:url-params="urlParams"
|
||||||
@click-tab="handleClickTab"
|
@click-tab="handleClickTab"
|
||||||
@filter="handleFilter"
|
@filter="handleFilter"
|
||||||
|
@ -638,7 +631,7 @@ export default {
|
||||||
</li>
|
</li>
|
||||||
<blocking-issues-count
|
<blocking-issues-count
|
||||||
class="gl-display-none gl-sm-display-block"
|
class="gl-display-none gl-sm-display-block"
|
||||||
:blocking-issues-count="issuable.blockedByCount"
|
:blocking-issues-count="issuable.blockingIssuesCount"
|
||||||
:is-list-item="true"
|
:is-list-item="true"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -101,6 +101,7 @@ export const i18n = {
|
||||||
export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map';
|
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_DUE_DATE = 'due_date';
|
||||||
|
export const PARAM_PAGE = 'page';
|
||||||
export const PARAM_SORT = 'sort';
|
export const PARAM_SORT = 'sort';
|
||||||
export const PARAM_STATE = 'state';
|
export const PARAM_STATE = 'state';
|
||||||
|
|
||||||
|
@ -124,21 +125,21 @@ export const CREATED_ASC = 'CREATED_ASC';
|
||||||
export const CREATED_DESC = 'CREATED_DESC';
|
export const CREATED_DESC = 'CREATED_DESC';
|
||||||
export const DUE_DATE_ASC = 'DUE_DATE_ASC';
|
export const DUE_DATE_ASC = 'DUE_DATE_ASC';
|
||||||
export const DUE_DATE_DESC = 'DUE_DATE_DESC';
|
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 LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
|
||||||
export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
|
export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
|
||||||
export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
|
export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
|
||||||
export const POPULARITY_ASC = 'POPULARITY_ASC';
|
export const POPULARITY_ASC = 'POPULARITY_ASC';
|
||||||
export const POPULARITY_DESC = 'POPULARITY_DESC';
|
export const POPULARITY_DESC = 'POPULARITY_DESC';
|
||||||
export const PRIORITY_ASC = 'PRIORITY_ASC';
|
|
||||||
export const PRIORITY_DESC = 'PRIORITY_DESC';
|
export const PRIORITY_DESC = 'PRIORITY_DESC';
|
||||||
export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC';
|
export const RELATIVE_POSITION_DESC = 'RELATIVE_POSITION_DESC';
|
||||||
export const UPDATED_ASC = 'UPDATED_ASC';
|
export const UPDATED_ASC = 'UPDATED_ASC';
|
||||||
export const UPDATED_DESC = 'UPDATED_DESC';
|
export const UPDATED_DESC = 'UPDATED_DESC';
|
||||||
export const WEIGHT_ASC = 'WEIGHT_ASC';
|
export const WEIGHT_ASC = 'WEIGHT_ASC';
|
||||||
export const WEIGHT_DESC = 'WEIGHT_DESC';
|
export const WEIGHT_DESC = 'WEIGHT_DESC';
|
||||||
|
|
||||||
const PRIORITY_ASC_SORT = 'priority_asc';
|
const SORT_ASC = 'asc';
|
||||||
|
const SORT_DESC = 'desc';
|
||||||
|
|
||||||
const CREATED_DATE_SORT = 'created_date';
|
const CREATED_DATE_SORT = 'created_date';
|
||||||
const CREATED_ASC_SORT = 'created_asc';
|
const CREATED_ASC_SORT = 'created_asc';
|
||||||
const UPDATED_DESC_SORT = 'updated_desc';
|
const UPDATED_DESC_SORT = 'updated_desc';
|
||||||
|
@ -146,30 +147,129 @@ const UPDATED_ASC_SORT = 'updated_asc';
|
||||||
const MILESTONE_SORT = 'milestone';
|
const MILESTONE_SORT = 'milestone';
|
||||||
const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
|
const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
|
||||||
const DUE_DATE_DESC_SORT = 'due_date_desc';
|
const DUE_DATE_DESC_SORT = 'due_date_desc';
|
||||||
const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
|
|
||||||
const POPULARITY_ASC_SORT = 'popularity_asc';
|
const POPULARITY_ASC_SORT = 'popularity_asc';
|
||||||
const WEIGHT_DESC_SORT = 'weight_desc';
|
const WEIGHT_DESC_SORT = 'weight_desc';
|
||||||
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_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 = {
|
export const urlSortParams = {
|
||||||
[PRIORITY_ASC]: PRIORITY_ASC_SORT,
|
[PRIORITY_DESC]: {
|
||||||
[PRIORITY_DESC]: PRIORITY,
|
sort: PRIORITY,
|
||||||
[CREATED_ASC]: CREATED_ASC_SORT,
|
},
|
||||||
[CREATED_DESC]: CREATED_DATE_SORT,
|
[CREATED_ASC]: {
|
||||||
[UPDATED_ASC]: UPDATED_ASC_SORT,
|
sort: CREATED_ASC_SORT,
|
||||||
[UPDATED_DESC]: UPDATED_DESC_SORT,
|
},
|
||||||
[MILESTONE_DUE_ASC]: MILESTONE_SORT,
|
[CREATED_DESC]: {
|
||||||
[MILESTONE_DUE_DESC]: MILESTONE_DUE_DESC_SORT,
|
sort: CREATED_DATE_SORT,
|
||||||
[DUE_DATE_ASC]: DUE_DATE,
|
},
|
||||||
[DUE_DATE_DESC]: DUE_DATE_DESC_SORT,
|
[UPDATED_ASC]: {
|
||||||
[POPULARITY_ASC]: POPULARITY_ASC_SORT,
|
sort: UPDATED_ASC_SORT,
|
||||||
[POPULARITY_DESC]: POPULARITY,
|
},
|
||||||
[LABEL_PRIORITY_ASC]: LABEL_PRIORITY_ASC_SORT,
|
[UPDATED_DESC]: {
|
||||||
[LABEL_PRIORITY_DESC]: LABEL_PRIORITY,
|
sort: UPDATED_DESC_SORT,
|
||||||
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
|
},
|
||||||
[WEIGHT_ASC]: WEIGHT,
|
[MILESTONE_DUE_ASC]: {
|
||||||
[WEIGHT_DESC]: WEIGHT_DESC_SORT,
|
sort: MILESTONE_SORT,
|
||||||
[BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAX_LIST_SIZE = 10;
|
export const MAX_LIST_SIZE = 10;
|
||||||
|
@ -194,7 +294,12 @@ export const TOKEN_TYPE_WEIGHT = 'weight';
|
||||||
export const filters = {
|
export const filters = {
|
||||||
[TOKEN_TYPE_AUTHOR]: {
|
[TOKEN_TYPE_AUTHOR]: {
|
||||||
[API_PARAM]: {
|
[API_PARAM]: {
|
||||||
[NORMAL_FILTER]: 'authorUsername',
|
[OPERATOR_IS]: {
|
||||||
|
[NORMAL_FILTER]: 'author_username',
|
||||||
|
},
|
||||||
|
[OPERATOR_IS_NOT]: {
|
||||||
|
[NORMAL_FILTER]: 'not[author_username]',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[URL_PARAM]: {
|
[URL_PARAM]: {
|
||||||
[OPERATOR_IS]: {
|
[OPERATOR_IS]: {
|
||||||
|
@ -207,8 +312,13 @@ export const filters = {
|
||||||
},
|
},
|
||||||
[TOKEN_TYPE_ASSIGNEE]: {
|
[TOKEN_TYPE_ASSIGNEE]: {
|
||||||
[API_PARAM]: {
|
[API_PARAM]: {
|
||||||
[NORMAL_FILTER]: 'assigneeUsernames',
|
[OPERATOR_IS]: {
|
||||||
[SPECIAL_FILTER]: 'assigneeId',
|
[NORMAL_FILTER]: 'assignee_username',
|
||||||
|
[SPECIAL_FILTER]: 'assignee_id',
|
||||||
|
},
|
||||||
|
[OPERATOR_IS_NOT]: {
|
||||||
|
[NORMAL_FILTER]: 'not[assignee_username]',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[URL_PARAM]: {
|
[URL_PARAM]: {
|
||||||
[OPERATOR_IS]: {
|
[OPERATOR_IS]: {
|
||||||
|
@ -223,7 +333,12 @@ export const filters = {
|
||||||
},
|
},
|
||||||
[TOKEN_TYPE_MILESTONE]: {
|
[TOKEN_TYPE_MILESTONE]: {
|
||||||
[API_PARAM]: {
|
[API_PARAM]: {
|
||||||
[NORMAL_FILTER]: 'milestoneTitle',
|
[OPERATOR_IS]: {
|
||||||
|
[NORMAL_FILTER]: 'milestone',
|
||||||
|
},
|
||||||
|
[OPERATOR_IS_NOT]: {
|
||||||
|
[NORMAL_FILTER]: 'not[milestone]',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[URL_PARAM]: {
|
[URL_PARAM]: {
|
||||||
[OPERATOR_IS]: {
|
[OPERATOR_IS]: {
|
||||||
|
@ -236,13 +351,16 @@ export const filters = {
|
||||||
},
|
},
|
||||||
[TOKEN_TYPE_LABEL]: {
|
[TOKEN_TYPE_LABEL]: {
|
||||||
[API_PARAM]: {
|
[API_PARAM]: {
|
||||||
[NORMAL_FILTER]: 'labelName',
|
[OPERATOR_IS]: {
|
||||||
[SPECIAL_FILTER]: 'labelName',
|
[NORMAL_FILTER]: 'labels',
|
||||||
|
},
|
||||||
|
[OPERATOR_IS_NOT]: {
|
||||||
|
[NORMAL_FILTER]: 'not[labels]',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[URL_PARAM]: {
|
[URL_PARAM]: {
|
||||||
[OPERATOR_IS]: {
|
[OPERATOR_IS]: {
|
||||||
[NORMAL_FILTER]: 'label_name[]',
|
[NORMAL_FILTER]: 'label_name[]',
|
||||||
[SPECIAL_FILTER]: 'label_name[]',
|
|
||||||
},
|
},
|
||||||
[OPERATOR_IS_NOT]: {
|
[OPERATOR_IS_NOT]: {
|
||||||
[NORMAL_FILTER]: 'not[label_name][]',
|
[NORMAL_FILTER]: 'not[label_name][]',
|
||||||
|
@ -251,8 +369,10 @@ export const filters = {
|
||||||
},
|
},
|
||||||
[TOKEN_TYPE_MY_REACTION]: {
|
[TOKEN_TYPE_MY_REACTION]: {
|
||||||
[API_PARAM]: {
|
[API_PARAM]: {
|
||||||
[NORMAL_FILTER]: 'myReactionEmoji',
|
[OPERATOR_IS]: {
|
||||||
[SPECIAL_FILTER]: 'myReactionEmoji',
|
[NORMAL_FILTER]: 'my_reaction_emoji',
|
||||||
|
[SPECIAL_FILTER]: 'my_reaction_emoji',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[URL_PARAM]: {
|
[URL_PARAM]: {
|
||||||
[OPERATOR_IS]: {
|
[OPERATOR_IS]: {
|
||||||
|
@ -263,8 +383,10 @@ export const filters = {
|
||||||
},
|
},
|
||||||
[TOKEN_TYPE_CONFIDENTIAL]: {
|
[TOKEN_TYPE_CONFIDENTIAL]: {
|
||||||
[API_PARAM]: {
|
[API_PARAM]: {
|
||||||
|
[OPERATOR_IS]: {
|
||||||
[NORMAL_FILTER]: 'confidential',
|
[NORMAL_FILTER]: 'confidential',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
[URL_PARAM]: {
|
[URL_PARAM]: {
|
||||||
[OPERATOR_IS]: {
|
[OPERATOR_IS]: {
|
||||||
[NORMAL_FILTER]: 'confidential',
|
[NORMAL_FILTER]: 'confidential',
|
||||||
|
@ -273,23 +395,33 @@ export const filters = {
|
||||||
},
|
},
|
||||||
[TOKEN_TYPE_ITERATION]: {
|
[TOKEN_TYPE_ITERATION]: {
|
||||||
[API_PARAM]: {
|
[API_PARAM]: {
|
||||||
[NORMAL_FILTER]: 'iterationId',
|
|
||||||
[SPECIAL_FILTER]: 'iterationWildcardId',
|
|
||||||
},
|
|
||||||
[URL_PARAM]: {
|
|
||||||
[OPERATOR_IS]: {
|
[OPERATOR_IS]: {
|
||||||
[NORMAL_FILTER]: 'iteration_id',
|
[NORMAL_FILTER]: 'iteration_title',
|
||||||
[SPECIAL_FILTER]: 'iteration_id',
|
[SPECIAL_FILTER]: 'iteration_id',
|
||||||
},
|
},
|
||||||
[OPERATOR_IS_NOT]: {
|
[OPERATOR_IS_NOT]: {
|
||||||
[NORMAL_FILTER]: 'not[iteration_id]',
|
[NORMAL_FILTER]: 'not[iteration_title]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[URL_PARAM]: {
|
||||||
|
[OPERATOR_IS]: {
|
||||||
|
[NORMAL_FILTER]: 'iteration_title',
|
||||||
|
[SPECIAL_FILTER]: 'iteration_id',
|
||||||
|
},
|
||||||
|
[OPERATOR_IS_NOT]: {
|
||||||
|
[NORMAL_FILTER]: 'not[iteration_title]',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[TOKEN_TYPE_EPIC]: {
|
[TOKEN_TYPE_EPIC]: {
|
||||||
[API_PARAM]: {
|
[API_PARAM]: {
|
||||||
[NORMAL_FILTER]: 'epicId',
|
[OPERATOR_IS]: {
|
||||||
[SPECIAL_FILTER]: 'epicId',
|
[NORMAL_FILTER]: 'epic_id',
|
||||||
|
[SPECIAL_FILTER]: 'epic_id',
|
||||||
|
},
|
||||||
|
[OPERATOR_IS_NOT]: {
|
||||||
|
[NORMAL_FILTER]: 'not[epic_id]',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[URL_PARAM]: {
|
[URL_PARAM]: {
|
||||||
[OPERATOR_IS]: {
|
[OPERATOR_IS]: {
|
||||||
|
@ -303,9 +435,14 @@ export const filters = {
|
||||||
},
|
},
|
||||||
[TOKEN_TYPE_WEIGHT]: {
|
[TOKEN_TYPE_WEIGHT]: {
|
||||||
[API_PARAM]: {
|
[API_PARAM]: {
|
||||||
|
[OPERATOR_IS]: {
|
||||||
[NORMAL_FILTER]: 'weight',
|
[NORMAL_FILTER]: 'weight',
|
||||||
[SPECIAL_FILTER]: 'weight',
|
[SPECIAL_FILTER]: 'weight',
|
||||||
},
|
},
|
||||||
|
[OPERATOR_IS_NOT]: {
|
||||||
|
[NORMAL_FILTER]: 'not[weight]',
|
||||||
|
},
|
||||||
|
},
|
||||||
[URL_PARAM]: {
|
[URL_PARAM]: {
|
||||||
[OPERATOR_IS]: {
|
[OPERATOR_IS]: {
|
||||||
[NORMAL_FILTER]: 'weight',
|
[NORMAL_FILTER]: 'weight',
|
||||||
|
|
|
@ -73,13 +73,6 @@ export function mountIssuesListApp() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.use(VueApollo);
|
|
||||||
|
|
||||||
const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
|
|
||||||
const apolloProvider = new VueApollo({
|
|
||||||
defaultClient,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
autocompleteAwardEmojisPath,
|
autocompleteAwardEmojisPath,
|
||||||
autocompleteUsersPath,
|
autocompleteUsersPath,
|
||||||
|
@ -90,6 +83,7 @@ export function mountIssuesListApp() {
|
||||||
email,
|
email,
|
||||||
emailsHelpPagePath,
|
emailsHelpPagePath,
|
||||||
emptyStateSvgPath,
|
emptyStateSvgPath,
|
||||||
|
endpoint,
|
||||||
exportCsvPath,
|
exportCsvPath,
|
||||||
groupEpicsPath,
|
groupEpicsPath,
|
||||||
hasBlockedIssuesFeature,
|
hasBlockedIssuesFeature,
|
||||||
|
@ -121,13 +115,14 @@ export function mountIssuesListApp() {
|
||||||
el,
|
el,
|
||||||
// Currently does not use Vue Apollo, but need to provide {} for now until the
|
// 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
|
// issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
|
||||||
apolloProvider,
|
apolloProvider: {},
|
||||||
provide: {
|
provide: {
|
||||||
autocompleteAwardEmojisPath,
|
autocompleteAwardEmojisPath,
|
||||||
autocompleteUsersPath,
|
autocompleteUsersPath,
|
||||||
calendarPath,
|
calendarPath,
|
||||||
canBulkUpdate: parseBoolean(canBulkUpdate),
|
canBulkUpdate: parseBoolean(canBulkUpdate),
|
||||||
emptyStateSvgPath,
|
emptyStateSvgPath,
|
||||||
|
endpoint,
|
||||||
groupEpicsPath,
|
groupEpicsPath,
|
||||||
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
|
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
|
||||||
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
|
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
#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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
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,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
API_PARAM,
|
|
||||||
BLOCKING_ISSUES_DESC,
|
BLOCKING_ISSUES_DESC,
|
||||||
CREATED_ASC,
|
CREATED_ASC,
|
||||||
CREATED_DESC,
|
CREATED_DESC,
|
||||||
|
@ -7,36 +6,29 @@ import {
|
||||||
DUE_DATE_DESC,
|
DUE_DATE_DESC,
|
||||||
DUE_DATE_VALUES,
|
DUE_DATE_VALUES,
|
||||||
filters,
|
filters,
|
||||||
LABEL_PRIORITY_ASC,
|
|
||||||
LABEL_PRIORITY_DESC,
|
LABEL_PRIORITY_DESC,
|
||||||
MILESTONE_DUE_ASC,
|
MILESTONE_DUE_ASC,
|
||||||
MILESTONE_DUE_DESC,
|
MILESTONE_DUE_DESC,
|
||||||
NORMAL_FILTER,
|
NORMAL_FILTER,
|
||||||
POPULARITY_ASC,
|
POPULARITY_ASC,
|
||||||
POPULARITY_DESC,
|
POPULARITY_DESC,
|
||||||
PRIORITY_ASC,
|
|
||||||
PRIORITY_DESC,
|
PRIORITY_DESC,
|
||||||
RELATIVE_POSITION_ASC,
|
RELATIVE_POSITION_DESC,
|
||||||
SPECIAL_FILTER,
|
SPECIAL_FILTER,
|
||||||
SPECIAL_FILTER_VALUES,
|
SPECIAL_FILTER_VALUES,
|
||||||
TOKEN_TYPE_ASSIGNEE,
|
TOKEN_TYPE_ASSIGNEE,
|
||||||
TOKEN_TYPE_ITERATION,
|
|
||||||
UPDATED_ASC,
|
UPDATED_ASC,
|
||||||
UPDATED_DESC,
|
UPDATED_DESC,
|
||||||
URL_PARAM,
|
|
||||||
urlSortParams,
|
urlSortParams,
|
||||||
WEIGHT_ASC,
|
WEIGHT_ASC,
|
||||||
WEIGHT_DESC,
|
WEIGHT_DESC,
|
||||||
} from '~/issues_list/constants';
|
} from '~/issues_list/constants';
|
||||||
import { isPositiveInteger } from '~/lib/utils/number_utils';
|
import { isPositiveInteger } from '~/lib/utils/number_utils';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import {
|
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||||
FILTERED_SEARCH_TERM,
|
|
||||||
OPERATOR_IS_NOT,
|
|
||||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
|
||||||
|
|
||||||
export const getSortKey = (sort) =>
|
export const getSortKey = (sort) =>
|
||||||
Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort);
|
Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort);
|
||||||
|
|
||||||
export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
|
export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
|
||||||
|
|
||||||
|
@ -46,7 +38,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
|
||||||
id: 1,
|
id: 1,
|
||||||
title: __('Priority'),
|
title: __('Priority'),
|
||||||
sortDirection: {
|
sortDirection: {
|
||||||
ascending: PRIORITY_ASC,
|
ascending: PRIORITY_DESC,
|
||||||
descending: PRIORITY_DESC,
|
descending: PRIORITY_DESC,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -94,7 +86,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
|
||||||
id: 7,
|
id: 7,
|
||||||
title: __('Label priority'),
|
title: __('Label priority'),
|
||||||
sortDirection: {
|
sortDirection: {
|
||||||
ascending: LABEL_PRIORITY_ASC,
|
ascending: LABEL_PRIORITY_DESC,
|
||||||
descending: LABEL_PRIORITY_DESC,
|
descending: LABEL_PRIORITY_DESC,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -102,8 +94,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
|
||||||
id: 8,
|
id: 8,
|
||||||
title: __('Manual'),
|
title: __('Manual'),
|
||||||
sortDirection: {
|
sortDirection: {
|
||||||
ascending: RELATIVE_POSITION_ASC,
|
ascending: RELATIVE_POSITION_DESC,
|
||||||
descending: RELATIVE_POSITION_ASC,
|
descending: RELATIVE_POSITION_DESC,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -186,36 +178,12 @@ const getFilterType = (data, tokenType = '') =>
|
||||||
? SPECIAL_FILTER
|
? SPECIAL_FILTER
|
||||||
: NORMAL_FILTER;
|
: NORMAL_FILTER;
|
||||||
|
|
||||||
const isIterationSpecialValue = (tokenType, value) =>
|
export const convertToParams = (filterTokens, paramType) =>
|
||||||
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
|
filterTokens
|
||||||
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
|
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
|
||||||
.reduce((acc, token) => {
|
.reduce((acc, token) => {
|
||||||
const filterType = getFilterType(token.value.data, token.type);
|
const filterType = getFilterType(token.value.data, token.type);
|
||||||
const param = filters[token.type][URL_PARAM][token.value.operator]?.[filterType];
|
const param = filters[token.type][paramType][token.value.operator]?.[filterType];
|
||||||
return Object.assign(acc, {
|
return Object.assign(acc, {
|
||||||
[param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
|
[param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { GlButton } from '@gitlab/ui';
|
import { GlButton } from '@gitlab/ui';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||||
|
import { scrollUp } from '~/lib/utils/scroll_utils';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import { PAGE_SIZE } from '~/releases/constants';
|
import { PAGE_SIZE } from '~/releases/constants';
|
||||||
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
|
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
|
||||||
|
@ -9,6 +10,7 @@ import { convertAllReleasesGraphQLResponse } from '~/releases/util';
|
||||||
import ReleaseBlock from './release_block.vue';
|
import ReleaseBlock from './release_block.vue';
|
||||||
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
|
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
|
||||||
import ReleasesEmptyState from './releases_empty_state.vue';
|
import ReleasesEmptyState from './releases_empty_state.vue';
|
||||||
|
import ReleasesPaginationApolloClient from './releases_pagination_apollo_client.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ReleasesIndexApolloClientApp',
|
name: 'ReleasesIndexApolloClientApp',
|
||||||
|
@ -17,6 +19,7 @@ export default {
|
||||||
ReleaseBlock,
|
ReleaseBlock,
|
||||||
ReleaseSkeletonLoader,
|
ReleaseSkeletonLoader,
|
||||||
ReleasesEmptyState,
|
ReleasesEmptyState,
|
||||||
|
ReleasesPaginationApolloClient,
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
projectPath: {
|
projectPath: {
|
||||||
|
@ -85,6 +88,16 @@ export default {
|
||||||
|
|
||||||
return convertAllReleasesGraphQLResponse(this.graphqlResponse).data;
|
return convertAllReleasesGraphQLResponse(this.graphqlResponse).data;
|
||||||
},
|
},
|
||||||
|
pageInfo() {
|
||||||
|
if (!this.graphqlResponse || this.hasError) {
|
||||||
|
return {
|
||||||
|
hasPreviousPage: false,
|
||||||
|
hasNextPage: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.graphqlResponse.data.project.releases.pageInfo;
|
||||||
|
},
|
||||||
shouldRenderEmptyState() {
|
shouldRenderEmptyState() {
|
||||||
return !this.releases.length && !this.hasError && !this.isLoading;
|
return !this.releases.length && !this.hasError && !this.isLoading;
|
||||||
},
|
},
|
||||||
|
@ -94,6 +107,13 @@ export default {
|
||||||
shouldRenderLoadingIndicator() {
|
shouldRenderLoadingIndicator() {
|
||||||
return this.isLoading && !this.hasError;
|
return this.isLoading && !this.hasError;
|
||||||
},
|
},
|
||||||
|
shouldRenderPagination() {
|
||||||
|
return (
|
||||||
|
!this.isLoading &&
|
||||||
|
!this.hasError &&
|
||||||
|
(this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage)
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.updateQueryParamsFromUrl();
|
this.updateQueryParamsFromUrl();
|
||||||
|
@ -108,6 +128,16 @@ export default {
|
||||||
this.cursors.before = getParameterByName('before');
|
this.cursors.before = getParameterByName('before');
|
||||||
this.cursors.after = getParameterByName('after');
|
this.cursors.after = getParameterByName('after');
|
||||||
},
|
},
|
||||||
|
onPaginationButtonPress() {
|
||||||
|
this.updateQueryParamsFromUrl();
|
||||||
|
|
||||||
|
// In some cases, Apollo Client is able to pull its results from the cache instead of making
|
||||||
|
// a new network request. In these cases, the page's content gets swapped out immediately without
|
||||||
|
// changing the page's scroll, leaving the user looking at the bottom of the new page.
|
||||||
|
// To make the experience consistent, regardless of how the data is sourced, we manually
|
||||||
|
// scroll to the top of the page every time a pagination button is pressed.
|
||||||
|
scrollUp();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
newRelease: __('New release'),
|
newRelease: __('New release'),
|
||||||
|
@ -140,6 +170,13 @@ export default {
|
||||||
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
|
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<releases-pagination-apollo-client
|
||||||
|
v-if="shouldRenderPagination"
|
||||||
|
:page-info="pageInfo"
|
||||||
|
@prev="onPaginationButtonPress"
|
||||||
|
@next="onPaginationButtonPress"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script>
|
||||||
|
import { GlKeysetPagination } from '@gitlab/ui';
|
||||||
|
import { isBoolean } from 'lodash';
|
||||||
|
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReleasesPaginationApolloClient',
|
||||||
|
components: { GlKeysetPagination },
|
||||||
|
props: {
|
||||||
|
pageInfo: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
validator: (info) => isBoolean(info.hasPreviousPage) && isBoolean(info.hasNextPage),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onPrev(before) {
|
||||||
|
historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
|
||||||
|
},
|
||||||
|
onNext(after) {
|
||||||
|
historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="gl-display-flex gl-justify-content-center">
|
||||||
|
<gl-keyset-pagination
|
||||||
|
v-bind="pageInfo"
|
||||||
|
:prev-text="__('Prev')"
|
||||||
|
:next-text="__('Next')"
|
||||||
|
v-on="$listeners"
|
||||||
|
@prev="onPrev($event)"
|
||||||
|
@next="onNext($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -39,7 +39,7 @@ export default {
|
||||||
return this.value.data;
|
return this.value.data;
|
||||||
},
|
},
|
||||||
activeIteration() {
|
activeIteration() {
|
||||||
return this.iterations.find((iteration) => iteration.id === Number(this.currentValue));
|
return this.iterations.find((iteration) => iteration.title === this.currentValue);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -99,8 +99,8 @@ export default {
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<gl-filtered-search-suggestion
|
<gl-filtered-search-suggestion
|
||||||
v-for="iteration in iterations"
|
v-for="iteration in iterations"
|
||||||
:key="iteration.id"
|
:key="iteration.title"
|
||||||
:value="String(iteration.id)"
|
:value="iteration.title"
|
||||||
>
|
>
|
||||||
{{ iteration.title }}
|
{{ iteration.title }}
|
||||||
</gl-filtered-search-suggestion>
|
</gl-filtered-search-suggestion>
|
||||||
|
|
|
@ -51,7 +51,6 @@ module IntegrationsActions
|
||||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||||
def integration
|
def integration
|
||||||
@integration ||= find_or_initialize_non_project_specific_integration(params[:id])
|
@integration ||= find_or_initialize_non_project_specific_integration(params[:id])
|
||||||
@service ||= @integration # TODO: remove references to @service https://gitlab.com/gitlab-org/gitlab/-/issues/329759
|
|
||||||
end
|
end
|
||||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||||
|
|
||||||
|
|
|
@ -185,18 +185,12 @@ class Projects::BranchesController < Projects::ApplicationController
|
||||||
# Here we get one more branch to indicate if there are more data we're not showing
|
# Here we get one more branch to indicate if there are more data we're not showing
|
||||||
limit = @overview_max_branches + 1
|
limit = @overview_max_branches + 1
|
||||||
|
|
||||||
if Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml)
|
|
||||||
@active_branches =
|
@active_branches =
|
||||||
BranchesFinder.new(@repository, { per_page: limit, sort: sort_value_recently_updated })
|
BranchesFinder.new(@repository, { per_page: limit, sort: sort_value_recently_updated })
|
||||||
.execute(gitaly_pagination: true).select(&:active?)
|
.execute(gitaly_pagination: true).select(&:active?)
|
||||||
@stale_branches =
|
@stale_branches =
|
||||||
BranchesFinder.new(@repository, { per_page: limit, sort: sort_value_oldest_updated })
|
BranchesFinder.new(@repository, { per_page: limit, sort: sort_value_oldest_updated })
|
||||||
.execute(gitaly_pagination: true).select(&:stale?)
|
.execute(gitaly_pagination: true).select(&:stale?)
|
||||||
else
|
|
||||||
@active_branches, @stale_branches = BranchesFinder.new(@repository, sort: sort_value_recently_updated).execute.partition(&:active?)
|
|
||||||
@active_branches = @active_branches.first(limit)
|
|
||||||
@stale_branches = @stale_branches.first(limit)
|
|
||||||
end
|
|
||||||
|
|
||||||
@branches = @active_branches + @stale_branches
|
@branches = @active_branches + @stale_branches
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,14 +85,13 @@ class Projects::ServicesController < Projects::ApplicationController
|
||||||
|
|
||||||
def integration
|
def integration
|
||||||
@integration ||= @project.find_or_initialize_service(params[:id])
|
@integration ||= @project.find_or_initialize_service(params[:id])
|
||||||
@service ||= @integration # TODO: remove references to @service https://gitlab.com/gitlab-org/gitlab/-/issues/329759
|
|
||||||
end
|
end
|
||||||
alias_method :service, :integration
|
alias_method :service, :integration
|
||||||
|
|
||||||
def web_hook_logs
|
def web_hook_logs
|
||||||
return unless @service.service_hook.present?
|
return unless integration.service_hook.present?
|
||||||
|
|
||||||
@web_hook_logs ||= @service.service_hook.web_hook_logs.recent.page(params[:page])
|
@web_hook_logs ||= integration.service_hook.web_hook_logs.recent.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_service_enabled
|
def ensure_service_enabled
|
||||||
|
@ -101,8 +100,8 @@ class Projects::ServicesController < Projects::ApplicationController
|
||||||
|
|
||||||
def serialize_as_json
|
def serialize_as_json
|
||||||
integration
|
integration
|
||||||
.as_json(only: @service.json_fields)
|
.as_json(only: integration.json_fields)
|
||||||
.merge(errors: @service.errors.as_json)
|
.merge(errors: integration.errors.as_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redirect_deprecated_prometheus_service
|
def redirect_deprecated_prometheus_service
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Projects
|
||||||
feature_category :integrations
|
feature_category :integrations
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@services = @project.find_or_initialize_services
|
@integrations = @project.find_or_initialize_services
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -190,6 +190,7 @@ module IssuesHelper
|
||||||
email: current_user&.notification_email,
|
email: current_user&.notification_email,
|
||||||
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
|
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
|
||||||
empty_state_svg_path: image_path('illustrations/issues.svg'),
|
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),
|
export_csv_path: export_csv_project_issues_path(project),
|
||||||
has_project_issues: project_issues(project).exists?.to_s,
|
has_project_issues: project_issues(project).exists?.to_s,
|
||||||
import_csv_issues_path: import_csv_namespace_project_issues_path,
|
import_csv_issues_path: import_csv_namespace_project_issues_path,
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.page-title= _('Delete serverless domain?')
|
%h3.page-title= _('Delete serverless domain?')
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
|
|
||||||
.modal-body
|
.modal-body
|
||||||
- if domain_attached
|
- if domain_attached
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#js-delete-user-modal
|
#js-delete-user-modal
|
||||||
#js-modal-texts.hidden{ "hidden": true, "aria-hidden": true }
|
#js-modal-texts.hidden{ "hidden": true, "aria-hidden": "true" }
|
||||||
%div{ data: { modal: "delete",
|
%div{ data: { modal: "delete",
|
||||||
title: s_("AdminUsers|Delete User %{username}?"),
|
title: s_("AdminUsers|Delete User %{username}?"),
|
||||||
action: s_('AdminUsers|Delete user'),
|
action: s_('AdminUsers|Delete user'),
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
%li
|
%li
|
||||||
%button.js-shortcuts-modal-trigger{ type: "button" }
|
%button.js-shortcuts-modal-trigger{ type: "button" }
|
||||||
= _("Keyboard shortcuts")
|
= _("Keyboard shortcuts")
|
||||||
%span.text-secondary.float-right{ "aria-hidden": true }= '?'.html_safe
|
%span.text-secondary.float-right{ "aria-hidden": "true" }= '?'.html_safe
|
||||||
%li.divider
|
%li.divider
|
||||||
%li
|
%li
|
||||||
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
|
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
|
||||||
|
|
|
@ -135,7 +135,7 @@
|
||||||
%h4.modal-title
|
%h4.modal-title
|
||||||
= s_("Profiles|Position and size your new avatar")
|
= s_("Profiles|Position and size your new avatar")
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _("Close") }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _("Close") }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
.modal-body
|
.modal-body
|
||||||
.profile-crop-image-container
|
.profile-crop-image-container
|
||||||
%img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") }
|
%img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") }
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.modal-title Import projects from Bitbucket
|
%h3.modal-title Import projects from Bitbucket
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
.modal-body
|
.modal-body
|
||||||
To enable importing projects from Bitbucket,
|
To enable importing projects from Bitbucket,
|
||||||
- if current_user.admin?
|
- if current_user.admin?
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.modal-title Import projects from GitLab.com
|
%h3.modal-title Import projects from GitLab.com
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
.modal-body
|
.modal-body
|
||||||
To enable importing projects from GitLab.com,
|
To enable importing projects from GitLab.com,
|
||||||
- if current_user.admin?
|
- if current_user.admin?
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.page-title= _('Reduce this project’s visibility?')
|
%h3.page-title= _('Reduce this project’s visibility?')
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true }= sprite_icon("close")
|
%span{ "aria-hidden": "true" }= sprite_icon("close")
|
||||||
.modal-body
|
.modal-body
|
||||||
%p
|
%p
|
||||||
- if @project.group
|
- if @project.group
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.page-title= _('Create New Directory')
|
%h3.page-title= _('Create New Directory')
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
.modal-body
|
.modal-body
|
||||||
= form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'js-create-dir-form js-quick-submit js-requires-input' do
|
= form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'js-create-dir-form js-quick-submit js-requires-input' do
|
||||||
.form-group.row
|
.form-group.row
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.page-title Delete #{@blob.name}
|
%h3.page-title Delete #{@blob.name}
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
|
|
||||||
.modal-body
|
.modal-body
|
||||||
= form_tag project_blob_path(@project, @id), method: :delete, class: 'js-delete-blob-form js-quick-submit js-requires-input' do
|
= form_tag project_blob_path(@project, @id), method: :delete, class: 'js-delete-blob-form js-quick-submit js-requires-input' do
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.page-title= title
|
%h3.page-title= title
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
.modal-body
|
.modal-body
|
||||||
= form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form', data: { method: method } do
|
= form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form', data: { method: method } do
|
||||||
.dropzone
|
.dropzone
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
%span.js-branch-name.ref-name>[branch name]
|
%span.js-branch-name.ref-name>[branch name]
|
||||||
= s_("Branches|Delete protected branch '%{branch_name}'?").html_safe % { branch_name: title_branch_name }
|
= s_("Branches|Delete protected branch '%{branch_name}'?").html_safe % { branch_name: title_branch_name }
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
|
|
||||||
.modal-body
|
.modal-body
|
||||||
%p
|
%p
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
- integrations_link_start = '<a href="%{url}">'.html_safe % { url: help_page_url('user/project/integrations/overview') }
|
- integrations_link_start = '<a href="%{url}">'.html_safe % { url: help_page_url('user/project/integrations/overview') }
|
||||||
- webhooks_link_start = '<a href="%{url}">'.html_safe % { url: project_hooks_path(@project) }
|
- webhooks_link_start = '<a href="%{url}">'.html_safe % { url: project_hooks_path(@project) }
|
||||||
%p= _("%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, webhooks_link_start: webhooks_link_start, link_end: '</a>'.html_safe }
|
%p= _("%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, webhooks_link_start: webhooks_link_start, link_end: '</a>'.html_safe }
|
||||||
= render 'shared/integrations/index', integrations: @services
|
= render 'shared/integrations/index', integrations: @integrations
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.page-title= _('Fork project?')
|
%h3.page-title= _('Fork project?')
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
.modal-body.p-3
|
.modal-body.p-3
|
||||||
%p= _("You can’t %{tag_start}edit%{tag_end} files directly in this project. Fork this project and submit a merge request with your changes.") % { tag_start: '', tag_end: ''}
|
%p= _("You can’t %{tag_start}edit%{tag_end} files directly in this project. Fork this project and submit a merge request with your changes.") % { tag_start: '', tag_end: ''}
|
||||||
.modal-footer
|
.modal-footer
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.page-title= _('Confirmation required')
|
%h3.page-title= _('Confirmation required')
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
|
|
||||||
.modal-body
|
.modal-body
|
||||||
%p.text-danger.js-confirm-text
|
%p.text-danger.js-confirm-text
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.form-group.row.js-template-warning.hidden.js-issuable-template-warning
|
.form-group.row.js-template-warning.hidden.js-issuable-template-warning
|
||||||
.col-sm-12
|
.col-sm-12
|
||||||
.warning_message.mb-0{ role: 'alert' }
|
.warning_message.mb-0{ role: 'alert' }
|
||||||
%btn.js-close-btn.js-dismiss-btn.close{ type: "button", "aria-hidden": true, "aria-label": _("Close") }
|
%btn.js-close-btn.js-dismiss-btn.close{ type: "button", "aria-hidden": "true", "aria-label": _("Close") }
|
||||||
= sprite_icon("close")
|
= sprite_icon("close")
|
||||||
|
|
||||||
%p
|
%p
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.modal-header
|
.modal-header
|
||||||
%h3.page-title= _('Enable Gitpod?')
|
%h3.page-title= _('Enable Gitpod?')
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||||
%span{ "aria-hidden": true } ×
|
%span{ "aria-hidden": "true" } ×
|
||||||
.modal-body.p-3
|
.modal-body.p-3
|
||||||
%p= (_("To use Gitpod you must first enable the feature in the integrations section of your %{user_prefs}.") % { user_prefs: link_to(_('user preferences'), profile_preferences_path(anchor: 'gitpod')) }).html_safe
|
%p= (_("To use Gitpod you must first enable the feature in the integrations section of your %{user_prefs}.") % { user_prefs: link_to(_('user preferences'), profile_preferences_path(anchor: 'gitpod')) }).html_safe
|
||||||
.modal-footer
|
.modal-footer
|
||||||
|
|
|
@ -21,8 +21,6 @@ component may increase reliability and availability through redundancy.
|
||||||
|
|
||||||
When database load balancing is enabled in GitLab, the load is balanced using
|
When database load balancing is enabled in GitLab, the load is balanced using
|
||||||
a simple round-robin algorithm, without any external dependencies such as Redis.
|
a simple round-robin algorithm, without any external dependencies such as Redis.
|
||||||
Load balancing is not enabled for Sidekiq as this would lead to consistency
|
|
||||||
problems, and Sidekiq mostly performs writes anyway.
|
|
||||||
|
|
||||||
In the following image, you can see the load is balanced rather evenly among
|
In the following image, you can see the load is balanced rather evenly among
|
||||||
all the secondaries (`db4`, `db5`, `db6`). Because `SELECT` queries are not
|
all the secondaries (`db4`, `db5`, `db6`). Because `SELECT` queries are not
|
||||||
|
@ -105,6 +103,32 @@ the following. This will balance the load between `host1.example.com` and
|
||||||
|
|
||||||
1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
|
1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
|
||||||
|
|
||||||
|
### Enable the load balancer for Sidekiq
|
||||||
|
|
||||||
|
Sidekiq mostly writes to the database, which means that most of its traffic hits the
|
||||||
|
primary database.
|
||||||
|
|
||||||
|
Some background jobs can use database replicas to read application state.
|
||||||
|
This allows to offload the primary database.
|
||||||
|
|
||||||
|
Load balancing is disabled by default in Sidekiq. When enabled, we can define
|
||||||
|
[the data consistency](../development/sidekiq_style_guide.md#job-data-consistency)
|
||||||
|
requirements for a specific job.
|
||||||
|
|
||||||
|
To enable it, define the `ENABLE_LOAD_BALANCING_FOR_SIDEKIQ` variable to the environment, as shown below.
|
||||||
|
|
||||||
|
For Omnibus installations:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gitlab_rails['env'] = {"ENABLE_LOAD_BALANCING_FOR_SIDEKIQ" => "true"}
|
||||||
|
```
|
||||||
|
|
||||||
|
For installations from source:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export ENABLE_LOAD_BALANCING_FOR_SIDEKIQ="true"
|
||||||
|
```
|
||||||
|
|
||||||
## Service Discovery
|
## Service Discovery
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5883) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.0.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5883) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.0.
|
||||||
|
|
|
@ -4,7 +4,7 @@ group: Distribution
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
# Debugging Tips
|
# Debugging tips **(FREE SELF)**
|
||||||
|
|
||||||
Sometimes things don't work the way they should. Here are some tips on debugging issues out
|
Sometimes things don't work the way they should. Here are some tips on debugging issues out
|
||||||
in production.
|
in production.
|
||||||
|
|
|
@ -5,21 +5,21 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
# Disaster Recovery
|
# Disaster recovery **(FREE SELF)**
|
||||||
|
|
||||||
This document describes a feature that allows to easily disable some important but computationally
|
This document describes a feature that allows you to disable some important but computationally
|
||||||
expensive parts of the application, in order to relieve stress on the database in an ongoing downtime.
|
expensive parts of the application to relieve stress on the database during an ongoing downtime.
|
||||||
|
|
||||||
## `ci_queueing_disaster_recovery`
|
## `ci_queueing_disaster_recovery`
|
||||||
|
|
||||||
This feature flag, if enabled temporarily disables fair scheduling on shared runners.
|
This feature flag, if temporarily enabled, disables fair scheduling on shared runners.
|
||||||
This can help reduce system resource usage on the `jobs/request` endpoint
|
This can help to reduce system resource usage on the `jobs/request` endpoint
|
||||||
by significantly reducing computations being performed.
|
by significantly reducing the computations being performed.
|
||||||
|
|
||||||
Side effects:
|
Side effects:
|
||||||
|
|
||||||
- In case of a large backlog of jobs, the jobs will be processed in the order
|
- In case of a large backlog of jobs, the jobs are processed in the order
|
||||||
they were put in the system instead of balancing the jobs across many projects
|
they were put in the system, instead of balancing the jobs across many projects.
|
||||||
- Projects which are out of quota will be run. This affects
|
- Projects which are out of quota will be run. This affects
|
||||||
only jobs that were created during the last hour, as prior jobs are canceled
|
only jobs created during the last hour, as prior jobs are canceled
|
||||||
by a periodic background worker (`StuckCiJobsWorker`).
|
by a periodic background worker (`StuckCiJobsWorker`).
|
||||||
|
|
|
@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
# Diagnostics tools
|
# Diagnostics tools **(FREE SELF)**
|
||||||
|
|
||||||
These are some of the diagnostics tools the GitLab Support team uses during troubleshooting.
|
These are some of the diagnostics tools the GitLab Support team uses during troubleshooting.
|
||||||
They are listed here for transparency, and they may be useful for users with experience
|
They are listed here for transparency, and they may be useful for users with experience
|
||||||
|
|
|
@ -356,6 +356,16 @@ DeployKeysProject.with_write_access.find_each do |deploy_key_mapping|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Find projects using an SQL query
|
||||||
|
|
||||||
|
Find and store an array of projects based on an SQL query:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Finds projects that end with '%ject'
|
||||||
|
projects = Project.find_by_sql("SELECT * FROM projects WHERE name LIKE '%ject'")
|
||||||
|
=> [#<Project id:12 root/my-first-project>>, #<Project id:13 root/my-second-project>>]
|
||||||
|
```
|
||||||
|
|
||||||
## Wikis
|
## Wikis
|
||||||
|
|
||||||
### Recreate
|
### Recreate
|
||||||
|
@ -709,6 +719,16 @@ emails.each do |e|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Find groups using an SQL query
|
||||||
|
|
||||||
|
Find and store an array of groups based on an SQL query:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Finds groups and subgroups that end with '%oup'
|
||||||
|
Group.find_by_sql("SELECT * FROM namespaces WHERE name LIKE '%oup'")
|
||||||
|
=> [#<Group id:3 @test-group>, #<Group id:4 @template-group/template-subgroup>]
|
||||||
|
```
|
||||||
|
|
||||||
## Routes
|
## Routes
|
||||||
|
|
||||||
### Remove redirecting routes
|
### Remove redirecting routes
|
||||||
|
|
|
@ -4,7 +4,7 @@ group: Distribution
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
# Troubleshooting a GitLab installation
|
# Troubleshooting a GitLab installation **(FREE SELF)**
|
||||||
|
|
||||||
This page documents a collection of resources to help you troubleshoot a GitLab
|
This page documents a collection of resources to help you troubleshoot a GitLab
|
||||||
installation.
|
installation.
|
||||||
|
|
|
@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
# Kubernetes, GitLab and You
|
# Kubernetes, GitLab, and you **(FREE SELF)**
|
||||||
|
|
||||||
This is a list of useful information regarding Kubernetes that the GitLab Support
|
This is a list of useful information regarding Kubernetes that the GitLab Support
|
||||||
Team sometimes uses while troubleshooting. GitLab is making this public, so that anyone
|
Team sometimes uses while troubleshooting. GitLab is making this public, so that anyone
|
||||||
|
|
|
@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
# Linux Cheat Sheet
|
# Linux cheat sheet **(FREE SELF)**
|
||||||
|
|
||||||
This is the GitLab Support Team's collection of information regarding Linux, that they
|
This is the GitLab Support Team's collection of information regarding Linux, that they
|
||||||
sometimes use while troubleshooting. It is listed here for transparency,
|
sometimes use while troubleshooting. It is listed here for transparency,
|
||||||
|
|
|
@ -4,7 +4,7 @@ group: Distribution
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
# Parsing GitLab logs with `jq`
|
# Parsing GitLab logs with `jq` **(FREE SELF)**
|
||||||
|
|
||||||
We recommend using log aggregation and search tools like Kibana and Splunk whenever possible,
|
We recommend using log aggregation and search tools like Kibana and Splunk whenever possible,
|
||||||
but if they are not available you can still quickly parse
|
but if they are not available you can still quickly parse
|
||||||
|
|
|
@ -4,7 +4,7 @@ group: Distribution
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
# Navigating GitLab via Rails console
|
# Navigating GitLab via Rails console **(FREE SELF)**
|
||||||
|
|
||||||
At the heart of GitLab is a web application [built using the Ruby on Rails
|
At the heart of GitLab is a web application [built using the Ruby on Rails
|
||||||
framework](https://about.gitlab.com/blog/2018/10/29/why-we-use-rails-to-build-gitlab/).
|
framework](https://about.gitlab.com/blog/2018/10/29/why-we-use-rails-to-build-gitlab/).
|
||||||
|
|
|
@ -4,7 +4,7 @@ group: Distribution
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
# Troubleshooting Sidekiq
|
# Troubleshooting Sidekiq **(FREE SELF)**
|
||||||
|
|
||||||
Sidekiq is the background job processor GitLab uses to asynchronously run
|
Sidekiq is the background job processor GitLab uses to asynchronously run
|
||||||
tasks. When things go wrong it can be difficult to troubleshoot. These
|
tasks. When things go wrong it can be difficult to troubleshoot. These
|
||||||
|
|
|
@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
# Troubleshooting SSL
|
# Troubleshooting SSL **(FREE SELF)**
|
||||||
|
|
||||||
This page contains a list of common SSL-related errors and scenarios that you
|
This page contains a list of common SSL-related errors and scenarios that you
|
||||||
may encounter while working with GitLab. It should serve as an addition to the
|
may encounter while working with GitLab. It should serve as an addition to the
|
||||||
|
|
|
@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
# Apps for a Testing Environment
|
# Apps for a testing environment **(FREE SELF)**
|
||||||
|
|
||||||
This is the GitLab Support Team's collection of information regarding testing environments,
|
This is the GitLab Support Team's collection of information regarding testing environments,
|
||||||
for use while troubleshooting. It is listed here for transparency, and it may be useful
|
for use while troubleshooting. It is listed here for transparency, and it may be useful
|
||||||
|
|
|
@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
# Finding relevant log entries with a correlation ID
|
# Finding relevant log entries with a correlation ID **(FREE SELF)**
|
||||||
|
|
||||||
In GitLab 11.6 and later, a unique request tracking ID, known as the "correlation ID" has been
|
In GitLab 11.6 and later, a unique request tracking ID, known as the "correlation ID" has been
|
||||||
logged by the GitLab instance for most requests. Each individual request to GitLab gets
|
logged by the GitLab instance for most requests. Each individual request to GitLab gets
|
||||||
|
|
|
@ -154,6 +154,12 @@ A good example of that would be a cache expiration worker.
|
||||||
A job scheduled for an idempotent worker is [deduplicated](#deduplication) when
|
A job scheduled for an idempotent worker is [deduplicated](#deduplication) when
|
||||||
an unstarted job with the same arguments is already in the queue.
|
an unstarted job with the same arguments is already in the queue.
|
||||||
|
|
||||||
|
WARNING:
|
||||||
|
For [data consistency jobs](#job-data-consistency), the deduplication is not compatible with the
|
||||||
|
`data_consistency` attribute set to `:sticky` or `:delayed`.
|
||||||
|
The reason for this is that deduplication always takes into account the latest binary replication pointer into account, not the first one.
|
||||||
|
There is an [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/325291) to improve this.
|
||||||
|
|
||||||
### Ensuring a worker is idempotent
|
### Ensuring a worker is idempotent
|
||||||
|
|
||||||
Make sure the worker tests pass using the following shared example:
|
Make sure the worker tests pass using the following shared example:
|
||||||
|
@ -456,6 +462,68 @@ If we expect an increase of **less than 5%**, then no further action is needed.
|
||||||
Otherwise, please ping `@gitlab-org/scalability` on the merge request and ask
|
Otherwise, please ping `@gitlab-org/scalability` on the merge request and ask
|
||||||
for a review.
|
for a review.
|
||||||
|
|
||||||
|
## Job data consistency
|
||||||
|
|
||||||
|
In order to utilize [Sidekiq read-only database replicas capabilities](../administration/database_load_balancing.md#enable-the-load-balancer-for-sidekiq),
|
||||||
|
set the `data_consistency` attribute of the job to `:always`, `:sticky`, or `:delayed`.
|
||||||
|
|
||||||
|
| **Data Consistency** | **Description** |
|
||||||
|
|--------------|-----------------------------|
|
||||||
|
| `:always` | The job is required to use the primary database (default). |
|
||||||
|
| `:sticky` | The job uses a replica as long as possible. It switches to primary either on write or long replication lag. It should be used on jobs that require to be executed as fast as possible. |
|
||||||
|
| `:delayed` | The job always uses replica, but switches to primary on write. The job is delayed if there's a long replication lag. If the replica is not up-to-date with the next retry, it switches to the primary. It should be used on jobs where we are fine to delay the execution of a given job due to their importance such as expire caches, execute hooks, etc. |
|
||||||
|
|
||||||
|
To set a data consistency for a job, use the `data_consistency` class method:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class DelayedWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
|
||||||
|
data_consistency :delayed
|
||||||
|
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
For [idempotent jobs](#idempotent-jobs), the deduplication is not compatible with the
|
||||||
|
`data_consistency` attribute set to `:sticky` or `:delayed`.
|
||||||
|
The reason for this is that deduplication always takes into account the latest binary replication pointer into account, not the first one.
|
||||||
|
There is an [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/325291) to improve this.
|
||||||
|
|
||||||
|
### `feature_flag` property
|
||||||
|
|
||||||
|
The `feature_flag` property allows you to toggle a job's `data_consistency`,
|
||||||
|
which permits you to safely toggle load balancing capabilities for a specific job.
|
||||||
|
When `feature_flag` is disabled, the job defaults to `:always`, which means that the job will always use the primary database.
|
||||||
|
|
||||||
|
The `feature_flag` property does not allow the use of
|
||||||
|
[feature gates based on actors](../development/feature_flags/index.md).
|
||||||
|
This means that the feature flag cannot be toggled only for particular
|
||||||
|
projects, groups, or users, but instead, you can safely use [percentage of time rollout](../development/feature_flags/index.md).
|
||||||
|
Note that since we check the feature flag on both Sidekiq client and server, rolling out a 10% of the time,
|
||||||
|
will likely results in 1% (0.1 [from client]*0.1 [from server]) of effective jobs using replicas.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class DelayedWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
|
||||||
|
data_consistency :delayed, feature_flag: :load_balancing_for_delayed_worker
|
||||||
|
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delayed job execution
|
||||||
|
|
||||||
|
Scheduling workers that utilize [Sidekiq read-only database replicas capabilities](#job-data-consistency),
|
||||||
|
(workers with `data_consistency` attribute set to `:sticky` or `:delayed`),
|
||||||
|
by calling `SomeWorker.perform_async` results in a worker performing in the future (1 second in the future).
|
||||||
|
|
||||||
|
This way, the replica has a chance to catch up, and the job will likely use the replica.
|
||||||
|
For workers with `data_consistency` set to `:delayed`, it can also reduce the number of retried jobs.
|
||||||
|
|
||||||
## Jobs with External Dependencies
|
## Jobs with External Dependencies
|
||||||
|
|
||||||
Most background jobs in the GitLab application communicate with other GitLab
|
Most background jobs in the GitLab application communicate with other GitLab
|
||||||
|
|
BIN
doc/subscriptions/img/license-file.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
doc/subscriptions/img/license-overview.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
doc/subscriptions/img/publicly-visible.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
doc/subscriptions/img/support-diagram.png
Normal file
After Width: | Height: | Size: 49 KiB |
|
@ -12,8 +12,8 @@ have access to. Subscriptions are valid for 12 months.
|
||||||
|
|
||||||
GitLab provides special subscriptions to participants in:
|
GitLab provides special subscriptions to participants in:
|
||||||
|
|
||||||
- [Education](#gitlab-for-education-subscriptions)
|
- [Education](#gitlab-for-education)
|
||||||
- [Open Source](#gitlab-for-open-source-subscriptions)
|
- [Open Source](#gitlab-for-open-source)
|
||||||
|
|
||||||
## Choose a GitLab subscription
|
## Choose a GitLab subscription
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ click D "./gitlab_com/index.html#view-your-gitlabcom-subscription"
|
||||||
click E "./self_managed/index.html#view-your-subscription"
|
click E "./self_managed/index.html#view-your-subscription"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Customers portal
|
## Customers Portal
|
||||||
|
|
||||||
With the [Customers Portal](https://customers.gitlab.com/) you can:
|
With the [Customers Portal](https://customers.gitlab.com/) you can:
|
||||||
|
|
||||||
|
@ -165,20 +165,102 @@ To change the password for this customers portal account:
|
||||||
1. Make the required changes to the **Your password** section.
|
1. Make the required changes to the **Your password** section.
|
||||||
1. Click **Save changes**.
|
1. Click **Save changes**.
|
||||||
|
|
||||||
## GitLab for Education subscriptions
|
## Community program subscriptions
|
||||||
|
|
||||||
The GitLab Education license can only be used for instructional-use or
|
### GitLab for Education
|
||||||
|
|
||||||
|
For qualifying non-profit educational institutions, the [GitLab for Education](https://about.gitlab.com/solutions/education/) program provides
|
||||||
|
the top GitLab tier, plus 50,000 CI minutes per month.
|
||||||
|
|
||||||
|
The GitLab for Education license can only be used for instructional-use or
|
||||||
non-commercial academic research.
|
non-commercial academic research.
|
||||||
|
|
||||||
Find more information how to apply and renew at
|
Find more information on how to apply and renew at
|
||||||
[GitLab for Education](https://about.gitlab.com/solutions/education/).
|
[GitLab for Education](https://about.gitlab.com/solutions/education/).
|
||||||
|
|
||||||
## GitLab for Open Source subscriptions
|
### GitLab for Open Source
|
||||||
|
|
||||||
|
For qualifying open source projects, the [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/) program provides
|
||||||
|
the top GitLab tier, plus 50,000 CI minutes per month.
|
||||||
|
|
||||||
|
You can find more information about the [program requirements](https://about.gitlab.com/solutions/open-source/join/#requirements),
|
||||||
|
[renewals](https://about.gitlab.com/solutions/open-source/join/$renewals),
|
||||||
|
and benefits on the [GitLab for Open Source application page](https://about.gitlab.com/solutions/open-source/join/).
|
||||||
|
|
||||||
All [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/)
|
|
||||||
requests, including subscription renewals, must be made by using the application process.
|
|
||||||
If you have any questions, send an email to `opensource@gitlab.com` for assistance.
|
If you have any questions, send an email to `opensource@gitlab.com` for assistance.
|
||||||
|
|
||||||
|
#### Verification for Open Source program
|
||||||
|
|
||||||
|
As part of the [application verification process](https://about.gitlab.com/solutions/open-source/join/), you must upload three screenshots.
|
||||||
|
These screenshots are needed to qualify you for the GitLab for Open Source program.
|
||||||
|
|
||||||
|
- [OSI-approved license overview](#license-overview)
|
||||||
|
- [OSI-approved license file](#license-file)
|
||||||
|
- [Publicly visible settings](#publicly-visible-settings)
|
||||||
|
|
||||||
|
##### OSI-approved license
|
||||||
|
|
||||||
|
You must apply an [OSI-approved license](https://opensource.org/licenses/) to each project in your group before you can be verified.
|
||||||
|
|
||||||
|
Add the license to the LICENSE file so that it shows up in the overview section of the project. This allows contributors to see it at a glance.
|
||||||
|
|
||||||
|
It's best to copy and paste the entire license into the file in its original form. GitLab defaults to **All rights reserved** if no license file is mentioned.
|
||||||
|
You must ensure that you add the correct license to each project within your group.
|
||||||
|
|
||||||
|
After you ensure that you are using OSI-approved licenses for your projects, you can take your screenshots.
|
||||||
|
|
||||||
|
###### License overview
|
||||||
|
|
||||||
|
Go to **Project Overview > Details**. Take a screenshot that includes a view of the license you've chosen for your project.
|
||||||
|
|
||||||
|
![License overview](img/license-overview.png)
|
||||||
|
|
||||||
|
###### License file
|
||||||
|
|
||||||
|
Navigate to one of the license files that you uploaded. You can usually find the license file by selecting **Project Overview > Details** and scanning the page for the license.
|
||||||
|
Make sure the screenshot includes the title of the license.
|
||||||
|
|
||||||
|
![License file](img/license-file.png)
|
||||||
|
|
||||||
|
##### Publicly visible settings
|
||||||
|
|
||||||
|
The goal of the GitLab for Open Source program is to enable collaboration on open source projects.
|
||||||
|
As a pre-condition to collaboration, people must be able to view the open source project.
|
||||||
|
As a result, we ask that all projects under this license are publicly visible.
|
||||||
|
|
||||||
|
Follow these instructions to take a screenshot of the publicly visible settings:
|
||||||
|
|
||||||
|
1. Go to your project and select **Settings**.
|
||||||
|
1. Expand **Visibility, project features, permissions**.
|
||||||
|
1. Set **Project Visibility** to **Public**.
|
||||||
|
1. Ensure others can request access by selecting the **Users can request access** checkbox.
|
||||||
|
1. Take the screenshot. Include as much of the publicly visible settings as possible. Make sure to include your project's name in the
|
||||||
|
upper-left of the screenshot.
|
||||||
|
|
||||||
|
![Publicly visible setting](img/publicly-visible.png)
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
From time to time, GitLab allows exceptions. One or two projects within a group can be private if there is a legitimate need for it, for example,
|
||||||
|
if a project holds sensitive data. Email `opensource@gitlab.com` with details of your use case to request written permission for exceptions.
|
||||||
|
|
||||||
|
### GitLab for Startups
|
||||||
|
|
||||||
|
For qualifying startups, the [GitLab for Startups](https://about.gitlab.com/solutions/startups/) program provides
|
||||||
|
the top GitLab tier, plus 50,000 CI minutes per month for 12 months.
|
||||||
|
|
||||||
|
For more information, including program requirements, see the [Startup program's landing page](https://about.gitlab.com/solutions/startups/).
|
||||||
|
|
||||||
|
Send all questions and requests related to the GitLab for Startups program to `startups@gitlab.com`.
|
||||||
|
|
||||||
|
### Support for Community Programs
|
||||||
|
|
||||||
|
Because these Community Programs are free of cost, regular Priority Support is not included. However, it can be purchased at a 95% discount in some cases.
|
||||||
|
If interested, email the relevant community program team: `education@gitlab.com`, `opensource@gitlab.com`, or `startups@gitlab.com`.
|
||||||
|
|
||||||
|
As a community member, you can follow this diagram to find support:
|
||||||
|
|
||||||
|
![Support diagram](img/support-diagram.png)
|
||||||
|
|
||||||
## Contact Support
|
## Contact Support
|
||||||
|
|
||||||
Learn more about:
|
Learn more about:
|
||||||
|
|
|
@ -43,6 +43,10 @@ To make full use of Auto DevOps with Kubernetes, you need:
|
||||||
the NGINX Ingress deployment to be scraped by Prometheus using
|
the NGINX Ingress deployment to be scraped by Prometheus using
|
||||||
`prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
|
`prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
If your cluster is installed on bare metal, see
|
||||||
|
[Auto DevOps Requirements for bare metal](#auto-devops-requirements-for-bare-metal).
|
||||||
|
|
||||||
- **Base domain** (for [Auto Review Apps](stages.md#auto-review-apps),
|
- **Base domain** (for [Auto Review Apps](stages.md#auto-review-apps),
|
||||||
[Auto Deploy](stages.md#auto-deploy), and [Auto Monitoring](stages.md#auto-monitoring))
|
[Auto Deploy](stages.md#auto-deploy), and [Auto Monitoring](stages.md#auto-monitoring))
|
||||||
|
|
||||||
|
@ -149,3 +153,18 @@ specific CI/CD variable.
|
||||||
|
|
||||||
For more details, see [Custom build job for Auto DevOps](../../ci/cloud_deployment/index.md#custom-build-job-for-auto-devops)
|
For more details, see [Custom build job for Auto DevOps](../../ci/cloud_deployment/index.md#custom-build-job-for-auto-devops)
|
||||||
for deployments to AWS EC2.
|
for deployments to AWS EC2.
|
||||||
|
|
||||||
|
## Auto DevOps requirements for bare metal
|
||||||
|
|
||||||
|
According to the [Kubernetes Ingress-NGINX docs](https://kubernetes.github.io/ingress-nginx/deploy/baremetal/):
|
||||||
|
|
||||||
|
> In traditional cloud environments, where network load balancers are available on-demand,
|
||||||
|
a single Kubernetes manifest suffices to provide a single point of contact to the NGINX Ingress
|
||||||
|
controller to external clients and, indirectly, to any application running inside the cluster.
|
||||||
|
Bare-metal environments lack this commodity, requiring a slightly different setup to offer the
|
||||||
|
same kind of access to external consumers.
|
||||||
|
|
||||||
|
The docs linked above explain the issue and present possible solutions, for example:
|
||||||
|
|
||||||
|
- Through [MetalLB](https://github.com/metallb/metallb).
|
||||||
|
- Through [PorterLB](https://github.com/kubesphere/porterlb).
|
||||||
|
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 21 KiB |
|
@ -36,7 +36,7 @@ You can undo changes at any point in this workflow:
|
||||||
- [When you're working locally](#undo-local-changes) and haven't yet pushed to a remote repository.
|
- [When you're working locally](#undo-local-changes) and haven't yet pushed to a remote repository.
|
||||||
- When you have already pushed to a remote repository and you want to:
|
- When you have already pushed to a remote repository and you want to:
|
||||||
- [Keep the history intact](#undo-remote-changes-without-changing-history) (preferred).
|
- [Keep the history intact](#undo-remote-changes-without-changing-history) (preferred).
|
||||||
- [Change the history](#undo-remote-changes-with-modifying-history) (requires
|
- [Change the history](#undo-remote-changes-while-changing-history) (requires
|
||||||
coordination with team and force pushes).
|
coordination with team and force pushes).
|
||||||
|
|
||||||
## Undo local changes
|
## Undo local changes
|
||||||
|
@ -152,7 +152,7 @@ If you want to change to another branch, you can use [`git stash`](https://www.g
|
||||||
|
|
||||||
## Undo committed local changes
|
## Undo committed local changes
|
||||||
|
|
||||||
When you commit to your local repository (`git commit`), the version control system records
|
When you commit to your local repository (`git commit`), Git records
|
||||||
your changes. Because you did not push to a remote repository yet, your changes are
|
your changes. Because you did not push to a remote repository yet, your changes are
|
||||||
not public (or shared with other developers). At this point, you can undo your changes.
|
not public (or shared with other developers). At this point, you can undo your changes.
|
||||||
|
|
||||||
|
@ -218,64 +218,53 @@ which clashes with what other developers have locally.
|
||||||
|
|
||||||
### Undo staged local changes with history modification
|
### Undo staged local changes with history modification
|
||||||
|
|
||||||
You can rewrite history in Git, but you should avoid it, because it can cause problems
|
The following tasks rewrite Git history.
|
||||||
when multiple developers are contributing to the same codebase.
|
|
||||||
|
|
||||||
There is one command for history modification and that is `git rebase`. Command
|
#### Delete a specific commit
|
||||||
provides interactive mode (`-i` flag) which enables you to:
|
|
||||||
|
|
||||||
- **reword** commit messages (there is also `git commit --amend` for editing
|
You can delete a specific commit. For example, if you have
|
||||||
last commit message).
|
commits `A-B-C-D` and you want to delete commit `B`.
|
||||||
- **edit** the commit content (changes introduced by commit) and message.
|
|
||||||
- **squash** multiple commits into a single one, and have a custom or aggregated
|
|
||||||
commit message.
|
|
||||||
- **drop** commits - delete them.
|
|
||||||
- and few more options.
|
|
||||||
|
|
||||||
Let us check few examples. Again there are commits `A-B-C-D` where you want to
|
1. Rebase the range from current commit `D` to `B`:
|
||||||
delete commit `B`.
|
|
||||||
|
|
||||||
- Rebase the range from current commit D to A:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git rebase -i A
|
git rebase -i A
|
||||||
```
|
```
|
||||||
|
|
||||||
- Command opens your favorite editor where you write `drop` in front of commit
|
A list of commits is displayed in your editor.
|
||||||
`B`, but you leave default `pick` with all other commits. Save and exit the
|
|
||||||
editor to perform a rebase. Remember: if you want to cancel delete whole
|
|
||||||
file content before saving and exiting the editor
|
|
||||||
|
|
||||||
In case you want to modify something introduced in commit `B`.
|
1. In front of commit `B`, replace `pick` with `drop`.
|
||||||
|
1. Leave the default, `pick`, for all other commits.
|
||||||
|
1. Save and exit the editor.
|
||||||
|
|
||||||
- Rebase the range from current commit D to A:
|
#### Modify a specific commit
|
||||||
|
|
||||||
|
You can modify a specific commit. For example, if you have
|
||||||
|
commits `A-B-C-D` and you want to modify something introduced in commit `B`.
|
||||||
|
|
||||||
|
1. Rebase the range from current commit `D` to `B`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git rebase -i A
|
git rebase -i A
|
||||||
```
|
```
|
||||||
|
|
||||||
- Command opens your favorite text editor where you write `edit` in front of commit
|
A list of commits is displayed in your editor.
|
||||||
`B`, but leave default `pick` with all other commits. Save and exit the editor to
|
|
||||||
perform a rebase.
|
|
||||||
|
|
||||||
- Now do your edits and commit changes:
|
1. In front of commit `B`, replace `pick` with `edit`.
|
||||||
|
1. Leave the default, `pick`, for all other commits.
|
||||||
|
1. Save and exit the editor.
|
||||||
|
1. Open the file in your editor, make your edits, and commit the changes:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git commit -a
|
git commit -a
|
||||||
```
|
```
|
||||||
|
|
||||||
You can find some more examples in the section explaining
|
|
||||||
[how to modify history](#how-modifying-history-is-done).
|
|
||||||
|
|
||||||
### Redoing the undo
|
### Redoing the undo
|
||||||
|
|
||||||
Sometimes you realize that the changes you undid were useful and you want them
|
You can recall previous local commits. However, not all previous commits are available, because
|
||||||
back. Well because of first paragraph you are in luck. Command `git reflog`
|
Git regularly [cleans the commits that are unreachable by branches or tags](https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery).
|
||||||
enables you to *recall* detached local commits by referencing or applying them
|
|
||||||
via commit ID. Although, do not expect to see really old commits in reflog, because
|
|
||||||
Git regularly [cleans the commits which are *unreachable* by branches or tags](https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery).
|
|
||||||
|
|
||||||
To view repository history and to track older commits you can use below command:
|
To view repository history and track prior commits, run `git reflog show`. For example:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ git reflog show
|
$ git reflog show
|
||||||
|
@ -293,63 +282,46 @@ eb37e74 HEAD@{6}: rebase -i (pick): Commit C
|
||||||
6e43d59 HEAD@{16}: commit: Commit B
|
6e43d59 HEAD@{16}: commit: Commit B
|
||||||
```
|
```
|
||||||
|
|
||||||
Output of command shows repository history. In first column there is commit ID,
|
This output shows the repository history, including:
|
||||||
in following column, number next to `HEAD` indicates how many commits ago something
|
|
||||||
was made, after that indicator of action that was made (commit, rebase, merge, ...)
|
- The commit SHA.
|
||||||
and then on end description of that action.
|
- How many `HEAD`-changing actions ago the commit was made (`HEAD@{12}` was 12 `HEAD`-changing actions ago).
|
||||||
|
- The action that was taken, for example: commit, rebase, merge.
|
||||||
|
- A description of the action that changed `HEAD`.
|
||||||
|
|
||||||
## Undo remote changes without changing history
|
## Undo remote changes without changing history
|
||||||
|
|
||||||
This topic is roughly same as modifying committed local changes without modifying
|
To undo changes in the remote repository, you can create a new commit with the changes you
|
||||||
history. **It should be the preferred way of undoing changes on any remote repository
|
want to undo. You should follow this process, which preserves the history and
|
||||||
or public branch.** Keep in mind that branching is the best solution when you want
|
provides a clear timeline and development structure. However, you only
|
||||||
to retain the history of faulty development, yet start anew from certain point.
|
need to follow this procedure if your work was merged into a branch that
|
||||||
|
other developers are using as the base for their work (for example, `main`).
|
||||||
Branching
|
|
||||||
enables you to include the existing changes in new development (by merging) and
|
|
||||||
it also provides a clear timeline and development structure.
|
|
||||||
|
|
||||||
![Use revert to keep branch flowing](img/revert.png)
|
![Use revert to keep branch flowing](img/revert.png)
|
||||||
|
|
||||||
If you want to revert changes introduced in certain `commit-id`, you can
|
To revert changes introduced in a specific commit `B`:
|
||||||
revert that `commit-id` (swap additions and deletions) in newly created commit:
|
|
||||||
You can do this with
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git revert commit-id
|
git revert B
|
||||||
```
|
```
|
||||||
|
|
||||||
or creating a new branch:
|
## Undo remote changes while changing history
|
||||||
|
|
||||||
```shell
|
You can undo remote changes and change history.
|
||||||
git checkout commit-id
|
|
||||||
git checkout -b new-path-of-feature
|
|
||||||
```
|
|
||||||
|
|
||||||
## Undo remote changes with modifying history
|
Even with an updated history, old commits can still be
|
||||||
|
accessed by commit SHA, at least until all the automated cleanup
|
||||||
This is useful when you want to *hide* certain things - like secret keys,
|
of detached commits is performed, or a cleanup is run manually. Even the cleanup might not remove old commits if there are still refs pointing to them.
|
||||||
passwords, and SSH keys. It is and should not be used to hide mistakes, as
|
|
||||||
it makes it harder to debug in case there are some other bugs. The main
|
|
||||||
reason for this is that you loose the real development progress. Keep in
|
|
||||||
mind that, even with modified history, commits are just detached and can still be
|
|
||||||
accessed through commit ID - at least until all repositories perform
|
|
||||||
the automated cleanup of detached commits.
|
|
||||||
|
|
||||||
![Modifying history causes problems on remote branch](img/rebase_reset.png)
|
![Modifying history causes problems on remote branch](img/rebase_reset.png)
|
||||||
|
|
||||||
### Where modifying history is generally acceptable
|
### When changing history is acceptable
|
||||||
|
|
||||||
Modified history breaks the development chain of other developers, as changed
|
You should not change the history when you're working in a public branch
|
||||||
history does not have matching commit IDs. For that reason it should not be
|
or a branch that might be used by other developers.
|
||||||
used on any public branch or on branch that might be used by other developers.
|
|
||||||
When contributing to big open source repositories (for example, [GitLab](https://gitlab.com/gitlab-org/gitlab/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria)
|
|
||||||
itself), it is acceptable to squash commits into a single one, to present a
|
|
||||||
nicer history of your contribution.
|
|
||||||
|
|
||||||
Keep in mind that this also removes the comments attached to certain commits
|
When you contribute to large open source repositories, like [GitLab](https://gitlab.com/gitlab-org/gitlab),
|
||||||
in merge requests, so if you need to retain traceability in GitLab, then
|
you can squash your commits into a single one.
|
||||||
modifying history is not acceptable.
|
|
||||||
|
|
||||||
A feature branch of a merge request is a public branch and might be used by
|
A feature branch of a merge request is a public branch and might be used by
|
||||||
other developers, but project process and rules might allow or require
|
other developers, but project process and rules might allow or require
|
||||||
|
@ -362,20 +334,12 @@ at merge).
|
||||||
NOTE:
|
NOTE:
|
||||||
Never modify the commit history of your [default branch](../../../user/project/repository/branches/default.md) or shared branch.
|
Never modify the commit history of your [default branch](../../../user/project/repository/branches/default.md) or shared branch.
|
||||||
|
|
||||||
### How modifying history is done
|
### How to change history
|
||||||
|
|
||||||
After you know what you want to modify (how far in history or how which range of
|
You can modify history by using `git rebase -i`. This command allows modification, squashing, deletion
|
||||||
old commits), use `git rebase -i commit-id`. This command displays all the commits from
|
of commits.
|
||||||
current version to chosen commit ID and allow modification, squashing, deletion
|
|
||||||
of that commits.
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ git rebase -i commit1-id..commit3-id
|
|
||||||
pick <commit1-id> <commit1-commit-message>
|
|
||||||
pick <commit2-id> <commit2-commit-message>
|
|
||||||
pick <commit3-id> <commit3-commit-message>
|
|
||||||
|
|
||||||
# Rebase commit1-id..commit3-id onto <commit4-id> (3 command(s))
|
|
||||||
#
|
#
|
||||||
# Commands:
|
# Commands:
|
||||||
# p, pick = use commit
|
# p, pick = use commit
|
||||||
|
@ -388,17 +352,16 @@ pick <commit3-id> <commit3-commit-message>
|
||||||
#
|
#
|
||||||
# These lines can be re-ordered; they are executed from top to bottom.
|
# These lines can be re-ordered; they are executed from top to bottom.
|
||||||
#
|
#
|
||||||
# If you remove a line here THAT COMMIT WILL BE LOST.
|
# If you remove a line THAT COMMIT WILL BE LOST.
|
||||||
#
|
#
|
||||||
# However, if you remove everything, the rebase will be aborted.
|
# However, if you remove everything, the rebase will be aborted.
|
||||||
#
|
#
|
||||||
# Note that empty commits are commented out
|
# Empty commits are commented out
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
The comment from the output clearly states that, if
|
If you decide to abort, do not close your editor, because the history
|
||||||
you decide to abort, don't just close your editor (as that
|
will change. Instead, remove all uncommented lines and save.
|
||||||
modifies history), but remove all uncommented lines and save.
|
|
||||||
|
|
||||||
Use `git rebase` carefully on
|
Use `git rebase` carefully on
|
||||||
shared and remote branches, but rest assured: nothing is broken until
|
shared and remote branches, but rest assured: nothing is broken until
|
||||||
|
|
|
@ -24,6 +24,10 @@ module Gitlab
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def compact
|
||||||
|
Collection.new(select { |variable| !variable.value.nil? })
|
||||||
|
end
|
||||||
|
|
||||||
def concat(resources)
|
def concat(resources)
|
||||||
return self if resources.nil?
|
return self if resources.nil?
|
||||||
|
|
||||||
|
|
|
@ -21803,9 +21803,6 @@ msgstr ""
|
||||||
msgid "NetworkPolicies|Edit policy"
|
msgid "NetworkPolicies|Edit policy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "NetworkPolicies|Editor mode"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "NetworkPolicies|Enforcement status"
|
msgid "NetworkPolicies|Enforcement status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -21905,9 +21902,6 @@ msgstr ""
|
||||||
msgid "NetworkPolicies|Unable to parse policy"
|
msgid "NetworkPolicies|Unable to parse policy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "NetworkPolicies|YAML editor"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "NetworkPolicies|all DNS names"
|
msgid "NetworkPolicies|all DNS names"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
58
rubocop/cop/usage_data/histogram_with_large_table.rb
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module RuboCop
|
||||||
|
module Cop
|
||||||
|
module UsageData
|
||||||
|
# This cop checks that histogram method is not used in usage_data.rb files
|
||||||
|
# for models representing large tables, as defined by migration helpers.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
#
|
||||||
|
# # bad
|
||||||
|
# histogram(Issue, buckets: 1..100)
|
||||||
|
# histogram(User.active, buckets: 1..100)
|
||||||
|
class HistogramWithLargeTable < RuboCop::Cop::Cop
|
||||||
|
MSG = 'Avoid histogram method on %{model_name}'
|
||||||
|
|
||||||
|
# Match one level const as Issue, Gitlab
|
||||||
|
def_node_matcher :one_level_node, <<~PATTERN
|
||||||
|
(send nil? :histogram
|
||||||
|
`(const {nil? cbase} $_)
|
||||||
|
...)
|
||||||
|
PATTERN
|
||||||
|
|
||||||
|
# Match two level const as ::Clusters::Cluster, ::Ci::Pipeline
|
||||||
|
def_node_matcher :two_level_node, <<~PATTERN
|
||||||
|
(send nil? :histogram
|
||||||
|
`(const
|
||||||
|
(const {nil? cbase} $_)
|
||||||
|
$_)
|
||||||
|
...)
|
||||||
|
PATTERN
|
||||||
|
|
||||||
|
def on_send(node)
|
||||||
|
one_level_matches = one_level_node(node)
|
||||||
|
two_level_matches = two_level_node(node)
|
||||||
|
|
||||||
|
return unless Array(one_level_matches).any? || Array(two_level_matches).any?
|
||||||
|
|
||||||
|
class_name = two_level_matches ? two_level_matches.join('::').to_sym : one_level_matches
|
||||||
|
|
||||||
|
if large_table?(class_name)
|
||||||
|
add_offense(node, location: :expression, message: format(MSG, model_name: class_name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def large_table?(model)
|
||||||
|
high_traffic_models.include?(model.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def high_traffic_models
|
||||||
|
cop_config['HighTrafficModels'] || []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -39,6 +39,64 @@ UsageData/LargeTable:
|
||||||
- :arel_table
|
- :arel_table
|
||||||
- :minimum
|
- :minimum
|
||||||
- :maximum
|
- :maximum
|
||||||
|
UsageData/HistogramWithLargeTable:
|
||||||
|
Enabled: true
|
||||||
|
Include:
|
||||||
|
- 'lib/gitlab/usage_data.rb'
|
||||||
|
- 'ee/lib/ee/gitlab/usage_data.rb'
|
||||||
|
HighTrafficModels: &high_traffic_models # models for all high traffic tables in Migration/UpdateLargeTable
|
||||||
|
- 'AuditEvent'
|
||||||
|
- 'Ci::BuildTraceSection'
|
||||||
|
- 'CommitStatus'
|
||||||
|
- 'Ci::Processable'
|
||||||
|
- 'Ci::Bridge'
|
||||||
|
- 'Ci::Build'
|
||||||
|
- 'GenericCommitStatus'
|
||||||
|
- 'Ci::BuildMetadata'
|
||||||
|
- 'Ci::JobArtifact'
|
||||||
|
- 'Ci::PipelineVariable'
|
||||||
|
- 'Ci::Pipeline'
|
||||||
|
- 'Ci::Stage'
|
||||||
|
- 'Deployment'
|
||||||
|
- 'Event'
|
||||||
|
- 'PushEvent'
|
||||||
|
- 'Issue'
|
||||||
|
- 'MergeRequestDiffCommit'
|
||||||
|
- 'MergeRequestDiffFile'
|
||||||
|
- 'MergeRequestDiff'
|
||||||
|
- 'MergeRequest::Metrics'
|
||||||
|
- 'MergeRequest'
|
||||||
|
- 'NamespaceSetting'
|
||||||
|
- 'Namespace'
|
||||||
|
- 'Group'
|
||||||
|
- 'NoteDiffFile'
|
||||||
|
- 'Note'
|
||||||
|
- 'DiffNote'
|
||||||
|
- 'DiscussionNote'
|
||||||
|
- 'SyntheticNote'
|
||||||
|
- 'LabelNote'
|
||||||
|
- 'MilestoneNote'
|
||||||
|
- 'StateNote'
|
||||||
|
- 'LegacyDiffNote'
|
||||||
|
- 'ProjectAuthorization'
|
||||||
|
- 'Project'
|
||||||
|
- 'ProjectCiCdSetting'
|
||||||
|
- 'ProjectSetting'
|
||||||
|
- 'ProjectFeature'
|
||||||
|
- 'ProtectedBranch'
|
||||||
|
- 'ExportedProtectedBranch'
|
||||||
|
- 'PushEventPayload'
|
||||||
|
- 'ResourceLabelEvent'
|
||||||
|
- 'Route'
|
||||||
|
- 'SentNotification'
|
||||||
|
- 'SystemNoteMetadata'
|
||||||
|
- 'ActsAsTaggableOn::Tagging'
|
||||||
|
- 'Todo'
|
||||||
|
- 'User'
|
||||||
|
- 'UserPreference'
|
||||||
|
- 'UserDetail'
|
||||||
|
- 'Vulnerabilities::Finding'
|
||||||
|
- 'WebHookLog'
|
||||||
UsageData/DistinctCountByLargeForeignKey:
|
UsageData/DistinctCountByLargeForeignKey:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Include:
|
Include:
|
||||||
|
|
|
@ -655,21 +655,6 @@ RSpec.describe Projects::BranchesController do
|
||||||
["feature", "improve/awesome", "merge-test", "markdown", "feature_conflict", "'test'"]
|
["feature", "improve/awesome", "merge-test", "markdown", "feature_conflict", "'test'"]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'branch_list_keyset_pagination is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(branch_list_keyset_pagination: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'sets active and stale branches' do
|
|
||||||
expect(assigns[:active_branches].map(&:name)).not_to include(
|
|
||||||
"feature", "improve/awesome", "merge-test", "markdown", "feature_conflict", "'test'"
|
|
||||||
)
|
|
||||||
expect(assigns[:stale_branches].map(&:name)).to eq(
|
|
||||||
["feature", "improve/awesome", "merge-test", "markdown", "feature_conflict", "'test'"]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,12 @@ describe('IssuesListApp component', () => {
|
||||||
dueDate: '2020-12-17',
|
dueDate: '2020-12-17',
|
||||||
startDate: '2020-12-10',
|
startDate: '2020-12-10',
|
||||||
title: 'My milestone',
|
title: 'My milestone',
|
||||||
webPath: '/milestone/webPath',
|
webUrl: '/milestone/webUrl',
|
||||||
},
|
},
|
||||||
dueDate: '2020-12-12',
|
dueDate: '2020-12-12',
|
||||||
|
timeStats: {
|
||||||
humanTimeEstimate: '1w',
|
humanTimeEstimate: '1w',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
|
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
|
||||||
|
@ -54,7 +56,7 @@ describe('IssuesListApp component', () => {
|
||||||
|
|
||||||
expect(milestone.text()).toBe(issue.milestone.title);
|
expect(milestone.text()).toBe(issue.milestone.title);
|
||||||
expect(milestone.find(GlIcon).props('name')).toBe('clock');
|
expect(milestone.find(GlIcon).props('name')).toBe('clock');
|
||||||
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath);
|
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each`
|
describe.each`
|
||||||
|
@ -100,7 +102,7 @@ describe('IssuesListApp component', () => {
|
||||||
|
|
||||||
const timeEstimate = wrapper.find('[data-testid="time-estimate"]');
|
const timeEstimate = wrapper.find('[data-testid="time-estimate"]');
|
||||||
|
|
||||||
expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
|
expect(timeEstimate.text()).toBe(issue.timeStats.humanTimeEstimate);
|
||||||
expect(timeEstimate.attributes('title')).toBe('Estimate');
|
expect(timeEstimate.attributes('title')).toBe('Estimate');
|
||||||
expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
|
expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
|
import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
|
||||||
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
|
import { mount, shallowMount } from '@vue/test-utils';
|
||||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
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 { TEST_HOST } from 'helpers/test_constants';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import {
|
import { apiParams, filteredTokens, locationSearch, urlParams } from 'jest/issues_list/mock_data';
|
||||||
filteredTokens,
|
|
||||||
getIssuesQueryResponse,
|
|
||||||
locationSearch,
|
|
||||||
urlParams,
|
|
||||||
} from 'jest/issues_list/mock_data';
|
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
||||||
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
|
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
|
||||||
|
@ -20,9 +11,13 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
|
||||||
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
|
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
|
||||||
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
|
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
|
||||||
import {
|
import {
|
||||||
|
apiSortParams,
|
||||||
CREATED_DESC,
|
CREATED_DESC,
|
||||||
DUE_DATE_OVERDUE,
|
DUE_DATE_OVERDUE,
|
||||||
|
PAGE_SIZE,
|
||||||
|
PAGE_SIZE_MANUAL,
|
||||||
PARAM_DUE_DATE,
|
PARAM_DUE_DATE,
|
||||||
|
RELATIVE_POSITION_DESC,
|
||||||
TOKEN_TYPE_ASSIGNEE,
|
TOKEN_TYPE_ASSIGNEE,
|
||||||
TOKEN_TYPE_AUTHOR,
|
TOKEN_TYPE_AUTHOR,
|
||||||
TOKEN_TYPE_CONFIDENTIAL,
|
TOKEN_TYPE_CONFIDENTIAL,
|
||||||
|
@ -45,14 +40,12 @@ describe('IssuesListApp component', () => {
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
|
||||||
localVue.use(VueApollo);
|
|
||||||
|
|
||||||
const defaultProvide = {
|
const defaultProvide = {
|
||||||
autocompleteUsersPath: 'autocomplete/users/path',
|
autocompleteUsersPath: 'autocomplete/users/path',
|
||||||
calendarPath: 'calendar/path',
|
calendarPath: 'calendar/path',
|
||||||
canBulkUpdate: false,
|
canBulkUpdate: false,
|
||||||
emptyStateSvgPath: 'empty-state.svg',
|
emptyStateSvgPath: 'empty-state.svg',
|
||||||
|
endpoint: 'api/endpoint',
|
||||||
exportCsvPath: 'export/csv/path',
|
exportCsvPath: 'export/csv/path',
|
||||||
hasBlockedIssuesFeature: true,
|
hasBlockedIssuesFeature: true,
|
||||||
hasIssueWeightsFeature: true,
|
hasIssueWeightsFeature: true,
|
||||||
|
@ -68,6 +61,22 @@ describe('IssuesListApp component', () => {
|
||||||
signInPath: 'sign/in/path',
|
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 findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
|
||||||
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
|
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
|
||||||
const findGlButton = () => wrapper.findComponent(GlButton);
|
const findGlButton = () => wrapper.findComponent(GlButton);
|
||||||
|
@ -77,26 +86,19 @@ describe('IssuesListApp component', () => {
|
||||||
const findGlLink = () => wrapper.findComponent(GlLink);
|
const findGlLink = () => wrapper.findComponent(GlLink);
|
||||||
const findIssuableList = () => wrapper.findComponent(IssuableList);
|
const findIssuableList = () => wrapper.findComponent(IssuableList);
|
||||||
|
|
||||||
const mountComponent = ({
|
const mountComponent = ({ provide = {}, mountFn = shallowMount } = {}) =>
|
||||||
provide = {},
|
mountFn(IssuesListApp, {
|
||||||
response = getIssuesQueryResponse,
|
|
||||||
mountFn = shallowMount,
|
|
||||||
} = {}) => {
|
|
||||||
const requestHandlers = [[getIssuesQuery, jest.fn().mockResolvedValue(response)]];
|
|
||||||
const apolloProvider = createMockApollo(requestHandlers);
|
|
||||||
|
|
||||||
return mountFn(IssuesListApp, {
|
|
||||||
localVue,
|
|
||||||
apolloProvider,
|
|
||||||
provide: {
|
provide: {
|
||||||
...defaultProvide,
|
...defaultProvide,
|
||||||
...provide,
|
...provide,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosMock = new AxiosMockAdapter(axios);
|
axiosMock = new AxiosMockAdapter(axios);
|
||||||
|
axiosMock
|
||||||
|
.onGet(defaultProvide.endpoint)
|
||||||
|
.reply(200, fetchIssuesResponse.data, fetchIssuesResponse.headers);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -106,37 +108,28 @@ describe('IssuesListApp component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('IssuableList', () => {
|
describe('IssuableList', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
jest.runOnlyPendingTimers();
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders', () => {
|
it('renders', () => {
|
||||||
expect(findIssuableList().props()).toMatchObject({
|
expect(findIssuableList().props()).toMatchObject({
|
||||||
namespace: defaultProvide.projectPath,
|
namespace: defaultProvide.projectPath,
|
||||||
recentSearchesStorageKey: 'issues',
|
recentSearchesStorageKey: 'issues',
|
||||||
searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
|
searchInputPlaceholder: 'Search or filter results…',
|
||||||
sortOptions: getSortOptions(true, true),
|
sortOptions: getSortOptions(true, true),
|
||||||
initialSortBy: CREATED_DESC,
|
initialSortBy: CREATED_DESC,
|
||||||
issuables: getIssuesQueryResponse.data.project.issues.nodes,
|
|
||||||
tabs: IssuableListTabs,
|
tabs: IssuableListTabs,
|
||||||
currentTab: IssuableStates.Opened,
|
currentTab: IssuableStates.Opened,
|
||||||
tabCounts: {
|
tabCounts,
|
||||||
opened: 1,
|
showPaginationControls: false,
|
||||||
closed: undefined,
|
issuables: [],
|
||||||
all: undefined,
|
totalItems: xTotal,
|
||||||
},
|
currentPage: xPage,
|
||||||
issuablesLoading: false,
|
previousPage: xPage - 1,
|
||||||
isManualOrdering: false,
|
nextPage: xPage + 1,
|
||||||
showBulkEditSidebar: false,
|
urlParams: { page: xPage, state },
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -164,9 +157,9 @@ describe('IssuesListApp component', () => {
|
||||||
|
|
||||||
describe('csv import/export component', () => {
|
describe('csv import/export component', () => {
|
||||||
describe('when user is signed in', () => {
|
describe('when user is signed in', () => {
|
||||||
const search = '?search=refactor&sort=created_date&state=opened';
|
it('renders', async () => {
|
||||||
|
const search = '?page=1&search=refactor&state=opened&sort=created_date';
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` });
|
global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` });
|
||||||
|
|
||||||
wrapper = mountComponent({
|
wrapper = mountComponent({
|
||||||
|
@ -174,13 +167,11 @@ describe('IssuesListApp component', () => {
|
||||||
mountFn: mount,
|
mountFn: mount,
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.runOnlyPendingTimers();
|
await waitForPromises();
|
||||||
});
|
|
||||||
|
|
||||||
it('renders', () => {
|
|
||||||
expect(findCsvImportExportButtons().props()).toMatchObject({
|
expect(findCsvImportExportButtons().props()).toMatchObject({
|
||||||
exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
|
exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
|
||||||
issuableCount: 1,
|
issuableCount: xTotal,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -198,7 +189,7 @@ describe('IssuesListApp component', () => {
|
||||||
it('renders when user has permissions', () => {
|
it('renders when user has permissions', () => {
|
||||||
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
|
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
|
||||||
|
|
||||||
expect(findGlButtonAt(2).text()).toBe(IssuesListApp.i18n.editIssues);
|
expect(findGlButtonAt(2).text()).toBe('Edit issues');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render when user does not have permissions', () => {
|
it('does not render when user does not have permissions', () => {
|
||||||
|
@ -224,7 +215,7 @@ describe('IssuesListApp component', () => {
|
||||||
it('renders when user has permissions', () => {
|
it('renders when user has permissions', () => {
|
||||||
wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount });
|
wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount });
|
||||||
|
|
||||||
expect(findGlButtonAt(2).text()).toBe(IssuesListApp.i18n.newIssueLabel);
|
expect(findGlButtonAt(2).text()).toBe('New issue');
|
||||||
expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath);
|
expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -247,6 +238,18 @@ 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', () => {
|
describe('search', () => {
|
||||||
it('is set from the url params', () => {
|
it('is set from the url params', () => {
|
||||||
global.jsdom.reconfigure({ url: `${TEST_HOST}${locationSearch}` });
|
global.jsdom.reconfigure({ url: `${TEST_HOST}${locationSearch}` });
|
||||||
|
@ -259,15 +262,13 @@ describe('IssuesListApp component', () => {
|
||||||
|
|
||||||
describe('sort', () => {
|
describe('sort', () => {
|
||||||
it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => {
|
it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => {
|
||||||
global.jsdom.reconfigure({
|
global.jsdom.reconfigure({ url: setUrlParams(urlSortParams[sortKey], TEST_HOST) });
|
||||||
url: setUrlParams({ sort: urlSortParams[sortKey] }, TEST_HOST),
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
|
|
||||||
expect(findIssuableList().props()).toMatchObject({
|
expect(findIssuableList().props()).toMatchObject({
|
||||||
initialSortBy: sortKey,
|
initialSortBy: sortKey,
|
||||||
urlParams: { sort: urlSortParams[sortKey] },
|
urlParams: urlSortParams[sortKey],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -325,10 +326,12 @@ describe('IssuesListApp component', () => {
|
||||||
describe('empty states', () => {
|
describe('empty states', () => {
|
||||||
describe('when there are issues', () => {
|
describe('when there are issues', () => {
|
||||||
describe('when search returns no results', () => {
|
describe('when search returns no results', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
global.jsdom.reconfigure({ url: `${TEST_HOST}?search=no+results` });
|
global.jsdom.reconfigure({ url: `${TEST_HOST}?search=no+results` });
|
||||||
|
|
||||||
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
|
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows empty state', () => {
|
it('shows empty state', () => {
|
||||||
|
@ -341,8 +344,10 @@ describe('IssuesListApp component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when "Open" tab has no issues', () => {
|
describe('when "Open" tab has no issues', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
|
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows empty state', () => {
|
it('shows empty state', () => {
|
||||||
|
@ -355,12 +360,14 @@ describe('IssuesListApp component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when "Closed" tab has no issues', () => {
|
describe('when "Closed" tab has no issues', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
global.jsdom.reconfigure({
|
global.jsdom.reconfigure({
|
||||||
url: setUrlParams({ state: IssuableStates.Closed }, TEST_HOST),
|
url: setUrlParams({ state: IssuableStates.Closed }, TEST_HOST),
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
|
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows empty state', () => {
|
it('shows empty state', () => {
|
||||||
|
@ -529,67 +536,74 @@ describe('IssuesListApp component', () => {
|
||||||
describe('events', () => {
|
describe('events', () => {
|
||||||
describe('when "click-tab" event is emitted by IssuableList', () => {
|
describe('when "click-tab" event is emitted by IssuableList', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
axiosMock.onGet(defaultProvide.endpoint).reply(200, fetchIssuesResponse.data, {
|
||||||
|
'x-page': 2,
|
||||||
|
'x-total': xTotal,
|
||||||
|
});
|
||||||
|
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
|
|
||||||
findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
|
findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates to the new tab', () => {
|
it('makes API call to filter the list by the new state and resets the page to 1', () => {
|
||||||
expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
|
expect(axiosMock.history.get[1].params).toMatchObject({
|
||||||
|
page: 1,
|
||||||
|
state: IssuableStates.Closed,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when "page-change" event is emitted by IssuableList', () => {
|
describe('when "page-change" event is emitted by IssuableList', () => {
|
||||||
beforeEach(() => {
|
const data = [{ id: 10, title: 'title', state }];
|
||||||
wrapper = mountComponent();
|
const page = 2;
|
||||||
|
const totalItems = 21;
|
||||||
|
|
||||||
findIssuableList().vm.$emit('page-change', 2);
|
beforeEach(async () => {
|
||||||
|
axiosMock.onGet(defaultProvide.endpoint).reply(200, data, {
|
||||||
|
'x-page': page,
|
||||||
|
'x-total': totalItems,
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates to the new page', () => {
|
wrapper = mountComponent();
|
||||||
expect(findIssuableList().props('currentPage')).toBe(2);
|
|
||||||
|
findIssuableList().vm.$emit('page-change', page);
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when "reorder" event is emitted by IssuableList', () => {
|
describe('when "reorder" event is emitted by IssuableList', () => {
|
||||||
const issueOne = {
|
const issueOne = { id: 1, iid: 101, title: 'Issue one' };
|
||||||
...getIssuesQueryResponse.data.project.issues.nodes[0],
|
const issueTwo = { id: 2, iid: 102, title: 'Issue two' };
|
||||||
id: 1,
|
const issueThree = { id: 3, iid: 103, title: 'Issue three' };
|
||||||
iid: 101,
|
const issueFour = { id: 4, iid: 104, title: 'Issue four' };
|
||||||
title: 'Issue one',
|
const issues = [issueOne, issueTwo, issueThree, issueFour];
|
||||||
};
|
|
||||||
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(() => {
|
beforeEach(async () => {
|
||||||
wrapper = mountComponent({ response });
|
axiosMock.onGet(defaultProvide.endpoint).reply(200, issues, fetchIssuesResponse.headers);
|
||||||
jest.runOnlyPendingTimers();
|
wrapper = mountComponent();
|
||||||
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when successful', () => {
|
describe('when successful', () => {
|
||||||
|
@ -630,18 +644,21 @@ describe('IssuesListApp component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when "sort" event is emitted by IssuableList', () => {
|
describe('when "sort" event is emitted by IssuableList', () => {
|
||||||
it.each(Object.keys(urlSortParams))(
|
it.each(Object.keys(apiSortParams))(
|
||||||
'updates to the new sort when payload is `%s`',
|
'fetches issues with correct params with payload `%s`',
|
||||||
async (sortKey) => {
|
async (sortKey) => {
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
|
|
||||||
findIssuableList().vm.$emit('sort', sortKey);
|
findIssuableList().vm.$emit('sort', sortKey);
|
||||||
|
|
||||||
jest.runOnlyPendingTimers();
|
await waitForPromises();
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(findIssuableList().props('urlParams')).toMatchObject({
|
expect(axiosMock.history.get[1].params).toEqual({
|
||||||
sort: urlSortParams[sortKey],
|
page: xPage,
|
||||||
|
per_page: sortKey === RELATIVE_POSITION_DESC ? PAGE_SIZE_MANUAL : PAGE_SIZE,
|
||||||
|
state,
|
||||||
|
with_labels_details: true,
|
||||||
|
...apiSortParams[sortKey],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -651,11 +668,13 @@ describe('IssuesListApp component', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
jest.spyOn(eventHub, '$emit');
|
jest.spyOn(eventHub, '$emit');
|
||||||
|
|
||||||
findIssuableList().vm.$emit('update-legacy-bulk-edit');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', () => {
|
it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', async () => {
|
||||||
|
findIssuableList().vm.$emit('update-legacy-bulk-edit');
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
|
expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -667,6 +686,10 @@ describe('IssuesListApp component', () => {
|
||||||
findIssuableList().vm.$emit('filter', filteredTokens);
|
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', () => {
|
it('updates IssuableList with url params', () => {
|
||||||
expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
|
expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,76 +3,6 @@ import {
|
||||||
OPERATOR_IS_NOT,
|
OPERATOR_IS_NOT,
|
||||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
} 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 = [
|
export const locationSearch = [
|
||||||
'?search=find+issues',
|
'?search=find+issues',
|
||||||
'author_username=homer',
|
'author_username=homer',
|
||||||
|
@ -89,8 +19,8 @@ export const locationSearch = [
|
||||||
'not[label_name][]=drama',
|
'not[label_name][]=drama',
|
||||||
'my_reaction_emoji=thumbsup',
|
'my_reaction_emoji=thumbsup',
|
||||||
'confidential=no',
|
'confidential=no',
|
||||||
'iteration_id=4',
|
'iteration_title=season:+%234',
|
||||||
'not[iteration_id]=20',
|
'not[iteration_title]=season:+%2320',
|
||||||
'epic_id=gitlab-org%3A%3A%2612',
|
'epic_id=gitlab-org%3A%3A%2612',
|
||||||
'not[epic_id]=gitlab-org%3A%3A%2634',
|
'not[epic_id]=gitlab-org%3A%3A%2634',
|
||||||
'weight=1',
|
'weight=1',
|
||||||
|
@ -121,8 +51,8 @@ export const filteredTokens = [
|
||||||
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
|
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
|
||||||
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
|
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
|
||||||
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
|
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
|
||||||
{ type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
|
{ type: 'iteration', value: { data: 'season: #4', operator: OPERATOR_IS } },
|
||||||
{ type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
|
{ type: 'iteration', value: { data: 'season: #20', operator: OPERATOR_IS_NOT } },
|
||||||
{ type: 'epic_id', value: { data: 'gitlab-org::&12', operator: OPERATOR_IS } },
|
{ type: 'epic_id', value: { data: 'gitlab-org::&12', operator: OPERATOR_IS } },
|
||||||
{ type: 'epic_id', value: { data: 'gitlab-org::&34', operator: OPERATOR_IS_NOT } },
|
{ type: 'epic_id', value: { data: 'gitlab-org::&34', operator: OPERATOR_IS_NOT } },
|
||||||
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
|
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
|
||||||
|
@ -141,32 +71,30 @@ export const filteredTokensWithSpecialValues = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const apiParams = {
|
export const apiParams = {
|
||||||
authorUsername: 'homer',
|
author_username: 'homer',
|
||||||
assigneeUsernames: ['bart', 'lisa'],
|
'not[author_username]': 'marge',
|
||||||
milestoneTitle: 'season 4',
|
assignee_username: ['bart', 'lisa'],
|
||||||
labelName: ['cartoon', 'tv'],
|
'not[assignee_username]': ['patty', 'selma'],
|
||||||
myReactionEmoji: 'thumbsup',
|
milestone: 'season 4',
|
||||||
|
'not[milestone]': 'season 20',
|
||||||
|
labels: ['cartoon', 'tv'],
|
||||||
|
'not[labels]': ['live action', 'drama'],
|
||||||
|
my_reaction_emoji: 'thumbsup',
|
||||||
confidential: 'no',
|
confidential: 'no',
|
||||||
iterationId: '4',
|
iteration_title: 'season: #4',
|
||||||
epicId: 'gitlab-org::&12',
|
'not[iteration_title]': 'season: #20',
|
||||||
|
epic_id: '12',
|
||||||
|
'not[epic_id]': 'gitlab-org::&34',
|
||||||
weight: '1',
|
weight: '1',
|
||||||
not: {
|
'not[weight]': '3',
|
||||||
authorUsername: 'marge',
|
|
||||||
assigneeUsernames: ['patty', 'selma'],
|
|
||||||
milestoneTitle: 'season 20',
|
|
||||||
labelName: ['live action', 'drama'],
|
|
||||||
iterationId: '20',
|
|
||||||
epicId: 'gitlab-org::&34',
|
|
||||||
weight: '3',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const apiParamsWithSpecialValues = {
|
export const apiParamsWithSpecialValues = {
|
||||||
assigneeId: '123',
|
assignee_id: '123',
|
||||||
assigneeUsernames: 'bart',
|
assignee_username: 'bart',
|
||||||
myReactionEmoji: 'None',
|
my_reaction_emoji: 'None',
|
||||||
iterationWildcardId: 'CURRENT',
|
iteration_id: 'Current',
|
||||||
epicId: 'None',
|
epic_id: 'None',
|
||||||
weight: 'None',
|
weight: 'None',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -181,8 +109,8 @@ export const urlParams = {
|
||||||
'not[label_name][]': ['live action', 'drama'],
|
'not[label_name][]': ['live action', 'drama'],
|
||||||
my_reaction_emoji: 'thumbsup',
|
my_reaction_emoji: 'thumbsup',
|
||||||
confidential: 'no',
|
confidential: 'no',
|
||||||
iteration_id: '4',
|
iteration_title: 'season: #4',
|
||||||
'not[iteration_id]': '20',
|
'not[iteration_title]': 'season: #20',
|
||||||
epic_id: 'gitlab-org%3A%3A%2612',
|
epic_id: 'gitlab-org%3A%3A%2612',
|
||||||
'not[epic_id]': 'gitlab-org::&34',
|
'not[epic_id]': 'gitlab-org::&34',
|
||||||
weight: '1',
|
weight: '1',
|
||||||
|
|
|
@ -8,20 +8,19 @@ import {
|
||||||
urlParams,
|
urlParams,
|
||||||
urlParamsWithSpecialValues,
|
urlParamsWithSpecialValues,
|
||||||
} from 'jest/issues_list/mock_data';
|
} from 'jest/issues_list/mock_data';
|
||||||
import { DUE_DATE_VALUES, urlSortParams } from '~/issues_list/constants';
|
import { API_PARAM, DUE_DATE_VALUES, URL_PARAM, urlSortParams } from '~/issues_list/constants';
|
||||||
import {
|
import {
|
||||||
convertToUrlParams,
|
convertToParams,
|
||||||
convertToSearchQuery,
|
convertToSearchQuery,
|
||||||
getDueDateValue,
|
getDueDateValue,
|
||||||
getFilterTokens,
|
getFilterTokens,
|
||||||
getSortKey,
|
getSortKey,
|
||||||
getSortOptions,
|
getSortOptions,
|
||||||
convertToApiParams,
|
|
||||||
} from '~/issues_list/utils';
|
} from '~/issues_list/utils';
|
||||||
|
|
||||||
describe('getSortKey', () => {
|
describe('getSortKey', () => {
|
||||||
it.each(Object.keys(urlSortParams))('returns %s given the correct inputs', (sortKey) => {
|
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);
|
expect(getSortKey(sort)).toBe(sortKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -81,26 +80,31 @@ describe('getFilterTokens', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('convertToApiParams', () => {
|
describe('convertToParams', () => {
|
||||||
it('returns api params given filtered tokens', () => {
|
it('returns api params given filtered tokens', () => {
|
||||||
expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
|
expect(convertToParams(filteredTokens, API_PARAM)).toEqual({
|
||||||
|
...apiParams,
|
||||||
|
epic_id: 'gitlab-org::&12',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns api params given filtered tokens with special values', () => {
|
it('returns api params given filtered tokens with special values', () => {
|
||||||
expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
|
expect(convertToParams(filteredTokensWithSpecialValues, API_PARAM)).toEqual(
|
||||||
});
|
apiParamsWithSpecialValues,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('convertToUrlParams', () => {
|
|
||||||
it('returns url params given filtered tokens', () => {
|
it('returns url params given filtered tokens', () => {
|
||||||
expect(convertToUrlParams(filteredTokens)).toEqual({
|
expect(convertToParams(filteredTokens, URL_PARAM)).toEqual({
|
||||||
...urlParams,
|
...urlParams,
|
||||||
epic_id: 'gitlab-org::&12',
|
epic_id: 'gitlab-org::&12',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns url params given filtered tokens with special values', () => {
|
it('returns url params given filtered tokens with special values', () => {
|
||||||
expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
|
expect(convertToParams(filteredTokensWithSpecialValues, URL_PARAM)).toEqual(
|
||||||
|
urlParamsWithSpecialValues,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import ReleasesIndexApolloClientApp from '~/releases/components/app_index_apollo
|
||||||
import ReleaseBlock from '~/releases/components/release_block.vue';
|
import ReleaseBlock from '~/releases/components/release_block.vue';
|
||||||
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
|
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
|
||||||
import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
|
import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
|
||||||
|
import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
|
||||||
import { PAGE_SIZE } from '~/releases/constants';
|
import { PAGE_SIZE } from '~/releases/constants';
|
||||||
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
|
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
|
||||||
|
|
||||||
|
@ -29,6 +30,8 @@ describe('app_index_apollo_client.vue', () => {
|
||||||
);
|
);
|
||||||
const projectPath = 'project/path';
|
const projectPath = 'project/path';
|
||||||
const newReleasePath = 'path/to/new/release/page';
|
const newReleasePath = 'path/to/new/release/page';
|
||||||
|
const before = 'beforeCursor';
|
||||||
|
const after = 'afterCursor';
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let allReleasesQueryResponse;
|
let allReleasesQueryResponse;
|
||||||
|
@ -64,6 +67,7 @@ describe('app_index_apollo_client.vue', () => {
|
||||||
const findNewReleaseButton = () =>
|
const findNewReleaseButton = () =>
|
||||||
wrapper.findByText(ReleasesIndexApolloClientApp.i18n.newRelease);
|
wrapper.findByText(ReleasesIndexApolloClientApp.i18n.newRelease);
|
||||||
const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock);
|
const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock);
|
||||||
|
const findPagination = () => wrapper.findComponent(ReleasesPaginationApolloClient);
|
||||||
|
|
||||||
// Expectations
|
// Expectations
|
||||||
const expectLoadingIndicator = () => {
|
const expectLoadingIndicator = () => {
|
||||||
|
@ -119,6 +123,18 @@ describe('app_index_apollo_client.vue', () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const expectPagination = () => {
|
||||||
|
it('renders the pagination buttons', () => {
|
||||||
|
expect(findPagination().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectNoPagination = () => {
|
||||||
|
it('does not render the pagination buttons', () => {
|
||||||
|
expect(findPagination().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('when the component is loading data', () => {
|
describe('when the component is loading data', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -130,6 +146,7 @@ describe('app_index_apollo_client.vue', () => {
|
||||||
expectNoFlashMessage();
|
expectNoFlashMessage();
|
||||||
expectNewReleaseButton();
|
expectNewReleaseButton();
|
||||||
expectReleases(0);
|
expectReleases(0);
|
||||||
|
expectNoPagination();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the data has successfully loaded, but there are no releases', () => {
|
describe('when the data has successfully loaded, but there are no releases', () => {
|
||||||
|
@ -143,6 +160,7 @@ describe('app_index_apollo_client.vue', () => {
|
||||||
expectNoFlashMessage();
|
expectNoFlashMessage();
|
||||||
expectNewReleaseButton();
|
expectNewReleaseButton();
|
||||||
expectReleases(0);
|
expectReleases(0);
|
||||||
|
expectNoPagination();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when an error occurs while loading data', () => {
|
describe('when an error occurs while loading data', () => {
|
||||||
|
@ -155,9 +173,10 @@ describe('app_index_apollo_client.vue', () => {
|
||||||
expectFlashMessage();
|
expectFlashMessage();
|
||||||
expectNewReleaseButton();
|
expectNewReleaseButton();
|
||||||
expectReleases(0);
|
expectReleases(0);
|
||||||
|
expectNoPagination();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the data has successfully loaded', () => {
|
describe('when the data has successfully loaded with a single page of results', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent();
|
createComponent();
|
||||||
});
|
});
|
||||||
|
@ -167,12 +186,24 @@ describe('app_index_apollo_client.vue', () => {
|
||||||
expectNoFlashMessage();
|
expectNoFlashMessage();
|
||||||
expectNewReleaseButton();
|
expectNewReleaseButton();
|
||||||
expectReleases(originalAllReleasesQueryResponse.data.project.releases.nodes.length);
|
expectReleases(originalAllReleasesQueryResponse.data.project.releases.nodes.length);
|
||||||
|
expectNoPagination();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the data has successfully loaded with multiple pages of results', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
allReleasesQueryResponse.data.project.releases.pageInfo.hasNextPage = true;
|
||||||
|
createComponent(Promise.resolve(allReleasesQueryResponse));
|
||||||
|
});
|
||||||
|
|
||||||
|
expectNoLoadingIndicator();
|
||||||
|
expectNoEmptyState();
|
||||||
|
expectNoFlashMessage();
|
||||||
|
expectNewReleaseButton();
|
||||||
|
expectReleases(originalAllReleasesQueryResponse.data.project.releases.nodes.length);
|
||||||
|
expectPagination();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('URL parameters', () => {
|
describe('URL parameters', () => {
|
||||||
const before = 'beforeCursor';
|
|
||||||
const after = 'afterCursor';
|
|
||||||
|
|
||||||
describe('when the URL contains no query parameters', () => {
|
describe('when the URL contains no query parameters', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent();
|
createComponent();
|
||||||
|
@ -241,4 +272,30 @@ describe('app_index_apollo_client.vue', () => {
|
||||||
expect(findNewReleaseButton().attributes().href).toBe(newReleasePath);
|
expect(findNewReleaseButton().attributes().href).toBe(newReleasePath);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pagination', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockQueryParams = { before };
|
||||||
|
|
||||||
|
allReleasesQueryResponse.data.project.releases.pageInfo.hasNextPage = true;
|
||||||
|
createComponent(Promise.resolve(allReleasesQueryResponse));
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requeries the GraphQL endpoint when a pagination button is clicked', async () => {
|
||||||
|
expect(allReleasesQueryMock.mock.calls).toEqual([[expect.objectContaining({ before })]]);
|
||||||
|
|
||||||
|
mockQueryParams = { after };
|
||||||
|
|
||||||
|
findPagination().vm.$emit('next', after);
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(allReleasesQueryMock.mock.calls).toEqual([
|
||||||
|
[expect.objectContaining({ before })],
|
||||||
|
[expect.objectContaining({ after })],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import { historyPushState } from '~/lib/utils/common_utils';
|
||||||
|
import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
|
||||||
|
|
||||||
|
jest.mock('~/lib/utils/common_utils', () => ({
|
||||||
|
...jest.requireActual('~/lib/utils/common_utils'),
|
||||||
|
historyPushState: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('releases_pagination_apollo_client.vue', () => {
|
||||||
|
const startCursor = 'startCursor';
|
||||||
|
const endCursor = 'endCursor';
|
||||||
|
let wrapper;
|
||||||
|
let onPrev;
|
||||||
|
let onNext;
|
||||||
|
|
||||||
|
const createComponent = (pageInfo) => {
|
||||||
|
onPrev = jest.fn();
|
||||||
|
onNext = jest.fn();
|
||||||
|
|
||||||
|
wrapper = mountExtended(ReleasesPaginationApolloClient, {
|
||||||
|
propsData: {
|
||||||
|
pageInfo,
|
||||||
|
},
|
||||||
|
listeners: {
|
||||||
|
prev: onPrev,
|
||||||
|
next: onNext,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const singlePageInfo = {
|
||||||
|
hasPreviousPage: false,
|
||||||
|
hasNextPage: false,
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const onlyNextPageInfo = {
|
||||||
|
hasPreviousPage: false,
|
||||||
|
hasNextPage: true,
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const onlyPrevPageInfo = {
|
||||||
|
hasPreviousPage: true,
|
||||||
|
hasNextPage: false,
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevAndNextPageInfo = {
|
||||||
|
hasPreviousPage: true,
|
||||||
|
hasNextPage: true,
|
||||||
|
startCursor,
|
||||||
|
endCursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPrevButton = () => wrapper.findByTestId('prevButton');
|
||||||
|
const findNextButton = () => wrapper.findByTestId('nextButton');
|
||||||
|
|
||||||
|
describe.each`
|
||||||
|
description | pageInfo | prevEnabled | nextEnabled
|
||||||
|
${'when there is only one page of results'} | ${singlePageInfo} | ${false} | ${false}
|
||||||
|
${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo} | ${false} | ${true}
|
||||||
|
${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo} | ${true} | ${false}
|
||||||
|
${'when there is both a previous and next page'} | ${prevAndNextPageInfo} | ${true} | ${true}
|
||||||
|
`('component states', ({ description, pageInfo, prevEnabled, nextEnabled }) => {
|
||||||
|
describe(description, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent(pageInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`renders the "Prev" button as ${prevEnabled ? 'enabled' : 'disabled'}`, () => {
|
||||||
|
expect(findPrevButton().attributes().disabled).toBe(prevEnabled ? undefined : 'disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`renders the "Next" button as ${nextEnabled ? 'enabled' : 'disabled'}`, () => {
|
||||||
|
expect(findNextButton().attributes().disabled).toBe(nextEnabled ? undefined : 'disabled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('button behavior', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent(prevAndNextPageInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('next button behavior', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
findNextButton().trigger('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits an "next" event with the "after" cursor', () => {
|
||||||
|
expect(onNext.mock.calls).toEqual([[endCursor]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls historyPushState with the new URL', () => {
|
||||||
|
expect(historyPushState.mock.calls).toEqual([
|
||||||
|
[expect.stringContaining(`?after=${endCursor}`)],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prev button behavior', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
findPrevButton().trigger('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits an "prev" event with the "before" cursor', () => {
|
||||||
|
expect(onPrev.mock.calls).toEqual([[startCursor]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls historyPushState with the new URL', () => {
|
||||||
|
expect(historyPushState.mock.calls).toEqual([
|
||||||
|
[expect.stringContaining(`?before=${startCursor}`)],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -302,6 +302,7 @@ RSpec.describe IssuesHelper do
|
||||||
email: current_user&.notification_email,
|
email: current_user&.notification_email,
|
||||||
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
|
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
|
||||||
empty_state_svg_path: '#',
|
empty_state_svg_path: '#',
|
||||||
|
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
|
||||||
export_csv_path: export_csv_project_issues_path(project),
|
export_csv_path: export_csv_project_issues_path(project),
|
||||||
has_project_issues: project_issues(project).exists?.to_s,
|
has_project_issues: project_issues(project).exists?.to_s,
|
||||||
import_csv_issues_path: '#',
|
import_csv_issues_path: '#',
|
||||||
|
|
|
@ -44,6 +44,30 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#compact' do
|
||||||
|
subject do
|
||||||
|
described_class.new
|
||||||
|
.append(key: 'STRING', value: 'string')
|
||||||
|
.append(key: 'NIL', value: nil)
|
||||||
|
.append(key: nil, value: 'string')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a new Collection instance', :aggregate_failures do
|
||||||
|
collection = subject.compact
|
||||||
|
|
||||||
|
expect(collection).to be_an_instance_of(described_class)
|
||||||
|
expect(collection).not_to eql(subject)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects pair that has nil value', :aggregate_failures do
|
||||||
|
collection = subject.compact
|
||||||
|
|
||||||
|
expect(collection).not_to include(key: 'NIL', value: nil, public: true)
|
||||||
|
expect(collection).to include(key: 'STRING', value: 'string', public: true)
|
||||||
|
expect(collection).to include(key: nil, value: 'string', public: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#concat' do
|
describe '#concat' do
|
||||||
it 'appends all elements from an array' do
|
it 'appends all elements from an array' do
|
||||||
collection = described_class.new([{ key: 'VAR_1', value: '1' }])
|
collection = described_class.new([{ key: 'VAR_1', value: '1' }])
|
||||||
|
|
108
spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'fast_spec_helper'
|
||||||
|
|
||||||
|
require_relative '../../../../rubocop/cop/usage_data/histogram_with_large_table'
|
||||||
|
|
||||||
|
RSpec.describe RuboCop::Cop::UsageData::HistogramWithLargeTable do
|
||||||
|
let(:high_traffic_models) { %w[Issue Ci::Build] }
|
||||||
|
let(:msg) { 'Avoid histogram method on' }
|
||||||
|
|
||||||
|
let(:config) do
|
||||||
|
RuboCop::Config.new('UsageData/HistogramWithLargeTable' => {
|
||||||
|
'HighTrafficModels' => high_traffic_models
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:cop) { described_class.new(config) }
|
||||||
|
|
||||||
|
context 'with large tables' do
|
||||||
|
context 'with one-level constants' do
|
||||||
|
context 'when calling histogram(Issue)' do
|
||||||
|
it 'registers an offense' do
|
||||||
|
expect_offense(<<~CODE)
|
||||||
|
histogram(Issue, :project_id, buckets: 1..100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when calling histogram(::Issue)' do
|
||||||
|
it 'registers an offense' do
|
||||||
|
expect_offense(<<~CODE)
|
||||||
|
histogram(::Issue, :project_id, buckets: 1..100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when calling histogram(Issue.closed)' do
|
||||||
|
it 'registers an offense' do
|
||||||
|
expect_offense(<<~CODE)
|
||||||
|
histogram(Issue.closed, :project_id, buckets: 1..100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when calling histogram(::Issue.closed)' do
|
||||||
|
it 'registers an offense' do
|
||||||
|
expect_offense(<<~CODE)
|
||||||
|
histogram(::Issue.closed, :project_id, buckets: 1..100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with two-level constants' do
|
||||||
|
context 'when calling histogram(::Ci::Build)' do
|
||||||
|
it 'registers an offense' do
|
||||||
|
expect_offense(<<~CODE)
|
||||||
|
histogram(::Ci::Build, buckets: 1..100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when calling histogram(::Ci::Build.active)' do
|
||||||
|
it 'registers an offense' do
|
||||||
|
expect_offense(<<~CODE)
|
||||||
|
histogram(::Ci::Build.active, buckets: 1..100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when calling histogram(Ci::Build)' do
|
||||||
|
it 'registers an offense' do
|
||||||
|
expect_offense(<<~CODE)
|
||||||
|
histogram(Ci::Build, buckets: 1..100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when calling histogram(Ci::Build.active)' do
|
||||||
|
it 'registers an offense' do
|
||||||
|
expect_offense(<<~CODE)
|
||||||
|
histogram(Ci::Build.active, buckets: 1..100)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with non related class' do
|
||||||
|
it 'does not register an offense' do
|
||||||
|
expect_no_offenses('histogram(MergeRequest, buckets: 1..100)')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with non related method' do
|
||||||
|
it 'does not register an offense' do
|
||||||
|
expect_no_offenses('count(Issue, buckets: 1..100)')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|