diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-07 21:09:26 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-07 21:09:26 +0000 |
commit | 17c8111494f51e79744c782db023804f5e4a7410 (patch) | |
tree | 8aebe53b8aea72f9d4abac1bf9131203302a5b6e | |
parent | 4b7575da97d88ef4c7b2ec599b0c3fc457b4f561 (diff) | |
download | gitlab-ce-17c8111494f51e79744c782db023804f5e4a7410.tar.gz |
Add latest changes from gitlab-org/gitlab@master
184 files changed, 1646 insertions, 2459 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 904bf117dc0..e527659a939 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -23,6 +23,8 @@ const Api = { projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', projectRunnersPath: '/api/:version/projects/:id/runners', projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches', + projectSearchPath: '/api/:version/projects/:id/search', + projectMilestonesPath: '/api/:version/projects/:id/milestones', mergeRequestsPath: '/api/:version/merge_requests', groupLabelsPath: '/groups/:namespace_path/-/labels', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', @@ -75,13 +77,11 @@ const Api = { const url = Api.buildUrl(Api.groupsPath); return axios .get(url, { - params: Object.assign( - { - search: query, - per_page: DEFAULT_PER_PAGE, - }, - options, - ), + params: { + search: query, + per_page: DEFAULT_PER_PAGE, + ...options, + }, }) .then(({ data }) => { callback(data); @@ -248,6 +248,23 @@ const Api = { .then(({ data }) => data); }, + projectSearch(id, options = {}) { + const url = Api.buildUrl(Api.projectSearchPath).replace(':id', encodeURIComponent(id)); + + return axios.get(url, { + params: { + search: options.search, + scope: options.scope, + }, + }); + }, + + projectMilestones(id) { + const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id)); + + return axios.get(url); + }, + mergeRequests(params = {}) { const url = Api.buildUrl(Api.mergeRequestsPath); @@ -282,7 +299,7 @@ const Api = { }; return axios .get(url, { - params: Object.assign({}, defaults, options), + params: { ...defaults, ...options }, }) .then(({ data }) => callback(data)) .catch(() => flash(__('Something went wrong while fetching projects'))); @@ -365,13 +382,11 @@ const Api = { users(query, options) { const url = Api.buildUrl(this.usersPath); return axios.get(url, { - params: Object.assign( - { - search: query, - per_page: DEFAULT_PER_PAGE, - }, - options, - ), + params: { + search: query, + per_page: DEFAULT_PER_PAGE, + ...options, + }, }); }, @@ -402,7 +417,7 @@ const Api = { }; return axios .get(url, { - params: Object.assign({}, defaults, options), + params: { ...defaults, ...options }, }) .then(({ data }) => callback(data)) .catch(() => flash(__('Something went wrong while fetching projects'))); diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js index d5d8edd5ac0..c35a073b291 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js @@ -22,7 +22,7 @@ function eventHasModifierKeys(event) { export default class ShortcutsBlob extends Shortcuts { constructor(opts) { - const options = Object.assign({}, defaults, opts); + const options = { ...defaults, ...opts }; super(options.skipResetBindings); this.options = options; diff --git a/app/assets/javascripts/blob/blob_fork_suggestion.js b/app/assets/javascripts/blob/blob_fork_suggestion.js index 476b9405a9e..44dfbfcfe1c 100644 --- a/app/assets/javascripts/blob/blob_fork_suggestion.js +++ b/app/assets/javascripts/blob/blob_fork_suggestion.js @@ -17,7 +17,7 @@ const defaults = { class BlobForkSuggestion { constructor(options) { - this.elementMap = Object.assign({}, defaults, options); + this.elementMap = { ...defaults, ...options }; this.onOpenButtonClick = this.onOpenButtonClick.bind(this); this.onCancelButtonClick = this.onCancelButtonClick.bind(this); } diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js index 68ea28e68d9..fceb8c9d48e 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -19,14 +19,15 @@ export function getBoardSortableDefaultOptions(obj) { const touchEnabled = 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch); - const defaultSortOptions = Object.assign({}, sortableConfig, { + const defaultSortOptions = { + ...sortableConfig, filter: '.no-drag', delay: touchEnabled ? 100 : 0, scrollSensitivity: touchEnabled ? 60 : 100, scrollSpeed: 20, onStart: sortableStart, onEnd: sortableEnd, - }); + }; Object.keys(obj).forEach(key => { defaultSortOptions[key] = obj[key]; diff --git a/app/assets/javascripts/close_reopen_report_toggle.js b/app/assets/javascripts/close_reopen_report_toggle.js index 882d20671cc..bcddce6e727 100644 --- a/app/assets/javascripts/close_reopen_report_toggle.js +++ b/app/assets/javascripts/close_reopen_report_toggle.js @@ -2,7 +2,7 @@ import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; // Todo: Remove this when fixing issue in input_setter plugin -const InputSetter = Object.assign({}, ISetter); +const InputSetter = { ...ISetter }; class CloseReopenReportToggle { constructor(opts = {}) { diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index d46525def06..3699a3b8b2b 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -325,7 +325,7 @@ export default class Clusters { handleClusterStatusSuccess(data) { const prevStatus = this.store.state.status; - const prevApplicationMap = Object.assign({}, this.store.state.applications); + const prevApplicationMap = { ...this.store.state.applications }; this.store.updateStateFromServer(data.data); diff --git a/app/assets/javascripts/comment_type_toggle.js b/app/assets/javascripts/comment_type_toggle.js index a259667bb75..2fcd40a901d 100644 --- a/app/assets/javascripts/comment_type_toggle.js +++ b/app/assets/javascripts/comment_type_toggle.js @@ -2,7 +2,7 @@ import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; // Todo: Remove this when fixing issue in input_setter plugin -const InputSetter = Object.assign({}, ISetter); +const InputSetter = { ...ISetter }; class CommentTypeToggle { constructor(opts = {}) { diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index ba585444ba5..801566d2f2f 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -13,7 +13,7 @@ import { import confidentialMergeRequestState from './confidential_merge_request/state'; // Todo: Remove this when fixing issue in input_setter plugin -const InputSetter = Object.assign({}, ISetter); +const InputSetter = { ...ISetter }; const CREATE_MERGE_REQUEST = 'create-mr'; const CREATE_BRANCH = 'create-branch'; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 304a0726597..4f9069f61a5 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -84,7 +84,7 @@ export default { events.forEach(item => { if (!item) return; - const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); + const eventItem = { ...DEFAULT_EVENT_OBJECTS[stage.slug], ...item }; eventItem.totalTime = eventItem.total_time; diff --git a/app/assets/javascripts/design_management/components/design_note_pin.vue b/app/assets/javascripts/design_management/components/design_note_pin.vue index b6b587a12e6..50ea69d52ce 100644 --- a/app/assets/javascripts/design_management/components/design_note_pin.vue +++ b/app/assets/javascripts/design_management/components/design_note_pin.vue @@ -28,9 +28,7 @@ export default { return this.label === null; }, pinStyle() { - return this.repositioning - ? Object.assign({}, this.position, { cursor: 'move' }) - : this.position; + return this.repositioning ? { ...this.position, cursor: 'move' } : this.position; }, pinLabel() { return this.isNewNote diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index b46b8d95d5f..eb4c6683035 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -233,7 +233,7 @@ export function trimFirstCharOfLineContent(line = {}) { // eslint-disable-next-line no-param-reassign delete line.text; - const parsedLine = Object.assign({}, line); + const parsedLine = { ...line }; if (line.rich_text) { const firstChar = parsedLine.rich_text.charAt(0); diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index e07ec693948..1992e753255 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -58,13 +58,14 @@ export default class EnvironmentsStore { let filtered = {}; if (env.size > 1) { - filtered = Object.assign({}, env, { + filtered = { + ...env, isFolder: true, isLoadingFolderContent: oldEnvironmentState.isLoading || false, folderName: env.name, isOpen: oldEnvironmentState.isOpen || false, children: oldEnvironmentState.children || [], - }); + }; } if (env.latest) { @@ -166,7 +167,7 @@ export default class EnvironmentsStore { let updated = env; if (env.latest) { - updated = Object.assign({}, env, env.latest); + updated = { ...env, ...env.latest }; delete updated.latest; } else { updated = env; @@ -192,7 +193,7 @@ export default class EnvironmentsStore { const { environments } = this.state; const updatedEnvironments = environments.map(env => { - const updateEnv = Object.assign({}, env); + const updateEnv = { ...env }; if (env.id === environment.id) { updateEnv[prop] = newValue; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index d051b60814e..161a65c511d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -120,7 +120,7 @@ export default class FilteredSearchDropdownManager { filter: key, }; const extraArguments = mappingKey.extraArguments || {}; - const glArguments = Object.assign({}, defaultArguments, extraArguments); + const glArguments = { ...defaultArguments, ...extraArguments }; // Passing glArguments to `new glClass(<arguments>)` mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))(); diff --git a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js index b3eb0475d6f..cdbc9ec84bd 100644 --- a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js +++ b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js @@ -2,14 +2,12 @@ import { uniq } from 'lodash'; class RecentSearchesStore { constructor(initialState = {}, allowedKeys) { - this.state = Object.assign( - { - isLocalStorageAvailable: true, - recentSearches: [], - allowedKeys, - }, - initialState, - ); + this.state = { + isLocalStorageAvailable: true, + recentSearches: [], + allowedKeys, + ...initialState, + }; } addRecentSearch(newSearch) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 1490498c511..be4b4b5f87d 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -595,13 +595,14 @@ class GitLabDropdown { return renderItem({ instance: this, - options: Object.assign({}, this.options, { + options: { + ...this.options, icon: this.icon, highlight: this.highlight, highlightText: text => this.highlightTextMatches(text, this.filterInput.val()), highlightTemplate: this.highlightTemplate.bind(this), parent, - }), + }, data, group, index, diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index ced10fff129..0b7735a7db9 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -8,7 +8,7 @@ export default class GLForm { constructor(form, enableGFM = {}) { this.form = form; this.textarea = this.form.find('textarea.js-gfm-input'); - this.enableGFM = Object.assign({}, defaultAutocompleteConfig, enableGFM); + this.enableGFM = { ...defaultAutocompleteConfig, ...enableGFM }; // Disable autocomplete for keywords which do not have dataSources available const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {}; Object.keys(this.enableGFM).forEach(item => { diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js index 012177479c6..bb2aea3ea76 100644 --- a/app/assets/javascripts/groups/new_group_child.js +++ b/app/assets/javascripts/groups/new_group_child.js @@ -2,7 +2,7 @@ import { visitUrl } from '../lib/utils/url_utility'; import DropLab from '../droplab/drop_lab'; import ISetter from '../droplab/plugins/input_setter'; -const InputSetter = Object.assign({}, ISetter); +const InputSetter = { ...ISetter }; const NEW_PROJECT = 'new-project'; const NEW_SUBGROUP = 'new-subgroup'; diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue index 195504a6861..70a92b8d3ab 100644 --- a/app/assets/javascripts/ide/components/nav_form.vue +++ b/app/assets/javascripts/ide/components/nav_form.vue @@ -25,13 +25,13 @@ export default { <div class="ide-nav-form p-0"> <tabs v-if="showMergeRequests" stop-propagation> <tab active> - <template slot="title"> + <template #title> {{ __('Branches') }} </template> <branches-search-list /> </tab> <tab> - <template slot="title"> + <template #title> {{ __('Merge Requests') }} </template> <merge-request-search-list /> diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js index 9b7ed68b893..29e29d7fcd3 100644 --- a/app/assets/javascripts/ide/lib/diff/diff.js +++ b/app/assets/javascripts/ide/lib/diff/diff.js @@ -14,13 +14,12 @@ export const computeDiff = (originalContent, newContent) => { endLineNumber: lineNumber + change.count - 1, }); } else if ('added' in change || 'removed' in change) { - acc.push( - Object.assign({}, change, { - lineNumber, - modified: undefined, - endLineNumber: lineNumber + change.count - 1, - }), - ); + acc.push({ + ...change, + lineNumber, + modified: undefined, + endLineNumber: lineNumber + change.count - 1, + }); } if (!change.removed) { diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js index 9230f3839c1..034fdad4305 100644 --- a/app/assets/javascripts/ide/stores/mutations/project.js +++ b/app/assets/javascripts/ide/stores/mutations/project.js @@ -16,9 +16,7 @@ export default { }); Object.assign(state, { - projects: Object.assign({}, state.projects, { - [projectPath]: project, - }), + projects: { ...state.projects, [projectPath]: project }, }); }, [types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) { diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js index 359943b4ab7..c8f14a680c2 100644 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ b/app/assets/javascripts/ide/stores/mutations/tree.js @@ -14,12 +14,13 @@ export default { }, [types.CREATE_TREE](state, { treePath }) { Object.assign(state, { - trees: Object.assign({}, state.trees, { + trees: { + ...state.trees, [treePath]: { tree: [], loading: true, }, - }), + }, }); }, [types.SET_DIRECTORY_DATA](state, { data, treePath }) { diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js index df3d90cff68..deaef686f59 100644 --- a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js +++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js @@ -32,9 +32,7 @@ export function removeCommentIndicator(imageFrameEl) { commentIndicatorEl.remove(); } - return Object.assign({}, meta, { - removed: willRemove, - }); + return { ...meta, removed: willRemove }; } export function showCommentIndicator(imageFrameEl, coordinate) { diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js index a319bcccb8f..74ca907c99f 100644 --- a/app/assets/javascripts/image_diff/helpers/dom_helper.js +++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js @@ -4,12 +4,7 @@ export function setPositionDataAttribute(el, options) { const { x, y, width, height } = options; const { position } = el.dataset; - const positionObject = Object.assign({}, JSON.parse(position), { - x, - y, - width, - height, - }); + const positionObject = { ...JSON.parse(position), x, y, width, height }; el.setAttribute('data-position', JSON.stringify(positionObject)); } diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js index 26c1b0ec7be..89f696dd1d8 100644 --- a/app/assets/javascripts/image_diff/image_diff.js +++ b/app/assets/javascripts/image_diff/image_diff.js @@ -75,9 +75,7 @@ export default class ImageDiff { if (this.renderCommentBadge) { imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options); } else { - const numberBadgeOptions = Object.assign({}, options, { - badgeText: index + 1, - }); + const numberBadgeOptions = { ...options, badgeText: index + 1 }; imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions); } diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index f4030939f2c..0ce8dfe4442 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -220,7 +220,7 @@ export const fetchJobsForStage = ({ dispatch }, stage = {}) => { }, }) .then(({ data }) => { - const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true })); + const retriedJobs = data.retried.map(job => ({ ...job, retried: true })); const jobs = data.latest_statuses.concat(retriedJobs); dispatch('receiveJobsForStageSuccess', jobs); @@ -236,7 +236,7 @@ export const receiveJobsForStageError = ({ commit }) => { export const triggerManualJob = ({ state }, variables) => { const parsedVariables = variables.map(variable => { - const copyVar = Object.assign({}, variable); + const copyVar = { ...variable }; delete copyVar.id; return copyVar; }); diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue new file mode 100644 index 00000000000..19148d6184f --- /dev/null +++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue @@ -0,0 +1,228 @@ +<script> +import { + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownHeader, + GlNewDropdownItem, + GlLoadingIcon, + GlSearchBoxByType, + GlIcon, +} from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; +import Api from '~/api'; +import createFlash from '~/flash'; +import { intersection, debounce } from 'lodash'; + +export default { + components: { + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownHeader, + GlNewDropdownItem, + GlLoadingIcon, + GlSearchBoxByType, + GlIcon, + }, + model: { + prop: 'preselectedMilestones', + event: 'change', + }, + props: { + projectId: { + type: String, + required: true, + }, + preselectedMilestones: { + type: Array, + default: () => [], + required: false, + }, + extraLinks: { + type: Array, + default: () => [], + required: false, + }, + }, + data() { + return { + searchQuery: '', + projectMilestones: [], + searchResults: [], + selectedMilestones: [], + requestCount: 0, + }; + }, + translations: { + milestone: __('Milestone'), + selectMilestone: __('Select milestone'), + noMilestone: __('No milestone'), + noResultsLabel: __('No matching results'), + searchMilestones: __('Search Milestones'), + }, + computed: { + selectedMilestonesLabel() { + if (this.milestoneTitles.length === 1) { + return this.milestoneTitles[0]; + } + + if (this.milestoneTitles.length > 1) { + const firstMilestoneName = this.milestoneTitles[0]; + const numberOfOtherMilestones = this.milestoneTitles.length - 1; + return sprintf(__('%{firstMilestoneName} + %{numberOfOtherMilestones} more'), { + firstMilestoneName, + numberOfOtherMilestones, + }); + } + + return this.$options.translations.noMilestone; + }, + milestoneTitles() { + return this.preselectedMilestones.map(milestone => milestone.title); + }, + dropdownItems() { + return this.searchResults.length ? this.searchResults : this.projectMilestones; + }, + noResults() { + return this.searchQuery.length > 2 && this.searchResults.length === 0; + }, + isLoading() { + return this.requestCount !== 0; + }, + }, + mounted() { + this.fetchMilestones(); + }, + methods: { + fetchMilestones() { + this.requestCount += 1; + + Api.projectMilestones(this.projectId) + .then(({ data }) => { + this.projectMilestones = this.getTitles(data); + this.selectedMilestones = intersection(this.projectMilestones, this.milestoneTitles); + }) + .catch(() => { + createFlash(__('An error occurred while loading milestones')); + }) + .finally(() => { + this.requestCount -= 1; + }); + }, + searchMilestones: debounce(function searchMilestones() { + this.requestCount += 1; + const options = { + search: this.searchQuery, + scope: 'milestones', + }; + + if (this.searchQuery.length < 3) { + this.requestCount -= 1; + this.searchResults = []; + return; + } + + Api.projectSearch(this.projectId, options) + .then(({ data }) => { + const searchResults = this.getTitles(data); + + this.searchResults = searchResults.length ? searchResults : []; + }) + .catch(() => { + createFlash(__('An error occurred while searching for milestones')); + }) + .finally(() => { + this.requestCount -= 1; + }); + }, 100), + toggleMilestoneSelection(clickedMilestone) { + if (!clickedMilestone) return []; + + let milestones = [...this.preselectedMilestones]; + const hasMilestone = this.milestoneTitles.includes(clickedMilestone); + + if (hasMilestone) { + milestones = milestones.filter(({ title }) => title !== clickedMilestone); + } else { + milestones.push({ title: clickedMilestone }); + } + + return milestones; + }, + onMilestoneClicked(clickedMilestone) { + const milestones = this.toggleMilestoneSelection(clickedMilestone); + this.$emit('change', milestones); + + this.selectedMilestones = intersection( + this.projectMilestones, + milestones.map(milestone => milestone.title), + ); + }, + isSelectedMilestone(milestoneTitle) { + return this.selectedMilestones.includes(milestoneTitle); + }, + getTitles(milestones) { + return milestones.filter(({ state }) => state === 'active').map(({ title }) => title); + }, + }, +}; +</script> + +<template> + <gl-new-dropdown> + <template slot="button-content"> + <span ref="buttonText" class="flex-grow-1 ml-1 text-muted">{{ + selectedMilestonesLabel + }}</span> + <gl-icon name="chevron-down" /> + </template> + + <gl-new-dropdown-header> + <span class="text-center d-block">{{ $options.translations.selectMilestone }}</span> + </gl-new-dropdown-header> + + <gl-new-dropdown-divider /> + + <gl-search-box-by-type + v-model.trim="searchQuery" + class="m-2" + :placeholder="this.$options.translations.searchMilestones" + @input="searchMilestones" + /> + + <gl-new-dropdown-item @click="onMilestoneClicked(null)"> + <span :class="{ 'pl-4': true, 'selected-item': selectedMilestones.length === 0 }"> + {{ $options.translations.noMilestone }} + </span> + </gl-new-dropdown-item> + + <gl-new-dropdown-divider /> + + <template v-if="isLoading"> + <gl-loading-icon /> + <gl-new-dropdown-divider /> + </template> + <template v-else-if="noResults"> + <div class="dropdown-item-space"> + <span ref="noResults" class="pl-4">{{ $options.translations.noResultsLabel }}</span> + </div> + <gl-new-dropdown-divider /> + </template> + <template v-else-if="dropdownItems.length"> + <gl-new-dropdown-item + v-for="item in dropdownItems" + :key="item" + role="milestone option" + @click="onMilestoneClicked(item)" + > + <span :class="{ 'pl-4': true, 'selected-item': isSelectedMilestone(item) }"> + {{ item }} + </span> + </gl-new-dropdown-item> + <gl-new-dropdown-divider /> + </template> + + <gl-new-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url"> + <span class="pl-4">{{ item.text }}</span> + </gl-new-dropdown-item> + </gl-new-dropdown> +</template> diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js index ec9c800b7a2..2580f8e86b1 100644 --- a/app/assets/javascripts/mr_notes/init_notes.js +++ b/app/assets/javascripts/mr_notes/init_notes.js @@ -15,6 +15,19 @@ export default () => { notesApp, }, store, + data() { + const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; + const noteableData = JSON.parse(notesDataset.noteableData); + noteableData.noteableType = notesDataset.noteableType; + noteableData.targetType = notesDataset.targetType; + + return { + noteableData, + currentUserData: JSON.parse(notesDataset.currentUserData), + notesData: JSON.parse(notesDataset.notesData), + helpPagePath: notesDataset.helpPagePath, + }; + }, computed: { ...mapGetters(['discussionTabCounter']), ...mapState({ @@ -54,19 +67,6 @@ export default () => { updateDiscussionTabCounter() { this.notesCountBadge.text(this.discussionTabCounter); }, - dataset() { - const data = this.$el.dataset; - const noteableData = JSON.parse(data.noteableData); - noteableData.noteableType = data.noteableType; - noteableData.targetType = data.targetType; - - return { - noteableData, - notesData: JSON.parse(data.notesData), - userData: JSON.parse(data.currentUserData), - helpPagePath: data.helpPagePath, - }; - }, }, render(createElement) { // NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`, @@ -76,8 +76,11 @@ export default () => { return createElement(discussionKeyboardNavigator, [ createElement('notes-app', { props: { - ...this.dataset(), + noteableData: this.noteableData, + notesData: this.notesData, + userData: this.currentUserData, shouldShow: this.isShowTabActive, + helpPagePath: this.helpPagePath, }, }), ]); diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index c1dd56aedf2..faa6006945d 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -230,10 +230,11 @@ export default { const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') }; if (doesHashExistInUrl(constants.NOTE_UNDERSCORE)) { - return Object.assign({}, defaultConfig, { + return { + ...defaultConfig, filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE, persistFilter: false, - }); + }; } return defaultConfig; }, diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 6fd3cee5340..8f9e2359e0d 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -14,36 +14,38 @@ document.addEventListener('DOMContentLoaded', () => { notesApp, }, store, - methods: { - setData() { - const notesDataset = this.$el.dataset; - const parsedUserData = JSON.parse(notesDataset.currentUserData); - const noteableData = JSON.parse(notesDataset.noteableData); - let currentUserData = {}; + data() { + const notesDataset = document.getElementById('js-vue-notes').dataset; + const parsedUserData = JSON.parse(notesDataset.currentUserData); + const noteableData = JSON.parse(notesDataset.noteableData); + let currentUserData = {}; - noteableData.noteableType = notesDataset.noteableType; - noteableData.targetType = notesDataset.targetType; + noteableData.noteableType = notesDataset.noteableType; + noteableData.targetType = notesDataset.targetType; - if (parsedUserData) { - currentUserData = { - id: parsedUserData.id, - name: parsedUserData.name, - username: parsedUserData.username, - avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, - path: parsedUserData.path, - }; - } - - return { - noteableData, - userData: currentUserData, - notesData: JSON.parse(notesDataset.notesData), + if (parsedUserData) { + currentUserData = { + id: parsedUserData.id, + name: parsedUserData.name, + username: parsedUserData.username, + avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, + path: parsedUserData.path, }; - }, + } + + return { + noteableData, + currentUserData, + notesData: JSON.parse(notesDataset.notesData), + }; }, render(createElement) { return createElement('notes-app', { - props: { ...this.setData() }, + props: { + noteableData: this.noteableData, + notesData: this.notesData, + userData: this.currentUserData, + }, }); }, }); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index a358515c2ec..0999d0aa7ac 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -248,7 +248,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const hasQuickActions = utils.hasQuickActions(placeholderText); const replyId = noteData.data.in_reply_to_discussion_id; let methodToDispatch; - const postData = Object.assign({}, noteData); + const postData = { ...noteData }; if (postData.isDraft === true) { methodToDispatch = replyId ? 'batchComments/addDraftToDiscussion' diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index e25f8ab4790..981914dd046 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -99,9 +99,10 @@ export default { // 3. If GitLab user does not have avatar, they might have a Gravatar } else if (this.pipeline.commit.author_gravatar_url) { - commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + commitAuthorInformation = { + ...this.pipeline.commit.author, avatar_url: this.pipeline.commit.author_gravatar_url, - }); + }; } // 4. If committer is not a GitLab User, they can have a Gravatar } else { diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue index 9739ef76867..80a1c83f171 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue @@ -29,7 +29,14 @@ export default { successPercentage() { // Returns a full number when the decimals equal .00. // Otherwise returns a float to two decimal points - return Number(((this.report.success_count / this.report.total_count) * 100 || 0).toFixed(2)); + // Do not include skipped tests as part of the total when doing success calculations. + + const totalCompletedCount = this.report.total_count - this.report.skipped_count; + + if (totalCompletedCount > 0) { + return Number(((this.report.success_count / totalCompletedCount) * 100 || 0).toFixed(2)); + } + return 0; }, formattedDuration() { return formatTime(secondsToMilliseconds(this.report.total_time)); diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index 1ef73760e02..c6f65277c8d 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -15,7 +15,7 @@ export default class PipelineStore { * @param {Object} pipeline */ storePipeline(pipeline = {}) { - const pipelineCopy = Object.assign({}, pipeline); + const pipelineCopy = { ...pipeline }; if (pipelineCopy.triggered_by) { pipelineCopy.triggered_by = [pipelineCopy.triggered_by]; diff --git a/app/assets/javascripts/registry/settings/store/mutations.js b/app/assets/javascripts/registry/settings/store/mutations.js index bb7071b020b..3ba13419b98 100644 --- a/app/assets/javascripts/registry/settings/store/mutations.js +++ b/app/assets/javascripts/registry/settings/store/mutations.js @@ -21,7 +21,7 @@ export default { state.original = Object.freeze(settings); }, [types.RESET_SETTINGS](state) { - state.settings = Object.assign({}, state.original); + state.settings = { ...state.original }; }, [types.TOGGLE_LOADING](state) { state.isLoading = !state.isLoading; diff --git a/app/assets/javascripts/registry/shared/constants.js b/app/assets/javascripts/registry/shared/constants.js index 7a839e4a3ed..4689d01b1c8 100644 --- a/app/assets/javascripts/registry/shared/constants.js +++ b/app/assets/javascripts/registry/shared/constants.js @@ -41,5 +41,5 @@ export const NAME_REGEX_KEEP_LABEL = s__( ); export const NAME_REGEX_KEEP_PLACEHOLDER = ''; export const NAME_REGEX_KEEP_DESCRIPTION = s__( - 'ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported', + 'ContainerRegistry|Regular expressions such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported', ); diff --git a/app/assets/javascripts/releases/components/app_edit.vue b/app/assets/javascripts/releases/components/app_edit.vue index 1a1b2591cc8..433df839acb 100644 --- a/app/assets/javascripts/releases/components/app_edit.vue +++ b/app/assets/javascripts/releases/components/app_edit.vue @@ -9,6 +9,7 @@ import { BACK_URL_PARAM } from '~/releases/constants'; import { getParameterByName } from '~/lib/utils/common_utils'; import AssetLinksForm from './asset_links_form.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue'; export default { name: 'ReleaseEditApp', @@ -18,6 +19,7 @@ export default { GlButton, MarkdownField, AssetLinksForm, + MilestoneCombobox, }, directives: { autofocusonshow, @@ -32,6 +34,10 @@ export default { 'markdownPreviewPath', 'releasesPagePath', 'updateReleaseApiDocsPath', + 'release', + 'newMilestonePath', + 'manageMilestonesPath', + 'projectId', ]), ...mapGetters('detail', ['isValid']), showForm() { @@ -82,6 +88,14 @@ export default { this.updateReleaseNotes(notes); }, }, + releaseMilestones: { + get() { + return this.$store.state.detail.release.milestones; + }, + set(milestones) { + this.updateReleaseMilestones(milestones); + }, + }, cancelPath() { return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath; }, @@ -91,6 +105,18 @@ export default { isSaveChangesDisabled() { return this.isUpdatingRelease || !this.isValid; }, + milestoneComboboxExtraLinks() { + return [ + { + text: __('Create new'), + url: this.newMilestonePath, + }, + { + text: __('Manage milestones'), + url: this.manageMilestonesPath, + }, + ]; + }, }, created() { this.fetchRelease(); @@ -101,6 +127,7 @@ export default { 'updateRelease', 'updateReleaseTitle', 'updateReleaseNotes', + 'updateReleaseMilestones', ]), }, }; @@ -137,6 +164,16 @@ export default { class="form-control" /> </gl-form-group> + <gl-form-group class="w-50"> + <label>{{ __('Milestones') }}</label> + <div class="d-flex flex-column col-md-6 col-sm-10 pl-0"> + <milestone-combobox + v-model="releaseMilestones" + :project-id="projectId" + :extra-links="milestoneComboboxExtraLinks" + /> + </div> + </gl-form-group> <gl-form-group> <label for="release-notes">{{ __('Release notes') }}</label> <div class="bordered-box pr-3 pl-3"> @@ -158,8 +195,7 @@ export default { :placeholder="__('Write your release notes or drag your files here…')" @keydown.meta.enter="updateRelease()" @keydown.ctrl.enter="updateRelease()" - > - </textarea> + ></textarea> </markdown-field> </div> </gl-form-group> @@ -174,12 +210,9 @@ export default { type="submit" :aria-label="__('Save changes')" :disabled="isSaveChangesDisabled" + >{{ __('Save changes') }}</gl-button > - {{ __('Save changes') }} - </gl-button> - <gl-button :href="cancelPath" class="js-cancel-button"> - {{ __('Cancel') }} - </gl-button> + <gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button> </div> </form> </div> diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue index 215a376fc76..67085ecca2b 100644 --- a/app/assets/javascripts/releases/components/app_index.vue +++ b/app/assets/javascripts/releases/components/app_index.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlSkeletonLoading, GlEmptyState, GlLink } from '@gitlab/ui'; +import { GlSkeletonLoading, GlEmptyState, GlLink, GlButton } from '@gitlab/ui'; import { getParameterByName, historyPushState, @@ -18,6 +18,7 @@ export default { ReleaseBlock, TablePagination, GlLink, + GlButton, }, props: { projectId: { @@ -69,14 +70,16 @@ export default { </script> <template> <div class="flex flex-column mt-2"> - <gl-link + <gl-button v-if="newReleasePath" :href="newReleasePath" :aria-describedby="shouldRenderEmptyState && 'releases-description'" - class="btn btn-success align-self-end mb-2 js-new-release-btn" + category="primary" + variant="success" + class="align-self-end mb-2 js-new-release-btn" > {{ __('New release') }} - </gl-link> + </gl-button> <gl-skeleton-loading v-if="isLoading" class="js-loading" /> diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue index 1e703c247ae..ed49841757a 100644 --- a/app/assets/javascripts/releases/components/release_block_header.vue +++ b/app/assets/javascripts/releases/components/release_block_header.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui'; +import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; import { BACK_URL_PARAM } from '~/releases/constants'; import { setUrlParams } from '~/lib/utils/url_utility'; @@ -10,6 +10,7 @@ export default { GlLink, GlBadge, Icon, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -50,14 +51,16 @@ export default { __('Upcoming Release') }}</gl-badge> </h2> - <gl-link + <gl-button v-if="editLink" v-gl-tooltip - class="btn btn-default append-right-10 js-edit-button ml-2" + category="primary" + variant="default" + class="append-right-10 js-edit-button ml-2 pb-2" :title="__('Edit this release')" :href="editLink" > <icon name="pencil" /> - </gl-link> + </gl-button> </div> </template> diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js index 7b84c18242c..3bc427dfa16 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/actions.js +++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js @@ -18,7 +18,12 @@ export const fetchRelease = ({ dispatch, state }) => { return api .release(state.projectId, state.tagName) - .then(({ data: release }) => { + .then(({ data }) => { + const release = { + ...data, + milestones: data.milestones || [], + }; + dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true })); }) .catch(error => { @@ -28,6 +33,8 @@ export const fetchRelease = ({ dispatch, state }) => { export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title); export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes); +export const updateReleaseMilestones = ({ commit }, milestones) => + commit(types.UPDATE_RELEASE_MILESTONES, milestones); export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE); export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => { @@ -45,12 +52,14 @@ export const updateRelease = ({ dispatch, state, getters }) => { dispatch('requestUpdateRelease'); const { release } = state; + const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : []; return ( api .updateRelease(state.projectId, state.tagName, { name: release.name, description: release.description, + milestones, }) /** diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js index 04944b76e42..1d6356990ce 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js +++ b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js @@ -4,6 +4,7 @@ export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR'; export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE'; export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES'; +export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES'; export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE'; export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS'; diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/detail/mutations.js index 3d97e3a75c2..5c29b402cba 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js +++ b/app/assets/javascripts/releases/stores/modules/detail/mutations.js @@ -28,6 +28,10 @@ export default { state.release.description = notes; }, + [types.UPDATE_RELEASE_MILESTONES](state, milestones) { + state.release.milestones = milestones; + }, + [types.REQUEST_UPDATE_RELEASE](state) { state.isUpdatingRelease = true; }, diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/detail/state.js index b513e1bed79..6d0d102c719 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/state.js +++ b/app/assets/javascripts/releases/stores/modules/detail/state.js @@ -6,6 +6,8 @@ export default ({ markdownPreviewPath, updateReleaseApiDocsPath, releaseAssetsDocsPath, + manageMilestonesPath, + newMilestonePath, }) => ({ projectId, tagName, @@ -14,6 +16,8 @@ export default ({ markdownPreviewPath, updateReleaseApiDocsPath, releaseAssetsDocsPath, + manageMilestonesPath, + newMilestonePath, /** The Release object */ release: null, diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index f4e546e4d4e..cf9064aba57 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -13,14 +13,11 @@ Terminal.applyAddon(webLinks); export default class GLTerminal { constructor(element, options = {}) { - this.options = Object.assign( - {}, - { - cursorBlink: true, - screenKeys: true, - }, - options, - ); + this.options = { + cursorBlink: true, + screenKeys: true, + ...options, + }; this.container = element; this.onDispose = []; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue index a95a5a50a8b..f38b66fdfdf 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue @@ -164,7 +164,11 @@ export default { 'js-dropdown-button', 'js-btn-cancel-create', 'js-sidebar-dropdown-toggle', - ].some(className => target?.classList.contains(className)); + ].some( + className => + target?.classList.contains(className) || + target?.parentElement.classList.contains(className), + ); const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some( className => $(target).parents(className).length, diff --git a/app/assets/stylesheets/components/milestone_combobox.scss b/app/assets/stylesheets/components/milestone_combobox.scss new file mode 100644 index 00000000000..e0637088bbb --- /dev/null +++ b/app/assets/stylesheets/components/milestone_combobox.scss @@ -0,0 +1,13 @@ +.selected-item::before { + content: '\f00c'; + color: $green-500; + position: absolute; + left: 16px; + top: 16px; + transform: translateY(-50%); + font: 14px FontAwesome; +} + +.dropdown-item-space { + padding: 8px 12px; +} diff --git a/app/controllers/projects/alert_management_controller.rb b/app/controllers/projects/alert_management_controller.rb index 9be8a89fc02..9c919186c42 100644 --- a/app/controllers/projects/alert_management_controller.rb +++ b/app/controllers/projects/alert_management_controller.rb @@ -3,6 +3,7 @@ class Projects::AlertManagementController < Projects::ApplicationController before_action :ensure_list_feature_enabled, only: :index before_action :ensure_detail_feature_enabled, only: :details + before_action :authorize_read_alert_management_alert! before_action do push_frontend_feature_flag(:alert_list_status_filtering_enabled) end diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb index 061cfb1fde1..a48dadc7fdb 100644 --- a/app/finders/alert_management/alerts_finder.rb +++ b/app/finders/alert_management/alerts_finder.rb @@ -31,7 +31,7 @@ module AlertManagement end def authorized? - Ability.allowed?(current_user, :read_alert_management_alerts, project) + Ability.allowed?(current_user, :read_alert_management_alert, project) end end end diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index eb0d2304ba3..37103c63a3d 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -18,7 +18,7 @@ module Mutations null: true, description: "The alert after mutation" - authorize :update_alert_management_alerts + authorize :update_alert_management_alert private diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index 69fc2718f1e..c0283e6d476 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -6,7 +6,7 @@ module Types graphql_name 'AlertManagementAlert' description "Describes an alert from the project's Alert Management" - authorize :read_alert_management_alerts + authorize :read_alert_management_alert field :iid, GraphQL::ID_TYPE, diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 3fb0e600465..2151d1a85d7 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -448,7 +448,7 @@ module ProjectsHelper clusters: :read_cluster, serverless: :read_cluster, error_tracking: :read_sentry_issue, - alert_management: :read_alert_management, + alert_management: :read_alert_management_alert, labels: :read_label, issues: :read_issue, project_members: :read_project_member, diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb index af51427dc91..1238567a4ed 100644 --- a/app/helpers/releases_helper.rb +++ b/app/helpers/releases_helper.rb @@ -30,7 +30,9 @@ module ReleasesHelper markdown_docs_path: help_page_path('user/markdown'), releases_page_path: project_releases_path(@project, anchor: @release.tag), update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'), - release_assets_docs_path: help_page(anchor: 'release-assets') + release_assets_docs_path: help_page(anchor: 'release-assets'), + manage_milestones_path: project_milestones_path(@project), + new_milestone_path: new_project_milestone_url(@project) } end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 6aa3d791a0f..a8105ae6f7c 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -236,11 +236,8 @@ class ProjectPolicy < BasePolicy enable :read_merge_request enable :read_sentry_issue enable :update_sentry_issue - enable :read_alert_management enable :read_prometheus enable :read_metrics_dashboard_annotation - enable :read_alert_management_alerts - enable :update_alert_management_alerts enable :metrics_dashboard end @@ -306,6 +303,8 @@ class ProjectPolicy < BasePolicy enable :create_metrics_dashboard_annotation enable :delete_metrics_dashboard_annotation enable :update_metrics_dashboard_annotation + enable :read_alert_management_alert + enable :update_alert_management_alert enable :create_design enable :destroy_design end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index c96599f9958..bf21eba28f7 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -6,6 +6,9 @@ class SearchService SEARCH_TERM_LIMIT = 64 SEARCH_CHAR_LIMIT = 4096 + DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE + MAX_PER_PAGE = 200 + def initialize(current_user, params = {}) @current_user = current_user @params = params.dup @@ -60,11 +63,19 @@ class SearchService end def search_objects - @search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page])) + @search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page)) end private + def per_page + per_page_param = params[:per_page].to_i + + return DEFAULT_PER_PAGE unless per_page_param.positive? + + [MAX_PER_PAGE, per_page_param].min + end + def visible_result?(object) return true unless object.respond_to?(:to_ability_name) && DeclarativePolicy.has_policy?(object) @@ -75,13 +86,13 @@ class SearchService results = results_collection.to_a permitted_results = results.select { |object| visible_result?(object) } - filtered_results = (results - permitted_results).each_with_object({}) do |object, memo| + redacted_results = (results - permitted_results).each_with_object({}) do |object, memo| memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name } end - log_redacted_search_results(filtered_results.values) if filtered_results.any? + log_redacted_search_results(redacted_results.values) if redacted_results.any? - return results_collection.id_not_in(filtered_results.keys) if results_collection.is_a?(ActiveRecord::Relation) + return results_collection.id_not_in(redacted_results.keys) if results_collection.is_a?(ActiveRecord::Relation) Kaminari.paginate_array( permitted_results, diff --git a/changelogs/unreleased/207267-expiration-policy-copy.yml b/changelogs/unreleased/207267-expiration-policy-copy.yml new file mode 100644 index 00000000000..9ddff8a76fb --- /dev/null +++ b/changelogs/unreleased/207267-expiration-policy-copy.yml @@ -0,0 +1,5 @@ +--- +title: Update the example regex in the image expiration policy UI +merge_request: 31104 +author: +type: changed diff --git a/changelogs/unreleased/207324-search-api-scoped-to-blobs-does-not-honor-per_page.yml b/changelogs/unreleased/207324-search-api-scoped-to-blobs-does-not-honor-per_page.yml new file mode 100644 index 00000000000..e1e24bfdd7d --- /dev/null +++ b/changelogs/unreleased/207324-search-api-scoped-to-blobs-does-not-honor-per_page.yml @@ -0,0 +1,5 @@ +--- +title: Honor per_page in Search API +merge_request: 29197 +author: +type: fixed diff --git a/changelogs/unreleased/216477-gllink-updates.yml b/changelogs/unreleased/216477-gllink-updates.yml new file mode 100644 index 00000000000..3a786fa479e --- /dev/null +++ b/changelogs/unreleased/216477-gllink-updates.yml @@ -0,0 +1,5 @@ +--- +title: Update style of buttons on the Releases page +merge_request: 31129 +author: Özgür Adem Işıklı @iozguradem +type: changed diff --git a/changelogs/unreleased/39467-allow-a-release-s-associated-milestones-to-be-edited-through-the-ed.yml b/changelogs/unreleased/39467-allow-a-release-s-associated-milestones-to-be-edited-through-the-ed.yml new file mode 100644 index 00000000000..f182418b0e1 --- /dev/null +++ b/changelogs/unreleased/39467-allow-a-release-s-associated-milestones-to-be-edited-through-the-ed.yml @@ -0,0 +1,5 @@ +--- +title: 'Allow to assign milestones to a release on the "Edit Release page"' +merge_request: 28583 +author: +type: added diff --git a/changelogs/unreleased/rw-exclude-skipped-tests-from-success.yml b/changelogs/unreleased/rw-exclude-skipped-tests-from-success.yml new file mode 100644 index 00000000000..c5d348f104e --- /dev/null +++ b/changelogs/unreleased/rw-exclude-skipped-tests-from-success.yml @@ -0,0 +1,4 @@ +--- +title: Changed test success calculation to exclude skipped tests +merge_request: 31154 +type: changed diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index 9019cf0a630..525348464db 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Audit Events **(STARTER)** GitLab offers a way to view the changes made within the GitLab server for owners and administrators on a [paid plan](https://about.gitlab.com/pricing/). diff --git a/doc/administration/logs.md b/doc/administration/logs.md index fd9556278a1..1af3d93edb6 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Log system GitLab has an advanced log system where everything is logged, so you diff --git a/doc/administration/monitoring/prometheus/gitlab_exporter.md b/doc/administration/monitoring/prometheus/gitlab_exporter.md index 9c5c67e7f67..3effca4a2bf 100644 --- a/doc/administration/monitoring/prometheus/gitlab_exporter.md +++ b/doc/administration/monitoring/prometheus/gitlab_exporter.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # GitLab exporter >- Available since [Omnibus GitLab 8.17](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/1132). diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 9391971fd3e..62f9ebd0168 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # GitLab Prometheus metrics >**Note:** diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index 8749e79f3fe..1fd6dfca802 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Monitoring GitLab with Prometheus > **Notes:** diff --git a/doc/administration/monitoring/prometheus/node_exporter.md b/doc/administration/monitoring/prometheus/node_exporter.md index d75b04f1ccd..357303ee4e1 100644 --- a/doc/administration/monitoring/prometheus/node_exporter.md +++ b/doc/administration/monitoring/prometheus/node_exporter.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Node exporter >**Note:** diff --git a/doc/administration/monitoring/prometheus/pgbouncer_exporter.md b/doc/administration/monitoring/prometheus/pgbouncer_exporter.md index ba8e464efcb..92ba2d9bb52 100644 --- a/doc/administration/monitoring/prometheus/pgbouncer_exporter.md +++ b/doc/administration/monitoring/prometheus/pgbouncer_exporter.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # PgBouncer exporter >**Note:** diff --git a/doc/administration/monitoring/prometheus/postgres_exporter.md b/doc/administration/monitoring/prometheus/postgres_exporter.md index 853e3837184..77ca502b21d 100644 --- a/doc/administration/monitoring/prometheus/postgres_exporter.md +++ b/doc/administration/monitoring/prometheus/postgres_exporter.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # PostgreSQL Server Exporter >**Note:** diff --git a/doc/administration/monitoring/prometheus/redis_exporter.md b/doc/administration/monitoring/prometheus/redis_exporter.md index 76f4add0c1b..bef87400f5a 100644 --- a/doc/administration/monitoring/prometheus/redis_exporter.md +++ b/doc/administration/monitoring/prometheus/redis_exporter.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Redis exporter >**Note:** diff --git a/doc/administration/monitoring/prometheus/registry_exporter.md b/doc/administration/monitoring/prometheus/registry_exporter.md index 2e2440389ed..3d28b26b685 100644 --- a/doc/administration/monitoring/prometheus/registry_exporter.md +++ b/doc/administration/monitoring/prometheus/registry_exporter.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Registry exporter > [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/2884) in GitLab 11.9. diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md index 79d4a3328b1..9e52896d013 100644 --- a/doc/administration/reference_architectures/index.md +++ b/doc/administration/reference_architectures/index.md @@ -50,7 +50,8 @@ For different cloud vendors, attempt to select options that best match the provi ## Up to 1,000 users -From 1 to 1,000 users, a [single-node setup with frequent backups](#automated-backups-core-only) is adequate. +> - **Supported users (approximate):** 1,000 +> - **High Availability:** False | Users | Configuration([8](#footnotes)) | GCP type | AWS type([9](#footnotes)) | |-------|--------------------------------|---------------|---------------------------| @@ -58,9 +59,20 @@ From 1 to 1,000 users, a [single-node setup with frequent backups](#automated-ba | 500 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | | 1000 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge | -This solution is appropriate for many teams that have a single server at their disposal. With automatic backup of the GitLab repositories, configuration, and the database, this can be an optimal solution if you don't have strict availability requirements. +For situations where you need to serve up to 1,000 users, a single-node +solution with [frequent backups](#automated-backups-core-only) is appropriate +for many organizations. With automatic backup of the GitLab repositories, +configuration, and the database, if you don't have strict availability +requirements, this is the ideal solution. + +For this default reference architecture, use the standard +[installation instructions](../../install/README.md) to install GitLab. -You can also optionally configure GitLab to use an [external PostgreSQL service](../external_database.md) or an [external object storage service](../high_availability/object_storage.md) for added performance and reliability at a relatively low complexity cost. +NOTE: **Note:** +You can also optionally configure GitLab to use an +[external PostgreSQL service](../external_database.md) or an +[external object storage service](../high_availability/object_storage.md) for +added performance and reliability at a reduced complexity cost. ## Up to 2,000 users @@ -70,12 +82,32 @@ You can also optionally configure GitLab to use an [external PostgreSQL service] | Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) | |--------------------------------------------------------------|-------|---------------------------------|---------------|----------------------------| -| GitLab Rails, Sidekiq, Consul ([1](#footnotes)) | 2 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge | -| PostgreSQL | 1 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | -| Gitaly ([2](#footnotes)) ([5](#footnotes)) ([7](#footnotes)) | X | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | -| Cloud Object Storage ([4](#footnotes)) | - | - | - | - | -| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | | External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | +| Object Storage ([4](#footnotes)) | - | - | - | - | +| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | +| PostgreSQL | 1 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | +| Redis ([3](#footnotes)) | 1 | 1 vCPU, 3.75GB Memory | n1-standard-1 | m5.large | +| Gitaly ([5](#footnotes)) ([7](#footnotes)) | X ([2](#footnotes)) | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | +| GitLab Rails ([1](#footnotes)) | 2 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge | +| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | + +To set up GitLab for up to 2000 users: + +1. [Configure the external load balancing node](../high_availability/load_balancer.md) + that will handle the load balancing of the two GitLab application services nodes. +1. [Configure the Object Storage](../object_storage.md) ([4](#footnotes)) used for shared data objects. +1. (Optional) [Configure NFS](../high_availability/nfs.md) to have + shared disk storage service as an alternative to Gitaly and/or + [Object Storage](../object_storage.md) (although not recommended). + NFS is required for GitLab Pages, you can skip this step if you're not using that feature. +1. [Configure PostgreSQL](../high_availability/load_balancer.md), the database for GitLab. +1. [Configure Redis](../high_availability/redis.md). +1. [Configure Gitaly](../gitaly/index.md#running-gitaly-on-its-own-server), + which is used to provide access to the Git repositories. +1. [Configure the main GitLab Rails application](../high_availability/gitlab.md) + to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all + frontend requests (UI, API, Git over HTTP/SSH). +1. [Configure Prometheus](../high_availability/monitoring_node.md) to monitor your GitLab environment. ## Up to 3,000 users @@ -83,9 +115,7 @@ NOTE: **Note:** The 3,000-user reference architecture documented below is designed to help your organization achieve a highly-available GitLab deployment. If you do not have the expertise or need to maintain a highly-available environment, you can have a simpler and less costly-to-operate environment by -deploying two or more GitLab Rails servers, external load balancing, an NFS -server, a PostgreSQL server and a Redis server. A reference architecture with -this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/quality/performance/-/issues/223). +following the [2,000-user reference architecture](#up-to-2000-users). > - **Supported users (approximate):** 3,000 > - **High Availability:** True @@ -100,7 +130,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual | Redis ([3](#footnotes)) | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | | Consul + Sentinel ([3](#footnotes)) | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | | Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | -| Cloud Object Storage ([4](#footnotes)) | - | - | - | - | +| Object Storage ([4](#footnotes)) | - | - | - | - | | NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | | Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | | External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | @@ -121,7 +151,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual | Redis ([3](#footnotes)) | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | | Consul + Sentinel ([3](#footnotes)) | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | | Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | -| Cloud Object Storage ([4](#footnotes)) | - | - | - | - | +| Object Storage ([4](#footnotes)) | - | - | - | - | | NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | | Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | | External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | @@ -145,7 +175,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual | Redis Sentinel ([3](#footnotes)) - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small | | Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | | Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | -| Cloud Object Storage ([4](#footnotes)) | - | - | - | - | +| Object Storage ([4](#footnotes)) | - | - | - | - | | NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | | Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | | External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | @@ -169,7 +199,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual | Redis Sentinel ([3](#footnotes)) - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small | | Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | | Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | -| Cloud Object Storage ([4](#footnotes)) | - | - | - | - | +| Object Storage ([4](#footnotes)) | - | - | - | - | | NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | | Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | | External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | @@ -194,7 +224,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual | Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | | Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | | NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | -| Cloud Object Storage ([4](#footnotes)) | - | - | - | - | +| Object Storage ([4](#footnotes)) | - | - | - | - | | Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | | External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | | Internal load balancing node ([6](#footnotes)) | 1 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge | @@ -286,7 +316,7 @@ column. | Component | Description | Configuration instructions | Bundled with Omnibus GitLab | |-----------|-------------|----------------------------| | Load balancer(s) ([6](#footnotes)) | Handles load balancing, typically when you have multiple GitLab application services nodes | [Load balancer configuration](../high_availability/load_balancer.md) ([6](#footnotes)) | No | -| Object storage service ([4](#footnotes)) | Recommended store for shared data objects | [Cloud Object Storage configuration](../object_storage.md) | No | +| Object storage service ([4](#footnotes)) | Recommended store for shared data objects | [Object Storage configuration](../object_storage.md) | No | | NFS ([5](#footnotes)) ([7](#footnotes)) | Shared disk storage service. Can be used as an alternative for Gitaly or Object Storage. Required for GitLab Pages | [NFS configuration](../high_availability/nfs.md) | No | | [Consul](../../development/architecture.md#consul) ([3](#footnotes)) | Service discovery and health checks/failover | [Consul HA configuration](../high_availability/consul.md) **(PREMIUM ONLY)** | Yes | | [PostgreSQL](../../development/architecture.md#postgresql) | Database | [PostgreSQL configuration](https://docs.gitlab.com/omnibus/settings/database.html) | Yes | @@ -316,14 +346,15 @@ column. with a review of expected data size and spread based on the recommendations above. 1. Recommended Redis setup differs depending on the size of the architecture. - For smaller architectures (less than 5,000 users), we suggest one Redis cluster for all + For smaller architectures (less than 3,000 users) a single instance should suffice. + For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all classes and that Redis Sentinel is hosted alongside Consul. For larger architectures (10,000 users or more) we suggest running a separate [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class and another for the Queues and Shared State classes respectively. We also recommend that you run the Redis Sentinel clusters separately for each Redis Cluster. -1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend a [Cloud Object Storage service](../object_storage.md) +1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md) over NFS where possible, due to better performance and availability. 1. NFS can be used as an alternative for both repository data (replacing Gitaly) and diff --git a/doc/api/projects.md b/doc/api/projects.md index aa5e9ef6e43..beb69e1aeee 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -798,7 +798,9 @@ GET /projects/:id "enabled": false, "keep_n": null, "older_than": null, - "name_regex": null, + "name_regex": null, // to be deprecated in GitLab 13.0 in favor of `name_regex_delete` + "name_regex_delete": null, + "name_regex_keep": null, "next_run_at": "2020-01-07T21:42:58.658Z" }, "created_at": "2013-09-30T13:46:02Z", @@ -1033,7 +1035,7 @@ POST /projects | `emails_disabled` | boolean | no | Disable email notifications | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | -| `container_expiration_policy_attributes` | hash | no | Update the container expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `enabled` (boolean) | +| `container_expiration_policy_attributes` | hash | no | Update the image expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean) | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `visibility` | string | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | @@ -1170,7 +1172,7 @@ PUT /projects/:id | `emails_disabled` | boolean | no | Disable email notifications | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | -| `container_expiration_policy_attributes` | hash | no | Update the container expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `enabled` (boolean) | +| `container_expiration_policy_attributes` | hash | no | Update the image expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean) | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `visibility` | string | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 07bf6bf5fbf..cd9ebc71ba7 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -468,13 +468,13 @@ When a merge request author has been blocked for longer than the `Review-response` SLO, they are free to remind the reviewer through Slack or assign another reviewer. -#### Customer critical merge requests +### Customer critical merge requests A merge request may benefit from being considered a customer critical priority because there is a significant benefit to the business in doing so. Properties of customer critical merge requests: -- The [Senior Director of Development](https://about.gitlab.com/job-families/engineering/engineering-management/#senior-director-engineering) [@clefelhocz1](https://gitlab.com/clefelhocz1) is the DRI for deciding if a merge request will be customer critical. +- The [Senior Director of Development](https://about.gitlab.com/job-families/engineering/engineering-management/#senior-director-engineering) ([@clefelhocz1](https://gitlab.com/clefelhocz1)) is the DRI for deciding if a merge request will be customer critical. - The DRI will assign the `customer-critical-merge-request` label to the merge request. - It is required that the reviewer(s) and maintainer(s) involved with a customer critical merge request are engaged as soon as this decision is made. - It is required to prioritize work for those involved on a customer critical merge request so that they have the time available necessary to focus on it. diff --git a/doc/user/incident_management/index.md b/doc/user/incident_management/index.md index 8b898a7b516..d8294d35d2a 100644 --- a/doc/user/incident_management/index.md +++ b/doc/user/incident_management/index.md @@ -1,5 +1,8 @@ --- description: "GitLab - Incident Management. GitLab offers solutions for handling incidents in your applications and services" +stage: Monitor +group: Health +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/#designated-technical-writers --- # Incident Management diff --git a/doc/user/packages/container_registry/img/expiration_policy_app_v13_0.png b/doc/user/packages/container_registry/img/expiration_policy_app_v13_0.png Binary files differindex a38014a2b9b..81e516e833b 100644 --- a/doc/user/packages/container_registry/img/expiration_policy_app_v13_0.png +++ b/doc/user/packages/container_registry/img/expiration_policy_app_v13_0.png diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 5ad476c9510..7603e99e578 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -505,6 +505,7 @@ then goes through a process of excluding tags from it until only the ones to be 1. Orders the remaining tags by `created_date`. 1. Excludes from the list the N tags based on the `keep_n` value (Number of tags to retain). 1. Excludes from the list the tags older than the `older_than` value (Expiration interval). +1. Excludes from the list any tags matching the `name_regex_keep` value (Images to preserve). 1. Finally, the remaining tags in the list are deleted from the Container Registry. ### Managing project expiration policy through the UI @@ -520,6 +521,7 @@ The UI allows you to configure the following: - **Expiration schedule:** how often the cron job checking the tags should run. - **Number of tags to retain:** how many tags to _always_ keep for each image. - **Docker tags with names matching this regex pattern will expire:** the regex used to determine what tags should be expired. To qualify all tags for expiration, use the default value of `.*`. +- **Docker tags with names matching this regex pattern will be preserved:** the regex used to determine what tags should be preserved. To preserve all tags, use the default value of `.*`. ### Managing project expiration policy through the API @@ -527,16 +529,10 @@ You can set, update, and disable the expiration policies using the GitLab API. Examples: -- Select all tags, keep at least 1 tag per image, expire any tag older than 14 days, run once a month, and the policy is enabled: +- Select all tags, keep at least 1 tag per image, expire any tag older than 14 days, run once a month, preserve any images with the name `master` and the policy is enabled: ```shell - curl --request PUT --header 'Content-Type: application/json;charset=UTF-8' --header "PRIVATE-TOKEN: <your_access_token>" --data-binary '{"container_expiration_policy_attributes":{"cadence":"1month","enabled":true,"keep_n":1,"older_than":"14d","name_regex":".*"}}' 'https://gitlab.example.com/api/v4/projects/2' - ``` - -- Select only tags with a name that contains `stable`, keep at least 50 tag per image, expire any tag older than 7 days, run every day, and the policy is enabled: - - ```shell - curl --request PUT --header 'Content-Type: application/json;charset=UTF-8' --header "PRIVATE-TOKEN: <your_access_token>" --data-binary '{"container_expiration_policy_attributes":{"cadence":"1day","enabled":true,"keep_n":50"older_than":"7d","name_regex":"*stable"}}' 'https://gitlab.example.com/api/v4/projects/2' + curl --request PUT --header 'Content-Type: application/json;charset=UTF-8' --header "PRIVATE-TOKEN: <your_access_token>" --data-binary '{"container_expiration_policy_attributes":{"cadence":"1month","enabled":true,"keep_n":1,"older_than":"14d","name_regex":"","name_regex_delete":".*","name_regex_keep":".*-master"}}' 'https://gitlab.example.com/api/v4/projects/2' ``` See the API documentation for further details: [Edit project](../../../api/projects.md#edit-project). diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index bb8cefd755d..51fe3fbf168 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Kubernetes clusters > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/35954) in GitLab 10.1 for projects. diff --git a/doc/user/project/integrations/generic_alerts.md b/doc/user/project/integrations/generic_alerts.md index 1a000fd1c44..f89922edb7a 100644 --- a/doc/user/project/integrations/generic_alerts.md +++ b/doc/user/project/integrations/generic_alerts.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: Health +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/#designated-technical-writers +--- + # Generic alerts integration > - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13203) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.4. diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 4a4c536c80f..8fca670eb41 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Prometheus integration > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8935) in GitLab 9.0. diff --git a/doc/user/project/integrations/prometheus_library/cloudwatch.md b/doc/user/project/integrations/prometheus_library/cloudwatch.md index 143130aebea..911493cdae9 100644 --- a/doc/user/project/integrations/prometheus_library/cloudwatch.md +++ b/doc/user/project/integrations/prometheus_library/cloudwatch.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Monitoring AWS Resources > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12621) in GitLab 9.4 diff --git a/doc/user/project/integrations/prometheus_library/haproxy.md b/doc/user/project/integrations/prometheus_library/haproxy.md index fa3590af8cf..712805b75f2 100644 --- a/doc/user/project/integrations/prometheus_library/haproxy.md +++ b/doc/user/project/integrations/prometheus_library/haproxy.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Monitoring HAProxy > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12621) in GitLab 9.4 diff --git a/doc/user/project/integrations/prometheus_library/index.md b/doc/user/project/integrations/prometheus_library/index.md index c2b3676b23f..6f2c2477eee 100644 --- a/doc/user/project/integrations/prometheus_library/index.md +++ b/doc/user/project/integrations/prometheus_library/index.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Prometheus Metrics library > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8935) in GitLab 9.0. diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index ca1555c793b..29efe08e53d 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Monitoring Kubernetes > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8935) in GitLab 9.0. diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md index d5f078f3ddf..eda6f64ccac 100644 --- a/doc/user/project/integrations/prometheus_library/nginx.md +++ b/doc/user/project/integrations/prometheus_library/nginx.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Monitoring NGINX > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12621) in GitLab 9.4 diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md index 62f8c08e298..b2bc217e8bf 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Monitoring NGINX Ingress Controller > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22133) in GitLab 11.7. diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md index af3b725deb6..6ba0a7610f6 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Monitoring NGINX Ingress Controller with VTS metrics > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13438) in GitLab 9.5. diff --git a/doc/user/project/integrations/prometheus_units.md b/doc/user/project/integrations/prometheus_units.md index d649a14ecf2..691d20e5de2 100644 --- a/doc/user/project/integrations/prometheus_units.md +++ b/doc/user/project/integrations/prometheus_units.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Unit formats reference > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/201999) in GitLab 12.9. diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md index fde4f7e64b3..23a50fd7766 100644 --- a/doc/user/project/operations/error_tracking.md +++ b/doc/user/project/operations/error_tracking.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: Health +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/#designated-technical-writers +--- + # Error Tracking > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/169) in GitLab 11.8. diff --git a/doc/user/project/operations/tracing.md b/doc/user/project/operations/tracing.md index 8282a980ced..07f60c37f1b 100644 --- a/doc/user/project/operations/tracing.md +++ b/doc/user/project/operations/tracing.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: APM +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/#designated-technical-writers +--- + # Tracing **(ULTIMATE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/7903) in GitLab Ultimate 11.5. diff --git a/doc/user/project/status_page/index.md b/doc/user/project/status_page/index.md index 02570ff912c..1acf978c81d 100644 --- a/doc/user/project/status_page/index.md +++ b/doc/user/project/status_page/index.md @@ -1,3 +1,9 @@ +--- +stage: Monitor +group: Health +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/#designated-technical-writers +--- + # GitLab Status Page > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2479) in GitLab 12.10. diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb index 8597903ad00..eb4361cdc53 100644 --- a/lib/gitlab/group_search_results.rb +++ b/lib/gitlab/group_search_results.rb @@ -4,8 +4,8 @@ module Gitlab class GroupSearchResults < SearchResults attr_reader :group - def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20) - super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page) + def initialize(current_user, limit_projects, group, query, default_project_filter: false) + super(current_user, limit_projects, query, default_project_filter: default_project_filter) @group = group end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index eb7ca80dd60..123dcf79065 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -2,30 +2,29 @@ module Gitlab class ProjectSearchResults < SearchResults - attr_reader :project, :repository_ref, :per_page + attr_reader :project, :repository_ref - def initialize(current_user, project, query, repository_ref = nil, per_page: 20) + def initialize(current_user, project, query, repository_ref = nil) @current_user = current_user @project = project @repository_ref = repository_ref.presence @query = query - @per_page = per_page end - def objects(scope, page = nil) + def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE) case scope when 'notes' notes.page(page).per(per_page) when 'blobs' - paginated_blobs(blobs(page), page) + paginated_blobs(blobs(limit: limit_up_to_page(page, per_page)), page, per_page) when 'wiki_blobs' - paginated_blobs(wiki_blobs, page) + paginated_blobs(wiki_blobs(limit: limit_up_to_page(page, per_page)), page, per_page) when 'commits' Kaminari.paginate_array(commits).page(page).per(per_page) when 'users' users.page(page).per(per_page) else - super(scope, page, false) + super(scope, page: page, per_page: per_page, without_count: false) end end @@ -49,7 +48,7 @@ module Gitlab end def limited_blobs_count - @limited_blobs_count ||= blobs.count + @limited_blobs_count ||= blobs(limit: count_limit).count end # rubocop: disable CodeReuse/ActiveRecord @@ -69,7 +68,7 @@ module Gitlab # rubocop: enable CodeReuse/ActiveRecord def wiki_blobs_count - @wiki_blobs_count ||= wiki_blobs.count + @wiki_blobs_count ||= wiki_blobs(limit: count_limit).count end def commits_count @@ -87,7 +86,7 @@ module Gitlab private - def paginated_blobs(blobs, page) + def paginated_blobs(blobs, page, per_page) results = Kaminari.paginate_array(blobs).page(page).per(per_page) Gitlab::Search::FoundBlob.preload_blobs(results) @@ -95,19 +94,19 @@ module Gitlab results end - def limit_up_to_page(page) + def limit_up_to_page(page, per_page) current_page = page&.to_i || 1 offset = per_page * (current_page - 1) count_limit + offset end - def blobs(page = 1) + def blobs(limit: count_limit) return [] unless Ability.allowed?(@current_user, :download_code, @project) - @blobs ||= Gitlab::FileFinder.new(project, repository_project_ref).find(query, content_match_cutoff: limit_up_to_page(page)) + @blobs ||= Gitlab::FileFinder.new(project, repository_project_ref).find(query, content_match_cutoff: limit) end - def wiki_blobs + def wiki_blobs(limit: count_limit) return [] unless Ability.allowed?(@current_user, :read_wiki, @project) @wiki_blobs ||= begin @@ -115,7 +114,7 @@ module Gitlab if project.wiki.empty? [] else - Gitlab::WikiFileFinder.new(project, repository_wiki_ref).find(query) + Gitlab::WikiFileFinder.new(project, repository_wiki_ref).find(query, content_match_cutoff: limit) end else [] diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 0473fa89a0d..c35ee62163a 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -4,8 +4,10 @@ module Gitlab class SearchResults COUNT_LIMIT = 100 COUNT_LIMIT_MESSAGE = "#{COUNT_LIMIT - 1}+" + DEFAULT_PAGE = 1 + DEFAULT_PER_PAGE = 20 - attr_reader :current_user, :query, :per_page + attr_reader :current_user, :query # Limit search results by passed projects # It allows us to search only for projects user has access to @@ -17,15 +19,14 @@ module Gitlab # query attr_reader :default_project_filter - def initialize(current_user, limit_projects, query, default_project_filter: false, per_page: 20) + def initialize(current_user, limit_projects, query, default_project_filter: false) @current_user = current_user @limit_projects = limit_projects || Project.all @query = query @default_project_filter = default_project_filter - @per_page = per_page end - def objects(scope, page = nil, without_count = true) + def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, without_count: true) collection = case scope when 'projects' projects @@ -39,7 +40,9 @@ module Gitlab users else Kaminari.paginate_array([]) - end.page(page).per(per_page) + end + + collection = collection.page(page).per(per_page) without_count ? collection.without_count : collection end diff --git a/lib/gitlab/sidekiq_logging/json_formatter.rb b/lib/gitlab/sidekiq_logging/json_formatter.rb index 45c6842c59b..64782e1e1d1 100644 --- a/lib/gitlab/sidekiq_logging/json_formatter.rb +++ b/lib/gitlab/sidekiq_logging/json_formatter.rb @@ -19,6 +19,7 @@ module Gitlab output[:message] = data when Hash convert_to_iso8601!(data) + convert_retry_to_integer!(data) stringify_args!(data) output.merge!(data) end @@ -41,6 +42,20 @@ module Gitlab Time.at(timestamp).utc.iso8601(3) end + def convert_retry_to_integer!(payload) + payload['retry'] = + case payload['retry'] + when Integer + payload['retry'] + when false, nil + 0 + when true + Sidekiq::JobRetry::DEFAULT_MAX_RETRY_ATTEMPTS + else + -1 + end + end + def stringify_args!(payload) payload['args'] = Gitlab::Utils::LogLimitedArray.log_limited_array(payload['args'].map(&:to_s)) if payload['args'] end diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index 7fcae04c14f..9911f9e62a6 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -11,8 +11,8 @@ module Gitlab @query = query end - def objects(scope, page = nil) - paginated_objects(snippet_titles, page) + def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE) + paginated_objects(snippet_titles, page, per_page) end def formatted_count(scope) @@ -38,7 +38,7 @@ module Gitlab snippets.search(query) end - def paginated_objects(relation, page) + def paginated_objects(relation, page, per_page) relation.page(page).per(per_page) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 11bebabf468..7cec0bbee5b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -321,6 +321,9 @@ msgstr "" msgid "%{firstLabel} +%{labelCount} more" msgstr "" +msgid "%{firstMilestoneName} + %{numberOfOtherMilestones} more" +msgstr "" + msgid "%{global_id} is not a valid id for %{expected_type}." msgstr "" @@ -2141,6 +2144,9 @@ msgstr "" msgid "An error occurred while loading merge requests." msgstr "" +msgid "An error occurred while loading milestones" +msgstr "" + msgid "An error occurred while loading terraform report" msgstr "" @@ -2216,6 +2222,9 @@ msgstr "" msgid "An error occurred while saving the template. Please check if the template exists." msgstr "" +msgid "An error occurred while searching for milestones" +msgstr "" + msgid "An error occurred while subscribing to notifications." msgstr "" @@ -5710,7 +5719,7 @@ msgstr "" msgid "ContainerRegistry|Quick Start" msgstr "" -msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported" +msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported" msgstr "" msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}" @@ -6213,6 +6222,9 @@ msgstr "" msgid "Create milestone" msgstr "" +msgid "Create new" +msgstr "" + msgid "Create new board" msgstr "" @@ -6889,11 +6901,6 @@ msgid_plural "Dependencies|%d additional vulnerabilities not shown" msgstr[0] "" msgstr[1] "" -msgid "Dependencies|%d vulnerability" -msgid_plural "Dependencies|%d vulnerabilities" -msgstr[0] "" -msgstr[1] "" - msgid "Dependencies|%d vulnerability detected" msgid_plural "Dependencies|%d vulnerabilities detected" msgstr[0] "" @@ -6929,12 +6936,6 @@ msgstr "" msgid "Dependencies|Packager" msgstr "" -msgid "Dependencies|Safe" -msgstr "" - -msgid "Dependencies|Status" -msgstr "" - msgid "Dependencies|The %{codeStartTag}dependency_scanning%{codeEndTag} job has failed and cannot generate the list. Please ensure the job is running properly and run the pipeline again." msgstr "" @@ -6944,9 +6945,6 @@ msgstr "" msgid "Dependencies|Unsupported file(s) detected" msgstr "" -msgid "Dependencies|Version" -msgstr "" - msgid "Dependencies|Vulnerable components" msgstr "" @@ -12739,6 +12737,9 @@ msgstr "" msgid "Manage labels" msgstr "" +msgid "Manage milestones" +msgstr "" + msgid "Manage project labels" msgstr "" @@ -14005,6 +14006,9 @@ msgstr "" msgid "No messages were logged" msgstr "" +msgid "No milestone" +msgstr "" + msgid "No milestones to show" msgstr "" @@ -18171,6 +18175,9 @@ msgstr "" msgid "Search Button" msgstr "" +msgid "Search Milestones" +msgstr "" + msgid "Search an environment spec" msgstr "" diff --git a/package.json b/package.json index acac9f5f36b..82646e20d11 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "@babel/plugin-syntax-import-meta": "^7.8.3", "@babel/preset-env": "^7.8.4", "@gitlab/at.js": "1.5.5", - "@gitlab/svgs": "1.125.0", - "@gitlab/ui": "14.0.0", + "@gitlab/svgs": "1.127.0", + "@gitlab/ui": "14.2.1", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.2-2", "@sentry/browser": "^5.10.2", diff --git a/spec/controllers/projects/alert_management_controller_spec.rb b/spec/controllers/projects/alert_management_controller_spec.rb index ccb9bfc3001..31185aa948a 100644 --- a/spec/controllers/projects/alert_management_controller_spec.rb +++ b/spec/controllers/projects/alert_management_controller_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' describe Projects::AlertManagementController do let_it_be(:project) { create(:project) } - let_it_be(:role) { :reporter } + let_it_be(:role) { :developer } let_it_be(:user) { create(:user) } let_it_be(:id) { 1 } @@ -24,6 +24,16 @@ describe Projects::AlertManagementController do expect(response).to have_gitlab_http_status(:ok) end + + context 'when user is unauthorized' do + let(:role) { :reporter } + + it 'shows 404' do + get :index, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:not_found) + end + end end context 'when alert_management_minimal is disabled' do @@ -50,6 +60,16 @@ describe Projects::AlertManagementController do expect(response).to have_gitlab_http_status(:ok) end + + context 'when user is unauthorized' do + let(:role) { :reporter } + + it 'shows 404' do + get :index, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:not_found) + end + end end context 'when alert_management_detail is disabled' do diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb index c499fac6bc0..a7c8c29517e 100644 --- a/spec/features/global_search_spec.rb +++ b/spec/features/global_search_spec.rb @@ -21,7 +21,7 @@ describe 'Global search' do describe 'I search through the issues and I see pagination' do before do - allow_next_instance_of(Gitlab::SearchResults) do |instance| + allow_next_instance_of(SearchService) do |instance| allow(instance).to receive(:per_page).and_return(1) end create_list(:issue, 2, project: project, title: 'initial') diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index f34c2fb69eb..d365048ab0b 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -15,7 +15,7 @@ describe('Api', () => { beforeEach(() => { mock = new MockAdapter(axios); originalGon = window.gon; - window.gon = Object.assign({}, dummyGon); + window.gon = { ...dummyGon }; }); afterEach(() => { diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js index 6a130c9c43d..ff153007be9 100644 --- a/spec/frontend/blob/components/blob_content_spec.js +++ b/spec/frontend/blob/components/blob_content_spec.js @@ -38,7 +38,7 @@ describe('Blob Content component', () => { it('renders error if there is any in the viewer', () => { const renderError = 'Oops'; - const viewer = Object.assign({}, SimpleViewerMock, { renderError }); + const viewer = { ...SimpleViewerMock, renderError }; createComponent({}, viewer); expect(wrapper.contains(GlLoadingIcon)).toBe(false); expect(wrapper.contains(BlobContentError)).toBe(true); diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js index 6f38167d4bd..3a53208f357 100644 --- a/spec/frontend/blob/components/blob_header_filepath_spec.js +++ b/spec/frontend/blob/components/blob_header_filepath_spec.js @@ -15,7 +15,7 @@ describe('Blob Header Filepath', () => { function createComponent(blobProps = {}, options = {}) { wrapper = shallowMount(BlobHeaderFilepath, { propsData: { - blob: Object.assign({}, MockBlob, blobProps), + blob: { ...MockBlob, ...blobProps }, }, ...options, }); diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js index d410ef10fc9..0e7d2f6516a 100644 --- a/spec/frontend/blob/components/blob_header_spec.js +++ b/spec/frontend/blob/components/blob_header_spec.js @@ -13,7 +13,7 @@ describe('Blob Header Default Actions', () => { const method = shouldMount ? mount : shallowMount; wrapper = method.call(this, BlobHeader, { propsData: { - blob: Object.assign({}, Blob, blobProps), + blob: { ...Blob, ...blobProps }, ...propsData, }, ...options, diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index 882310030f8..fa21053e2de 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -64,7 +64,7 @@ describe('Board list component', () => { let getIssues; function generateIssues(compWrapper) { for (let i = 1; i < 20; i += 1) { - const issue = Object.assign({}, compWrapper.list.issues[0]); + const issue = { ...compWrapper.list.issues[0] }; issue.id += i; compWrapper.list.issues.push(issue); } diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js index 2de04f7da1f..73d08661199 100644 --- a/spec/frontend/clusters/components/knative_domain_editor_spec.js +++ b/spec/frontend/clusters/components/knative_domain_editor_spec.js @@ -93,7 +93,7 @@ describe('KnativeDomainEditor', () => { it('displays toast indicating a successful update', () => { wrapper.vm.$toast = { show: jest.fn() }; - wrapper.setProps({ knative: Object.assign({ updateSuccessful: true }, knative) }); + wrapper.setProps({ knative: { updateSuccessful: true, ...knative } }); return wrapper.vm.$nextTick(() => { expect(wrapper.vm.$toast.show).toHaveBeenCalledWith( diff --git a/spec/frontend/commit/pipelines/pipelines_spec.js b/spec/frontend/commit/pipelines/pipelines_spec.js index b88cba90b87..86ae207e7b7 100644 --- a/spec/frontend/commit/pipelines/pipelines_spec.js +++ b/spec/frontend/commit/pipelines/pipelines_spec.js @@ -118,7 +118,7 @@ describe('Pipelines table in Commits and Merge requests', () => { let pipelineCopy; beforeEach(() => { - pipelineCopy = Object.assign({}, pipeline); + pipelineCopy = { ...pipeline }; }); describe('when latest pipeline has detached flag and canRunPipeline is true', () => { @@ -128,12 +128,7 @@ describe('Pipelines table in Commits and Merge requests', () => { mock.onGet('endpoint.json').reply(200, [pipelineCopy]); - vm = mountComponent( - PipelinesTable, - Object.assign({}, props, { - canRunPipeline: true, - }), - ); + vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: true }); setImmediate(() => { expect(vm.$el.querySelector('.js-run-mr-pipeline')).not.toBeNull(); @@ -149,12 +144,7 @@ describe('Pipelines table in Commits and Merge requests', () => { mock.onGet('endpoint.json').reply(200, [pipelineCopy]); - vm = mountComponent( - PipelinesTable, - Object.assign({}, props, { - canRunPipeline: false, - }), - ); + vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: false }); setImmediate(() => { expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull(); @@ -170,12 +160,7 @@ describe('Pipelines table in Commits and Merge requests', () => { mock.onGet('endpoint.json').reply(200, [pipelineCopy]); - vm = mountComponent( - PipelinesTable, - Object.assign({}, props, { - canRunPipeline: true, - }), - ); + vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: true }); setImmediate(() => { expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull(); @@ -191,12 +176,7 @@ describe('Pipelines table in Commits and Merge requests', () => { mock.onGet('endpoint.json').reply(200, [pipelineCopy]); - vm = mountComponent( - PipelinesTable, - Object.assign({}, props, { - canRunPipeline: false, - }), - ); + vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: false }); setImmediate(() => { expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull(); @@ -211,14 +191,12 @@ describe('Pipelines table in Commits and Merge requests', () => { mock.onGet('endpoint.json').reply(200, [pipelineCopy]); - vm = mountComponent( - PipelinesTable, - Object.assign({}, props, { - canRunPipeline: true, - projectId: '5', - mergeRequestId: 3, - }), - ); + vm = mountComponent(PipelinesTable, { + ...props, + canRunPipeline: true, + projectId: '5', + mergeRequestId: 3, + }); }); it('updates the loading state', done => { diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js index ba5a4f96204..83becc7a20a 100644 --- a/spec/frontend/diffs/components/diff_discussions_spec.js +++ b/spec/frontend/diffs/components/diff_discussions_spec.js @@ -13,7 +13,7 @@ const localVue = createLocalVue(); describe('DiffDiscussions', () => { let store; let wrapper; - const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + const getDiscussionsMockData = () => [{ ...discussionsMockData }]; const createComponent = props => { store = createStore(); diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js index 31c6a4d5b60..0504f3933e0 100644 --- a/spec/frontend/diffs/components/diff_expansion_cell_spec.js +++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js @@ -81,7 +81,7 @@ describe('DiffExpansionCell', () => { isTop: false, isBottom: false, }; - const props = Object.assign({}, defaults, options); + const props = { ...defaults, ...options }; vm = createComponentWithStore(cmp, store, props).$mount(); }; diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js index 4d8345d494d..da18d8e7894 100644 --- a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js +++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js @@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; import discussionsMockData from '../mock_data/diff_discussions'; -const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; +const getDiscussionsMockData = () => [{ ...discussionsMockData }]; describe('DiffGutterAvatars', () => { let wrapper; diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js index 9b032d10fdc..3e0acd0dace 100644 --- a/spec/frontend/diffs/components/diff_line_note_form_spec.js +++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js @@ -9,7 +9,7 @@ describe('DiffLineNoteForm', () => { let wrapper; let diffFile; let diffLines; - const getDiffFileMock = () => Object.assign({}, diffFileMockData); + const getDiffFileMock = () => ({ ...diffFileMockData }); beforeEach(() => { diffFile = getDiffFileMock(); diff --git a/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js b/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js index f423c3b111e..90f012fbafe 100644 --- a/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js +++ b/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js @@ -16,7 +16,7 @@ describe('InlineDiffExpansionRow', () => { isTop: false, isBottom: false, }; - const props = Object.assign({}, defaults, options); + const props = { ...defaults, ...options }; return createComponentWithStore(cmp, createStore(), props).$mount(); }; diff --git a/spec/frontend/diffs/components/inline_diff_view_spec.js b/spec/frontend/diffs/components/inline_diff_view_spec.js index a63c13fb271..9b0cf6a84d9 100644 --- a/spec/frontend/diffs/components/inline_diff_view_spec.js +++ b/spec/frontend/diffs/components/inline_diff_view_spec.js @@ -8,8 +8,8 @@ import discussionsMockData from '../mock_data/diff_discussions'; describe('InlineDiffView', () => { let component; - const getDiffFileMock = () => Object.assign({}, diffFileMockData); - const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + const getDiffFileMock = () => ({ ...diffFileMockData }); + const getDiscussionsMockData = () => [{ ...discussionsMockData }]; const notesLength = getDiscussionsMockData()[0].notes.length; beforeEach(done => { diff --git a/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js b/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js index 15b2a824697..38112445e8d 100644 --- a/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js +++ b/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js @@ -16,7 +16,7 @@ describe('ParallelDiffExpansionRow', () => { isTop: false, isBottom: false, }; - const props = Object.assign({}, defaults, options); + const props = { ...defaults, ...options }; return createComponentWithStore(cmp, createStore(), props).$mount(); }; diff --git a/spec/frontend/diffs/components/parallel_diff_view_spec.js b/spec/frontend/diffs/components/parallel_diff_view_spec.js index 0eefbc7ec08..03cf1b72b62 100644 --- a/spec/frontend/diffs/components/parallel_diff_view_spec.js +++ b/spec/frontend/diffs/components/parallel_diff_view_spec.js @@ -7,7 +7,7 @@ import diffFileMockData from '../mock_data/diff_file'; describe('ParallelDiffView', () => { let component; - const getDiffFileMock = () => Object.assign({}, diffFileMockData); + const getDiffFileMock = () => ({ ...diffFileMockData }); beforeEach(() => { const diffFile = getDiffFileMock(); diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js index ca47f51cb15..dac5be2d656 100644 --- a/spec/frontend/diffs/store/getters_spec.js +++ b/spec/frontend/diffs/store/getters_spec.js @@ -14,10 +14,10 @@ describe('Diffs Module Getters', () => { beforeEach(() => { localState = state(); - discussionMock = Object.assign({}, discussion); + discussionMock = { ...discussion }; discussionMock.diff_file.file_hash = diffFileMock.fileHash; - discussionMock1 = Object.assign({}, discussion); + discussionMock1 = { ...discussion }; discussionMock1.diff_file.file_hash = diffFileMock.fileHash; }); diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js index 422332bab28..31053a3055a 100644 --- a/spec/frontend/diffs/store/utils_spec.js +++ b/spec/frontend/diffs/store/utils_spec.js @@ -372,13 +372,13 @@ describe('DiffsStoreUtils', () => { mock = getDiffFileMock(); preparedDiff = { diff_files: [mock] }; splitInlineDiff = { - diff_files: [Object.assign({}, mock, { parallel_diff_lines: undefined })], + diff_files: [{ ...mock, parallel_diff_lines: undefined }], }; splitParallelDiff = { - diff_files: [Object.assign({}, mock, { highlighted_diff_lines: undefined })], + diff_files: [{ ...mock, highlighted_diff_lines: undefined }], }; completedDiff = { - diff_files: [Object.assign({}, mock, { highlighted_diff_lines: undefined })], + diff_files: [{ ...mock, highlighted_diff_lines: undefined }], }; preparedDiff.diff_files = utils.prepareDiffData(preparedDiff); diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js index 6b2a814d721..35eda21e047 100644 --- a/spec/frontend/groups/components/app_spec.js +++ b/spec/frontend/groups/components/app_spec.js @@ -216,7 +216,7 @@ describe('AppComponent', () => { let groupItem; beforeEach(() => { - groupItem = Object.assign({}, mockParentGroupItem); + groupItem = { ...mockParentGroupItem }; groupItem.isOpen = false; groupItem.isChildrenLoading = false; }); @@ -271,7 +271,7 @@ describe('AppComponent', () => { describe('showLeaveGroupModal', () => { it('caches candidate group (as props) which is to be left', () => { - const group = Object.assign({}, mockParentGroupItem); + const group = { ...mockParentGroupItem }; expect(vm.targetGroup).toBe(null); expect(vm.targetParentGroup).toBe(null); @@ -282,7 +282,7 @@ describe('AppComponent', () => { }); it('updates props which show modal confirmation dialog', () => { - const group = Object.assign({}, mockParentGroupItem); + const group = { ...mockParentGroupItem }; expect(vm.showModal).toBe(false); expect(vm.groupLeaveConfirmationMessage).toBe(''); @@ -297,7 +297,7 @@ describe('AppComponent', () => { describe('hideLeaveGroupModal', () => { it('hides modal confirmation which is shown before leaving the group', () => { - const group = Object.assign({}, mockParentGroupItem); + const group = { ...mockParentGroupItem }; vm.showLeaveGroupModal(group, mockParentGroupItem); expect(vm.showModal).toBe(true); @@ -312,7 +312,7 @@ describe('AppComponent', () => { let childGroupItem; beforeEach(() => { - groupItem = Object.assign({}, mockParentGroupItem); + groupItem = { ...mockParentGroupItem }; groupItem.children = mockChildren; [childGroupItem] = groupItem.children; groupItem.isChildrenLoading = false; diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js index 4b545f05c58..a40fa9bece8 100644 --- a/spec/frontend/groups/components/group_folder_spec.js +++ b/spec/frontend/groups/components/group_folder_spec.js @@ -52,7 +52,7 @@ describe('GroupFolderComponent', () => { }); it('should render more children link when groups list has children over MAX_CHILDREN_COUNT limit', () => { - const parentGroup = Object.assign({}, mockParentGroupItem); + const parentGroup = { ...mockParentGroupItem }; parentGroup.childrenCount = 21; const newVm = createComponent(mockGroups, parentGroup); diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index d1f7653923a..7eb1c54ddb2 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -52,7 +52,7 @@ describe('GroupItemComponent', () => { describe('hasChildren', () => { it('should return boolean value representing if group has any children present', () => { let newVm; - const group = Object.assign({}, mockParentGroupItem); + const group = { ...mockParentGroupItem }; group.childrenCount = 5; newVm = createComponent(group); @@ -71,7 +71,7 @@ describe('GroupItemComponent', () => { describe('hasAvatar', () => { it('should return boolean value representing if group has any avatar present', () => { let newVm; - const group = Object.assign({}, mockParentGroupItem); + const group = { ...mockParentGroupItem }; group.avatarUrl = null; newVm = createComponent(group); @@ -90,7 +90,7 @@ describe('GroupItemComponent', () => { describe('isGroup', () => { it('should return boolean value representing if group item is of type `group` or not', () => { let newVm; - const group = Object.assign({}, mockParentGroupItem); + const group = { ...mockParentGroupItem }; group.type = 'group'; newVm = createComponent(group); @@ -138,7 +138,7 @@ describe('GroupItemComponent', () => { it('should navigate page to group homepage if group does not have any children present', () => { jest.spyOn(urlUtilities, 'visitUrl').mockImplementation(); - const group = Object.assign({}, mockParentGroupItem); + const group = { ...mockParentGroupItem }; group.childrenCount = 0; const newVm = createComponent(group); jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js index 2e0738bd1b4..c0dc1a816e6 100644 --- a/spec/frontend/groups/components/item_actions_spec.js +++ b/spec/frontend/groups/components/item_actions_spec.js @@ -46,7 +46,7 @@ describe('ItemActionsComponent', () => { }); it('should render Edit Group button with correct attribute values', () => { - const group = Object.assign({}, mockParentGroupItem); + const group = { ...mockParentGroupItem }; group.canEdit = true; const newVm = createComponent(group); @@ -64,7 +64,7 @@ describe('ItemActionsComponent', () => { }); it('should render Leave Group button with correct attribute values', () => { - const group = Object.assign({}, mockParentGroupItem); + const group = { ...mockParentGroupItem }; group.canLeave = true; const newVm = createComponent(group); diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js index fb4285a2b04..771643609ec 100644 --- a/spec/frontend/groups/components/item_stats_spec.js +++ b/spec/frontend/groups/components/item_stats_spec.js @@ -23,7 +23,7 @@ describe('ItemStatsComponent', () => { describe('visibilityIcon', () => { it('should return icon class based on `item.visibility` value', () => { Object.keys(VISIBILITY_TYPE_ICON).forEach(visibility => { - const item = Object.assign({}, mockParentGroupItem, { visibility }); + const item = { ...mockParentGroupItem, visibility }; const vm = createComponent(item); expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]); @@ -35,10 +35,7 @@ describe('ItemStatsComponent', () => { describe('visibilityTooltip', () => { it('should return tooltip string for Group based on `item.visibility` value', () => { Object.keys(GROUP_VISIBILITY_TYPE).forEach(visibility => { - const item = Object.assign({}, mockParentGroupItem, { - visibility, - type: ITEM_TYPE.GROUP, - }); + const item = { ...mockParentGroupItem, visibility, type: ITEM_TYPE.GROUP }; const vm = createComponent(item); expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]); @@ -48,10 +45,7 @@ describe('ItemStatsComponent', () => { it('should return tooltip string for Project based on `item.visibility` value', () => { Object.keys(PROJECT_VISIBILITY_TYPE).forEach(visibility => { - const item = Object.assign({}, mockParentGroupItem, { - visibility, - type: ITEM_TYPE.PROJECT, - }); + const item = { ...mockParentGroupItem, visibility, type: ITEM_TYPE.PROJECT }; const vm = createComponent(item); expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]); @@ -65,13 +59,13 @@ describe('ItemStatsComponent', () => { let item; let vm; - item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); + item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT }; vm = createComponent(item); expect(vm.isProject).toBeTruthy(); vm.$destroy(); - item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); + item = { ...mockParentGroupItem, type: ITEM_TYPE.GROUP }; vm = createComponent(item); expect(vm.isProject).toBeFalsy(); @@ -84,13 +78,13 @@ describe('ItemStatsComponent', () => { let item; let vm; - item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); + item = { ...mockParentGroupItem, type: ITEM_TYPE.GROUP }; vm = createComponent(item); expect(vm.isGroup).toBeTruthy(); vm.$destroy(); - item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); + item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT }; vm = createComponent(item); expect(vm.isGroup).toBeFalsy(); @@ -109,10 +103,7 @@ describe('ItemStatsComponent', () => { }); it('renders start count and last updated information for project item correctly', () => { - const item = Object.assign({}, mockParentGroupItem, { - type: ITEM_TYPE.PROJECT, - starCount: 4, - }); + const item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT, starCount: 4 }; const vm = createComponent(item); const projectStarIconEl = vm.$el.querySelector('.project-stars'); diff --git a/spec/frontend/groups/components/item_stats_value_spec.js b/spec/frontend/groups/components/item_stats_value_spec.js index 9561a329887..da6f145fa19 100644 --- a/spec/frontend/groups/components/item_stats_value_spec.js +++ b/spec/frontend/groups/components/item_stats_value_spec.js @@ -27,7 +27,7 @@ describe('ItemStatsValueComponent', () => { describe('isValuePresent', () => { it('returns true if non-empty `value` is present', () => { - vm = createComponent(Object.assign({}, itemConfig, { value: 10 })); + vm = createComponent({ ...itemConfig, value: 10 }); expect(vm.isValuePresent).toBeTruthy(); }); diff --git a/spec/frontend/groups/store/groups_store_spec.js b/spec/frontend/groups/store/groups_store_spec.js index 9eefcbe0275..7d12f73d270 100644 --- a/spec/frontend/groups/store/groups_store_spec.js +++ b/spec/frontend/groups/store/groups_store_spec.js @@ -108,8 +108,8 @@ describe('ProjectsStore', () => { describe('removeGroup', () => { it('should remove children from group item in state', () => { const store = new GroupsStore(); - const rawParentGroup = Object.assign({}, mockGroups[0]); - const rawChildGroup = Object.assign({}, mockGroups[1]); + const rawParentGroup = { ...mockGroups[0] }; + const rawChildGroup = { ...mockGroups[1] }; store.setGroups([rawParentGroup]); store.setGroupChildren(store.state.groups[0], [rawChildGroup]); diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js index 30a09092f70..b56957e1f6d 100644 --- a/spec/frontend/ide/components/ide_review_spec.js +++ b/spec/frontend/ide/components/ide_review_spec.js @@ -15,7 +15,7 @@ describe('IDE review mode', () => { store = createStore(); store.state.currentProjectId = 'abcproject'; store.state.currentBranchId = 'master'; - store.state.projects.abcproject = Object.assign({}, projectData); + store.state.projects.abcproject = { ...projectData }; Vue.set(store.state.trees, 'abcproject/master', { tree: [file('fileName')], loading: false, diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js index ad2b3c8c01a..78a280e6304 100644 --- a/spec/frontend/ide/components/ide_spec.js +++ b/spec/frontend/ide/components/ide_spec.js @@ -10,7 +10,7 @@ function bootstrap(projData) { store.state.currentProjectId = 'abcproject'; store.state.currentBranchId = 'master'; - store.state.projects.abcproject = Object.assign({}, projData); + store.state.projects.abcproject = { ...projData }; Vue.set(store.state.trees, 'abcproject/master', { tree: [], loading: false, @@ -27,7 +27,7 @@ describe('ide component, empty repo', () => { let vm; beforeEach(() => { - const emptyProjData = Object.assign({}, projectData, { empty_repo: true, branches: {} }); + const emptyProjData = { ...projectData, empty_repo: true, branches: {} }; vm = bootstrap(emptyProjData); vm.$mount(); }); diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js index 6694ac497fb..30f11db3153 100644 --- a/spec/frontend/ide/components/ide_tree_list_spec.js +++ b/spec/frontend/ide/components/ide_tree_list_spec.js @@ -14,7 +14,7 @@ describe('IDE tree list', () => { const bootstrapWithTree = (tree = normalBranchTree) => { store.state.currentProjectId = 'abcproject'; store.state.currentBranchId = 'master'; - store.state.projects.abcproject = Object.assign({}, projectData); + store.state.projects.abcproject = { ...projectData }; Vue.set(store.state.trees, 'abcproject/master', { tree, loading: false, diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js index 97a0a2432f1..01f007f09c3 100644 --- a/spec/frontend/ide/components/ide_tree_spec.js +++ b/spec/frontend/ide/components/ide_tree_spec.js @@ -13,7 +13,7 @@ describe('IdeRepoTree', () => { store.state.currentProjectId = 'abcproject'; store.state.currentBranchId = 'master'; - store.state.projects.abcproject = Object.assign({}, projectData); + store.state.projects.abcproject = { ...projectData }; Vue.set(store.state.trees, 'abcproject/master', { tree: [file('fileName')], loading: false, diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js index 8f60823ee72..9491b52c888 100644 --- a/spec/frontend/import_projects/components/import_projects_table_spec.js +++ b/spec/frontend/import_projects/components/import_projects_table_spec.js @@ -17,11 +17,12 @@ describe('ImportProjectsTable', () => { }; function initStore() { - const stubbedActions = Object.assign({}, actions, { + const stubbedActions = { + ...actions, fetchJobs: jest.fn(), fetchRepos: jest.fn(actions.requestRepos), fetchImport: jest.fn(actions.requestImport), - }); + }; const store = new Vuex.Store({ state: state(), diff --git a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js index 8efd526e360..8be645c496f 100644 --- a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js +++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js @@ -18,9 +18,7 @@ describe('ProviderRepoTableRow', () => { }; function initStore() { - const stubbedActions = Object.assign({}, actions, { - fetchImport, - }); + const stubbedActions = { ...actions, fetchImport }; const store = new Vuex.Store({ state: state(), diff --git a/spec/frontend/integrations/edit/components/active_toggle_spec.js b/spec/frontend/integrations/edit/components/active_toggle_spec.js index 7f13d707efd..4b968c9a70b 100644 --- a/spec/frontend/integrations/edit/components/active_toggle_spec.js +++ b/spec/frontend/integrations/edit/components/active_toggle_spec.js @@ -13,7 +13,7 @@ describe('ActiveToggle', () => { const createComponent = props => { wrapper = mount(ActiveToggle, { - propsData: Object.assign({}, defaultProps, props), + propsData: { ...defaultProps, ...props }, }); }; diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js index 16d9df96c16..e8aa920fdd4 100644 --- a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js +++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js @@ -13,7 +13,7 @@ describe('JiraTriggerFields', () => { const createComponent = props => { wrapper = mount(JiraTriggerFields, { - propsData: Object.assign({}, defaultProps, props), + propsData: { ...defaultProps, ...props }, }); }; diff --git a/spec/frontend/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js index 9c448c498e2..0053475dd13 100644 --- a/spec/frontend/issue_show/components/description_spec.js +++ b/spec/frontend/issue_show/components/description_spec.js @@ -113,12 +113,7 @@ describe('Description component', () => { beforeEach(() => { vm.$destroy(); TaskList.mockClear(); - vm = mountComponent( - DescriptionComponent, - Object.assign({}, props, { - issuableType: 'issuableType', - }), - ); + vm = mountComponent(DescriptionComponent, { ...props, issuableType: 'issuableType' }); }); it('re-inits the TaskList when description changed', () => { diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js index f2e202674ee..5ce69221dab 100644 --- a/spec/frontend/jobs/components/log/line_header_spec.js +++ b/spec/frontend/jobs/components/log/line_header_spec.js @@ -86,7 +86,7 @@ describe('Job Log Header Line', () => { describe('with duration', () => { beforeEach(() => { - createComponent(Object.assign({}, data, { duration: '00:10' })); + createComponent({ ...data, duration: '00:10' }); }); it('renders the duration badge', () => { diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js index d77690ffac0..3557d3b94b6 100644 --- a/spec/frontend/jobs/store/mutations_spec.js +++ b/spec/frontend/jobs/store/mutations_spec.js @@ -59,7 +59,7 @@ describe('Jobs Store Mutations', () => { describe('when traceSize is bigger than the total size', () => { it('sets isTraceSizeVisible to false', () => { - const copy = Object.assign({}, stateCopy, { traceSize: 5118460, size: 2321312 }); + const copy = { ...stateCopy, traceSize: 5118460, size: 2321312 }; mutations[types.RECEIVE_TRACE_SUCCESS](copy, { total: 511846 }); diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index 1edfda30fec..c8dc90c9ace 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -503,7 +503,7 @@ describe('common_utils', () => { beforeEach(() => { window.gon = window.gon || {}; - beforeGon = Object.assign({}, window.gon); + beforeGon = { ...window.gon }; window.gon.sprite_icons = 'icons.svg'; }); diff --git a/spec/frontend/milestones/mock_data.js b/spec/frontend/milestones/mock_data.js new file mode 100644 index 00000000000..c64eeeba663 --- /dev/null +++ b/spec/frontend/milestones/mock_data.js @@ -0,0 +1,82 @@ +export const milestones = [ + { + id: 41, + iid: 6, + project_id: 8, + title: 'v0.1', + description: '', + state: 'active', + created_at: '2020-04-04T01:30:40.051Z', + updated_at: '2020-04-04T01:30:40.051Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6', + }, + { + id: 40, + iid: 5, + project_id: 8, + title: 'v4.0', + description: 'Laboriosam nisi sapiente dolores et magnam nobis ad earum.', + state: 'closed', + created_at: '2020-01-13T19:39:15.191Z', + updated_at: '2020-01-13T19:39:15.191Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/5', + }, + { + id: 39, + iid: 4, + project_id: 8, + title: 'v3.0', + description: 'Necessitatibus illo alias et repellat dolorum assumenda ut.', + state: 'closed', + created_at: '2020-01-13T19:39:15.176Z', + updated_at: '2020-01-13T19:39:15.176Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/4', + }, + { + id: 38, + iid: 3, + project_id: 8, + title: 'v2.0', + description: 'Doloribus qui repudiandae iste sit.', + state: 'closed', + created_at: '2020-01-13T19:39:15.161Z', + updated_at: '2020-01-13T19:39:15.161Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/3', + }, + { + id: 37, + iid: 2, + project_id: 8, + title: 'v1.0', + description: 'Illo sint odio officia ea.', + state: 'closed', + created_at: '2020-01-13T19:39:15.146Z', + updated_at: '2020-01-13T19:39:15.146Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/2', + }, + { + id: 36, + iid: 1, + project_id: 8, + title: 'v0.0', + description: 'Sed quae facilis deleniti at delectus assumenda nobis veritatis.', + state: 'active', + created_at: '2020-01-13T19:39:15.127Z', + updated_at: '2020-01-13T19:39:15.127Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/1', + }, +]; + +export default milestones; diff --git a/spec/frontend/milestones/project_milestone_combobox_spec.js b/spec/frontend/milestones/project_milestone_combobox_spec.js new file mode 100644 index 00000000000..a7321d21559 --- /dev/null +++ b/spec/frontend/milestones/project_milestone_combobox_spec.js @@ -0,0 +1,150 @@ +import { milestones as projectMilestones } from './mock_data'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { shallowMount } from '@vue/test-utils'; +import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue'; +import { GlNewDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; + +const TEST_SEARCH_ENDPOINT = '/api/v4/projects/8/search'; + +const extraLinks = [ + { text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' }, + { text: 'Manage milestones', url: '/h5bp/html5-boilerplate/-/milestones' }, +]; + +const preselectedMilestones = []; +const projectId = '8'; + +describe('Milestone selector', () => { + let wrapper; + let mock; + + const findNoResultsMessage = () => wrapper.find({ ref: 'noResults' }); + + const factory = (options = {}) => { + wrapper = shallowMount(MilestoneCombobox, { + ...options, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + gon.api_version = 'v4'; + + mock.onGet('/api/v4/projects/8/milestones').reply(200, projectMilestones); + + factory({ + propsData: { + projectId, + preselectedMilestones, + extraLinks, + }, + }); + }); + + afterEach(() => { + mock.restore(); + wrapper.destroy(); + wrapper = null; + }); + + it('renders the dropdown', () => { + expect(wrapper.find(GlNewDropdown)).toExist(); + }); + + it('renders additional links', () => { + const links = wrapper.findAll('[href]'); + links.wrappers.forEach((item, idx) => { + expect(item.text()).toBe(extraLinks[idx].text); + expect(item.attributes('href')).toBe(extraLinks[idx].url); + }); + }); + + describe('before results', () => { + it('should show a loading icon', () => { + const request = mock.onGet(TEST_SEARCH_ENDPOINT, { + params: { search: 'TEST_SEARCH', scope: 'milestones' }, + }); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + + return wrapper.vm.$nextTick().then(() => { + request.reply(200, []); + }); + }); + + it('should not show any dropdown items', () => { + expect(wrapper.findAll('[role="milestone option"]')).toHaveLength(0); + }); + + it('should have "No milestone" as the button text', () => { + expect(wrapper.find({ ref: 'buttonText' }).text()).toBe('No milestone'); + }); + }); + + describe('with empty results', () => { + beforeEach(() => { + mock + .onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'TEST_SEARCH', scope: 'milestones' } }) + .reply(200, []); + wrapper.find(GlSearchBoxByType).vm.$emit('input', 'TEST_SEARCH'); + return axios.waitForAll(); + }); + + it('should display that no matching items are found', () => { + expect(findNoResultsMessage().exists()).toBe(true); + }); + }); + + describe('with results', () => { + let items; + beforeEach(() => { + mock + .onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'v0.1', scope: 'milestones' } }) + .reply(200, [ + { + id: 41, + iid: 6, + project_id: 8, + title: 'v0.1', + description: '', + state: 'active', + created_at: '2020-04-04T01:30:40.051Z', + updated_at: '2020-04-04T01:30:40.051Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6', + }, + ]); + wrapper.find(GlSearchBoxByType).vm.$emit('input', 'v0.1'); + return axios.waitForAll().then(() => { + items = wrapper.findAll('[role="milestone option"]'); + }); + }); + + it('should display one item per result', () => { + expect(items).toHaveLength(1); + }); + + it('should emit a change if an item is clicked', () => { + items.at(0).vm.$emit('click'); + expect(wrapper.emitted().change.length).toBe(1); + expect(wrapper.emitted().change[0]).toEqual([[{ title: 'v0.1' }]]); + }); + + it('should not have a selecton icon on any item', () => { + items.wrappers.forEach(item => { + expect(item.find('.selected-item').exists()).toBe(false); + }); + }); + + it('should have a selecton icon if an item is clicked', () => { + items.at(0).vm.$emit('click'); + expect(wrapper.find('.selected-item').exists()).toBe(true); + }); + + it('should not display a message about no results', () => { + expect(findNoResultsMessage().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js index b91f599f158..b14ec2a65be 100644 --- a/spec/frontend/notes/components/noteable_discussion_spec.js +++ b/spec/frontend/notes/components/noteable_discussion_spec.js @@ -138,7 +138,7 @@ describe('noteable_discussion component', () => { describe('signout widget', () => { beforeEach(() => { - originalGon = Object.assign({}, window.gon); + originalGon = { ...window.gon }; window.gon = window.gon || {}; }); diff --git a/spec/frontend/notes/old_notes_spec.js b/spec/frontend/notes/old_notes_spec.js index 9f3ab4185c4..cb1d563ece7 100644 --- a/spec/frontend/notes/old_notes_spec.js +++ b/spec/frontend/notes/old_notes_spec.js @@ -193,7 +193,7 @@ describe.skip('Old Notes (~/notes.js)', () => { $('.js-comment-button').click(); const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`); - const updatedNote = Object.assign({}, noteEntity); + const updatedNote = { ...noteEntity }; updatedNote.note = 'bar'; notes.updateNote(updatedNote, $targetNote); diff --git a/spec/frontend/notes/stores/collapse_utils_spec.js b/spec/frontend/notes/stores/collapse_utils_spec.js index d3019f4b9a4..a74809eed79 100644 --- a/spec/frontend/notes/stores/collapse_utils_spec.js +++ b/spec/frontend/notes/stores/collapse_utils_spec.js @@ -18,9 +18,7 @@ describe('Collapse utils', () => { }); it('returns false when a system note is not a description type', () => { - expect(isDescriptionSystemNote(Object.assign({}, mockSystemNote, { note: 'foo' }))).toEqual( - false, - ); + expect(isDescriptionSystemNote({ ...mockSystemNote, note: 'foo' })).toEqual(false); }); it('gets the time difference between two notes', () => { diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js index 67757ad56c5..27e3490d64b 100644 --- a/spec/frontend/notes/stores/mutation_spec.js +++ b/spec/frontend/notes/stores/mutation_spec.js @@ -50,7 +50,7 @@ describe('Notes Store mutations', () => { }); describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { - const newReply = Object.assign({}, note, { discussion_id: discussionMock.id }); + const newReply = { ...note, discussion_id: discussionMock.id }; let state; @@ -86,7 +86,7 @@ describe('Notes Store mutations', () => { describe('EXPAND_DISCUSSION', () => { it('should expand a collapsed discussion', () => { - const discussion = Object.assign({}, discussionMock, { expanded: false }); + const discussion = { ...discussionMock, expanded: false }; const state = { discussions: [discussion], @@ -100,7 +100,7 @@ describe('Notes Store mutations', () => { describe('COLLAPSE_DISCUSSION', () => { it('should collapse an expanded discussion', () => { - const discussion = Object.assign({}, discussionMock, { expanded: true }); + const discussion = { ...discussionMock, expanded: true }; const state = { discussions: [discussion], @@ -114,7 +114,7 @@ describe('Notes Store mutations', () => { describe('REMOVE_PLACEHOLDER_NOTES', () => { it('should remove all placeholder notes in indivudal notes and discussion', () => { - const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true }); + const placeholderNote = { ...individualNote, isPlaceholderNote: true }; const state = { discussions: [placeholderNote] }; mutations.REMOVE_PLACEHOLDER_NOTES(state); @@ -298,7 +298,7 @@ describe('Notes Store mutations', () => { describe('TOGGLE_DISCUSSION', () => { it('should open a closed discussion', () => { - const discussion = Object.assign({}, discussionMock, { expanded: false }); + const discussion = { ...discussionMock, expanded: false }; const state = { discussions: [discussion], @@ -348,8 +348,8 @@ describe('Notes Store mutations', () => { }); it('should open all closed discussions', () => { - const discussion1 = Object.assign({}, discussionMock, { id: 0, expanded: false }); - const discussion2 = Object.assign({}, discussionMock, { id: 1, expanded: true }); + const discussion1 = { ...discussionMock, id: 0, expanded: false }; + const discussion2 = { ...discussionMock, id: 1, expanded: true }; const discussionIds = [discussion1.id, discussion2.id]; const state = { discussions: [discussion1, discussion2] }; @@ -362,8 +362,8 @@ describe('Notes Store mutations', () => { }); it('should close all opened discussions', () => { - const discussion1 = Object.assign({}, discussionMock, { id: 0, expanded: false }); - const discussion2 = Object.assign({}, discussionMock, { id: 1, expanded: true }); + const discussion1 = { ...discussionMock, id: 0, expanded: false }; + const discussion2 = { ...discussionMock, id: 1, expanded: true }; const discussionIds = [discussion1.id, discussion2.id]; const state = { discussions: [discussion1, discussion2] }; @@ -382,7 +382,7 @@ describe('Notes Store mutations', () => { discussions: [individualNote], }; - const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); + const updated = { ...individualNote.notes[0], note: 'Foo' }; mutations.UPDATE_NOTE(state, updated); diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js index 88e56eee1d6..d32534326c5 100644 --- a/spec/frontend/pipelines/graph/stage_column_component_spec.js +++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js @@ -26,7 +26,7 @@ describe('stage column component', () => { beforeEach(() => { const mockGroups = []; for (let i = 0; i < 3; i += 1) { - const mockedJob = Object.assign({}, mockJob); + const mockedJob = { ...mockJob }; mockedJob.id += i; mockGroups.push(mockedJob); } diff --git a/spec/javascripts/pipelines/linked_pipelines_mock.json b/spec/frontend/pipelines/linked_pipelines_mock.json index 60e214ddc32..8ad19ef4865 100644 --- a/spec/javascripts/pipelines/linked_pipelines_mock.json +++ b/spec/frontend/pipelines/linked_pipelines_mock.json @@ -1766,7 +1766,8 @@ "name": "GitLab Docs", "full_path": "/gitlab-com/gitlab-docs", "full_name": "GitLab.com / GitLab Docs" - } + }, + "triggered": [{}] }, { "id": 34993052, diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index 40c493e3aa5..e46b9c7c817 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -601,7 +601,7 @@ describe('Pipelines', () => { describe('updates results when a staged is clicked', () => { beforeEach(() => { - const copyPipeline = Object.assign({}, pipelineWithStages); + const copyPipeline = { ...pipelineWithStages }; copyPipeline.id += 1; mock .onGet('twitter/flight/pipelines.json') diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js index c43210c5350..3d564c8758c 100644 --- a/spec/frontend/pipelines/pipelines_table_row_spec.js +++ b/spec/frontend/pipelines/pipelines_table_row_spec.js @@ -169,7 +169,7 @@ describe('Pipelines Table Row', () => { }; beforeEach(() => { - const withActions = Object.assign({}, pipeline); + const withActions = { ...pipeline }; withActions.details.scheduled_actions = [scheduledJobAction]; withActions.flags.cancelable = true; withActions.flags.retryable = true; diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js index b020aaedd06..6aa041bcb7f 100644 --- a/spec/frontend/pipelines/stage_spec.js +++ b/spec/frontend/pipelines/stage_spec.js @@ -96,7 +96,7 @@ describe('Pipelines stage component', () => { describe('update endpoint correctly', () => { beforeEach(() => { - const copyStage = Object.assign({}, stageReply); + const copyStage = { ...stageReply }; copyStage.latest_statuses[0].name = 'this is the updated content'; mock.onGet('bar.json').reply(200, copyStage); createComponent({ diff --git a/spec/javascripts/pipelines/stores/pipeline_store.js b/spec/frontend/pipelines/stores/pipeline_store_spec.js index 4a0b3bf4c02..68d438109b3 100644 --- a/spec/javascripts/pipelines/stores/pipeline_store.js +++ b/spec/frontend/pipelines/stores/pipeline_store_spec.js @@ -7,14 +7,12 @@ describe('EE Pipeline store', () => { beforeEach(() => { store = new PipelineStore(); - data = Object.assign({}, LinkedPipelines); + data = { ...LinkedPipelines }; + + store.storePipeline(data); }); describe('storePipeline', () => { - beforeAll(() => { - store.storePipeline(data); - }); - describe('triggered_by', () => { it('sets triggered_by as an array', () => { expect(store.state.pipeline.triggered_by.length).toEqual(1); @@ -50,10 +48,6 @@ describe('EE Pipeline store', () => { }); describe('resetTriggeredByPipeline', () => { - beforeEach(() => { - store.storePipeline(data); - }); - it('closes the pipeline & nested ones', () => { store.state.pipeline.triggered_by[0].isExpanded = true; store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded = true; @@ -66,10 +60,6 @@ describe('EE Pipeline store', () => { }); describe('openTriggeredByPipeline', () => { - beforeEach(() => { - store.storePipeline(data); - }); - it('opens the given pipeline', () => { store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]); @@ -78,10 +68,6 @@ describe('EE Pipeline store', () => { }); describe('closeTriggeredByPipeline', () => { - beforeEach(() => { - store.storePipeline(data); - }); - it('closes the given pipeline', () => { // open it first store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]); @@ -93,15 +79,11 @@ describe('EE Pipeline store', () => { }); describe('resetTriggeredPipelines', () => { - beforeEach(() => { - store.storePipeline(data); - }); - it('closes the pipeline & nested ones', () => { store.state.pipeline.triggered[0].isExpanded = true; store.state.pipeline.triggered[0].triggered[0].isExpanded = true; - store.resetTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]); + store.resetTriggeredPipelines(store.state.pipeline, store.state.pipeline.triggered[0]); expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false); expect(store.state.pipeline.triggered[0].triggered[0].isExpanded).toEqual(false); @@ -109,10 +91,6 @@ describe('EE Pipeline store', () => { }); describe('openTriggeredPipeline', () => { - beforeEach(() => { - store.storePipeline(data); - }); - it('opens the given pipeline', () => { store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]); @@ -121,10 +99,6 @@ describe('EE Pipeline store', () => { }); describe('closeTriggeredPipeline', () => { - beforeEach(() => { - store.storePipeline(data); - }); - it('closes the given pipeline', () => { // open it first store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]); @@ -136,12 +110,8 @@ describe('EE Pipeline store', () => { }); describe('toggleLoading', () => { - beforeEach(() => { - store.storePipeline(data); - }); - it('toggles the isLoading property for the given pipeline', () => { - store.togglePipeline(store.state.pipeline.triggered[0]); + store.toggleLoading(store.state.pipeline.triggered[0]); expect(store.state.pipeline.triggered[0].isLoading).toEqual(true); }); diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js index 9eaa563025d..a0eb93c4e6b 100644 --- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js @@ -20,7 +20,7 @@ describe('Mutations TestReports Store', () => { describe('set endpoint', () => { it('should set endpoint', () => { - const expectedState = Object.assign({}, mockState, { endpoint: 'foo' }); + const expectedState = { ...mockState, endpoint: 'foo' }; mutations[types.SET_ENDPOINT](mockState, 'foo'); expect(mockState.endpoint).toEqual(expectedState.endpoint); @@ -47,14 +47,14 @@ describe('Mutations TestReports Store', () => { describe('toggle loading', () => { it('should set to true', () => { - const expectedState = Object.assign({}, mockState, { isLoading: true }); + const expectedState = { ...mockState, isLoading: true }; mutations[types.TOGGLE_LOADING](mockState); expect(mockState.isLoading).toEqual(expectedState.isLoading); }); it('should toggle back to false', () => { - const expectedState = Object.assign({}, mockState, { isLoading: false }); + const expectedState = { ...mockState, isLoading: false }; mockState.isLoading = true; mutations[types.TOGGLE_LOADING](mockState); diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js index 160d93d2e6b..8f041e46472 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js @@ -82,17 +82,19 @@ describe('Test reports summary', () => { describe('success percentage calculation', () => { it.each` - name | successCount | totalCount | result - ${'displays 0 when there are no tests'} | ${0} | ${0} | ${'0'} - ${'displays whole number when possible'} | ${10} | ${50} | ${'20'} - ${'rounds to 0.01'} | ${1} | ${16604} | ${'0.01'} - ${'correctly rounds to 50'} | ${8302} | ${16604} | ${'50'} - ${'rounds down for large close numbers'} | ${16603} | ${16604} | ${'99.99'} - ${'correctly displays 100'} | ${16604} | ${16604} | ${'100'} - `('$name', ({ successCount, totalCount, result }) => { + name | successCount | totalCount | skippedCount | result + ${'displays 0 when there are no tests'} | ${0} | ${0} | ${0} | ${'0'} + ${'displays whole number when possible'} | ${10} | ${50} | ${0} | ${'20'} + ${'excludes skipped tests from total'} | ${10} | ${50} | ${5} | ${'22.22'} + ${'rounds to 0.01'} | ${1} | ${16604} | ${0} | ${'0.01'} + ${'correctly rounds to 50'} | ${8302} | ${16604} | ${0} | ${'50'} + ${'rounds down for large close numbers'} | ${16603} | ${16604} | ${0} | ${'99.99'} + ${'correctly displays 100'} | ${16604} | ${16604} | ${0} | ${'100'} + `('$name', ({ successCount, totalCount, skippedCount, result }) => { createComponent({ report: { success_count: successCount, + skipped_count: skippedCount, total_count: totalCount, }, }); diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_spec.js index 09bafe4aa9b..4450b047acd 100644 --- a/spec/frontend/releases/components/app_edit_spec.js +++ b/spec/frontend/releases/components/app_edit_spec.js @@ -1,11 +1,13 @@ import Vuex from 'vuex'; import { mount } from '@vue/test-utils'; import ReleaseEditApp from '~/releases/components/app_edit.vue'; -import { release as originalRelease } from '../mock_data'; +import { release as originalRelease, milestones as originalMilestones } from '../mock_data'; import * as commonUtils from '~/lib/utils/common_utils'; import { BACK_URL_PARAM } from '~/releases/constants'; import AssetLinksForm from '~/releases/components/asset_links_form.vue'; import { merge } from 'lodash'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; describe('Release edit component', () => { let wrapper; @@ -13,6 +15,7 @@ describe('Release edit component', () => { let actions; let getters; let state; + let mock; const factory = ({ featureFlags = {}, store: storeUpdates = {} } = {}) => { state = { @@ -20,6 +23,7 @@ describe('Release edit component', () => { markdownDocsPath: 'path/to/markdown/docs', updateReleaseApiDocsPath: 'path/to/update/release/api/docs', releasesPagePath: 'path/to/releases/page', + projectId: '8', }; actions = { @@ -62,8 +66,11 @@ describe('Release edit component', () => { }; beforeEach(() => { + mock = new MockAdapter(axios); gon.api_version = 'v4'; + mock.onGet('/api/v4/projects/8/milestones').reply(200, originalMilestones); + release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true }); }); diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index 231d877a25d..854f06821be 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -130,6 +130,15 @@ describe('Release detail actions', () => { }); }); + describe('updateReleaseMilestones', () => { + it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => { + const newReleaseMilestones = ['v0.0', 'v0.1']; + return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [ + { type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones }, + ]); + }); + }); + describe('requestUpdateRelease', () => { it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () => testAction(actions.requestUpdateRelease, undefined, state, [ @@ -248,6 +257,7 @@ describe('Release detail actions', () => { { name: state.release.name, description: state.release.description, + milestones: state.release.milestones.map(milestone => milestone.title), }, ], ]); diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index 1f6038bc7f0..612ca858f05 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -84,9 +84,7 @@ describe('Blob Embeddable', () => { }); it('sets rich viewer correctly', () => { - const data = Object.assign({}, dataMock, { - activeViewerType: RichViewerMock.type, - }); + const data = { ...dataMock, activeViewerType: RichViewerMock.type }; createComponent({}, data); expect(wrapper.find(RichViewer).exists()).toBe(true); }); diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js index b9b60883eb3..88261a75f6c 100644 --- a/spec/frontend/snippets/components/snippet_title_spec.js +++ b/spec/frontend/snippets/components/snippet_title_spec.js @@ -17,7 +17,7 @@ describe('Snippet header component', () => { }; function createComponent({ props = snippet } = {}) { - const defaultProps = Object.assign({}, props); + const defaultProps = { ...props }; wrapper = shallowMount(SnippetTitle, { propsData: { diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js index 1f77b8c26b9..08a26d46618 100644 --- a/spec/frontend/tracking_spec.js +++ b/spec/frontend/tracking_spec.js @@ -46,10 +46,11 @@ describe('Tracking', () => { expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking'); expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking'); - window.snowplowOptions = Object.assign({}, window.snowplowOptions, { + window.snowplowOptions = { + ...window.snowplowOptions, formTracking: true, linkClickTracking: true, - }); + }; initUserTracking(); expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking'); diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js index a7ecb863cfb..8a604355625 100644 --- a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js @@ -61,7 +61,7 @@ describe('Merge Request Collapsible Extension', () => { describe('while loading', () => { beforeEach(() => { - mountComponent(Object.assign({}, data, { isLoading: true })); + mountComponent({ ...data, isLoading: true }); }); it('renders the buttons disabled', () => { @@ -86,7 +86,7 @@ describe('Merge Request Collapsible Extension', () => { describe('with error', () => { beforeEach(() => { - mountComponent(Object.assign({}, data, { hasError: true })); + mountComponent({ ...data, hasError: true }); }); it('does not render the buttons', () => { diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js index cda5ca68d9b..5f3a8654990 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js @@ -13,7 +13,7 @@ describe('MrWidgetPipelineContainer', () => { const factory = (props = {}) => { wrapper = mount(MrWidgetPipelineContainer, { propsData: { - mr: Object.assign({}, mockStore), + mr: { ...mockStore }, ...props, }, }); diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js index 732491378fa..46df2d2aaf1 100644 --- a/spec/frontend/vue_shared/components/file_row_spec.js +++ b/spec/frontend/vue_shared/components/file_row_spec.js @@ -91,9 +91,7 @@ describe('File row component', () => { jest.spyOn(wrapper.vm, 'scrollIntoView'); wrapper.setProps({ - file: Object.assign({}, wrapper.props('file'), { - active: true, - }), + file: { ...wrapper.props('file'), active: true }, }); return nextTick().then(() => { @@ -125,9 +123,7 @@ describe('File row component', () => { it('matches the current route against encoded file URL', () => { const fileName = 'with space'; - const rowFile = Object.assign({}, file(fileName), { - url: `/${fileName}`, - }); + const rowFile = { ...file(fileName), url: `/${fileName}` }; const routerPath = `/project/${escapeFileUrl(fileName)}`; createComponent( { diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js index 4c654e01f74..90c3fe54901 100644 --- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js +++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js @@ -36,9 +36,7 @@ describe('IssueMilestoneComponent', () => { describe('isMilestoneStarted', () => { it('should return `false` when milestoneStart prop is not defined', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { - start_date: '', - }), + milestone: { ...mockMilestone, start_date: '' }, }); expect(wrapper.vm.isMilestoneStarted).toBe(false); @@ -46,9 +44,7 @@ describe('IssueMilestoneComponent', () => { it('should return `true` when milestone start date is past current date', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { - start_date: '1990-07-22', - }), + milestone: { ...mockMilestone, start_date: '1990-07-22' }, }); expect(wrapper.vm.isMilestoneStarted).toBe(true); @@ -58,9 +54,7 @@ describe('IssueMilestoneComponent', () => { describe('isMilestonePastDue', () => { it('should return `false` when milestoneDue prop is not defined', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { - due_date: '', - }), + milestone: { ...mockMilestone, due_date: '' }, }); expect(wrapper.vm.isMilestonePastDue).toBe(false); @@ -68,9 +62,7 @@ describe('IssueMilestoneComponent', () => { it('should return `true` when milestone due is past current date', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { - due_date: '1990-07-22', - }), + milestone: { ...mockMilestone, due_date: '1990-07-22' }, }); expect(wrapper.vm.isMilestonePastDue).toBe(true); @@ -84,9 +76,7 @@ describe('IssueMilestoneComponent', () => { it('returns string containing absolute milestone start date when due date is not present', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { - due_date: '', - }), + milestone: { ...mockMilestone, due_date: '' }, }); expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)'); @@ -94,10 +84,7 @@ describe('IssueMilestoneComponent', () => { it('returns empty string when both milestone start and due dates are not present', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { - start_date: '', - due_date: '', - }), + milestone: { ...mockMilestone, start_date: '', due_date: '' }, }); expect(wrapper.vm.milestoneDatesAbsolute).toBe(''); @@ -107,9 +94,7 @@ describe('IssueMilestoneComponent', () => { describe('milestoneDatesHuman', () => { it('returns string containing milestone due date when date is yet to be due', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { - due_date: `${new Date().getFullYear() + 10}-01-01`, - }), + milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` }, }); expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining'); @@ -117,10 +102,7 @@ describe('IssueMilestoneComponent', () => { it('returns string containing milestone start date when date has already started and due date is not present', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { - start_date: '1990-07-22', - due_date: '', - }), + milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' }, }); expect(wrapper.vm.milestoneDatesHuman).toContain('Started'); @@ -128,10 +110,11 @@ describe('IssueMilestoneComponent', () => { it('returns string containing milestone start date when date is yet to start and due date is not present', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { + milestone: { + ...mockMilestone, start_date: `${new Date().getFullYear() + 10}-01-01`, due_date: '', - }), + }, }); expect(wrapper.vm.milestoneDatesHuman).toContain('Starts'); @@ -139,10 +122,7 @@ describe('IssueMilestoneComponent', () => { it('returns empty string when milestone start and due dates are not present', () => { wrapper.setProps({ - milestone: Object.assign({}, mockMilestone, { - start_date: '', - due_date: '', - }), + milestone: { ...mockMilestone, start_date: '', due_date: '' }, }); expect(wrapper.vm.milestoneDatesHuman).toBe(''); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js index e2e11c94c0d..a4121448492 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js @@ -8,11 +8,12 @@ import { mockLabels, } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; -const componentConfig = Object.assign({}, mockConfig, { +const componentConfig = { + ...mockConfig, fieldName: 'label_id[]', labels: mockLabels, showExtraOptions: false, -}); +}; const createComponent = (config = componentConfig) => { const Component = Vue.extend(dropdownButtonComponent); @@ -34,7 +35,7 @@ describe('DropdownButtonComponent', () => { describe('computed', () => { describe('dropdownToggleText', () => { it('returns text as `Label` when `labels` prop is empty array', () => { - const mockEmptyLabels = Object.assign({}, componentConfig, { labels: [] }); + const mockEmptyLabels = { ...componentConfig, labels: [] }; const vmEmptyLabels = createComponent(mockEmptyLabels); expect(vmEmptyLabels.dropdownToggleText).toBe('Label'); @@ -42,9 +43,7 @@ describe('DropdownButtonComponent', () => { }); it('returns first label name with remaining label count when `labels` prop has more than one item', () => { - const mockMoreLabels = Object.assign({}, componentConfig, { - labels: mockLabels.concat(mockLabels), - }); + const mockMoreLabels = { ...componentConfig, labels: mockLabels.concat(mockLabels) }; const vmMoreLabels = createComponent(mockMoreLabels); expect(vmMoreLabels.dropdownToggleText).toBe( @@ -54,9 +53,7 @@ describe('DropdownButtonComponent', () => { }); it('returns first label name when `labels` prop has only one item present', () => { - const singleLabel = Object.assign({}, componentConfig, { - labels: [mockLabels[0]], - }); + const singleLabel = { ...componentConfig, labels: [mockLabels[0]] }; const vmSingleLabel = createComponent(singleLabel); expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js index 6e2363ba96f..072d8fe2fe2 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js @@ -15,7 +15,7 @@ describe('LabelsSelect Actions', () => { }; beforeEach(() => { - state = Object.assign({}, defaultState()); + state = { ...defaultState() }; }); describe('setInitialState', () => { diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb index 5263de117cf..19fe9fc3900 100644 --- a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb +++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb @@ -9,7 +9,7 @@ describe Mutations::AlertManagement::UpdateAlertStatus do let(:new_status) { 'acknowledged' } let(:args) { { status: new_status, project_path: project.full_path, iid: alert.iid } } - specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alerts) } + specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) } describe '#resolve' do subject(:resolve) { mutation_for(project, current_user).resolve(args) } diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb index aefd88f7645..65291f31422 100644 --- a/spec/graphql/types/alert_management/alert_type_spec.rb +++ b/spec/graphql/types/alert_management/alert_type_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe GitlabSchema.types['AlertManagementAlert'] do specify { expect(described_class.graphql_name).to eq('AlertManagementAlert') } - specify { expect(described_class).to require_graphql_authorizations(:read_alert_management_alerts) } + specify { expect(described_class).to require_graphql_authorizations(:read_alert_management_alert) } it 'exposes the expected fields' do expected_fields = %i[ diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb index 282758679cb..de4086e48db 100644 --- a/spec/helpers/releases_helper_spec.rb +++ b/spec/helpers/releases_helper_spec.rb @@ -54,7 +54,9 @@ describe ReleasesHelper do markdown_docs_path releases_page_path update_release_api_docs_path - release_assets_docs_path) + release_assets_docs_path + manage_milestones_path + new_milestone_path) expect(helper.data_for_edit_release_page.keys).to eq(keys) end end diff --git a/spec/javascripts/emoji_spec.js b/spec/javascripts/emoji_spec.js index 3db4d9800f1..25bc95e0dd6 100644 --- a/spec/javascripts/emoji_spec.js +++ b/spec/javascripts/emoji_spec.js @@ -69,7 +69,7 @@ const defaults = { }; function testGlEmojiElement(element, name, unicodeVersion, unicodeMoji, options = {}) { - const opts = Object.assign({}, defaults, options); + const opts = { ...defaults, ...options }; expect(element.tagName.toLowerCase()).toBe('gl-emoji'); expect(element.dataset.name).toBe(name); expect(element.dataset.fallbackSrc.length).toBeGreaterThan(0); @@ -383,9 +383,7 @@ describe('gl_emoji', () => { it('bomb(6.0) with 6.0 support', () => { const emojiKey = 'bomb'; - const unicodeSupportMap = Object.assign({}, emptySupportMap, { - '6.0': true, - }); + const unicodeSupportMap = { ...emptySupportMap, '6.0': true }; const isSupported = isEmojiUnicodeSupported( unicodeSupportMap, emojiFixtureMap[emojiKey].moji, @@ -409,9 +407,7 @@ describe('gl_emoji', () => { it('bomb(6.0) without 6.0 but with 9.0 support', () => { const emojiKey = 'bomb'; - const unicodeSupportMap = Object.assign({}, emptySupportMap, { - '9.0': true, - }); + const unicodeSupportMap = { ...emptySupportMap, '9.0': true }; const isSupported = isEmojiUnicodeSupported( unicodeSupportMap, emojiFixtureMap[emojiKey].moji, @@ -423,7 +419,8 @@ describe('gl_emoji', () => { it('construction_worker_tone5(8.0) without skin tone modifier support', () => { const emojiKey = 'construction_worker_tone5'; - const unicodeSupportMap = Object.assign({}, emptySupportMap, { + const unicodeSupportMap = { + ...emptySupportMap, skinToneModifier: false, '9.0': true, '8.0': true, @@ -437,7 +434,7 @@ describe('gl_emoji', () => { 3.2: true, '3.0': true, 1.1: true, - }); + }; const isSupported = isEmojiUnicodeSupported( unicodeSupportMap, emojiFixtureMap[emojiKey].moji, @@ -449,13 +446,14 @@ describe('gl_emoji', () => { it('use native keycap on >=57 chrome', () => { const emojiKey = 'five'; - const unicodeSupportMap = Object.assign({}, emptySupportMap, { + const unicodeSupportMap = { + ...emptySupportMap, '3.0': true, meta: { isChrome: true, chromeVersion: 57, }, - }); + }; const isSupported = isEmojiUnicodeSupported( unicodeSupportMap, emojiFixtureMap[emojiKey].moji, @@ -467,13 +465,14 @@ describe('gl_emoji', () => { it('fallback keycap on <57 chrome', () => { const emojiKey = 'five'; - const unicodeSupportMap = Object.assign({}, emptySupportMap, { + const unicodeSupportMap = { + ...emptySupportMap, '3.0': true, meta: { isChrome: true, chromeVersion: 50, }, - }); + }; const isSupported = isEmojiUnicodeSupported( unicodeSupportMap, emojiFixtureMap[emojiKey].moji, diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 00bc552bd7d..06f76c581f2 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -44,19 +44,17 @@ describe('glDropdown', function describeDropdown() { }; function initDropDown(hasRemote, isFilterable, extraOpts = {}) { - const options = Object.assign( - { - selectable: true, - filterable: isFilterable, - data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData, - search: { - fields: ['name'], - }, - text: project => project.name_with_namespace || project.name, - id: project => project.id, + const options = { + selectable: true, + filterable: isFilterable, + data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData, + search: { + fields: ['name'], }, - extraOpts, - ); + text: project => project.name_with_namespace || project.name, + id: project => project.id, + ...extraOpts, + }; this.dropdownButtonElement = $( '#js-project-dropdown', this.dropdownContainerElement, diff --git a/spec/javascripts/image_diff/image_badge_spec.js b/spec/javascripts/image_diff/image_badge_spec.js index 2b23dce5d30..a1589d7b7a0 100644 --- a/spec/javascripts/image_diff/image_badge_spec.js +++ b/spec/javascripts/image_diff/image_badge_spec.js @@ -10,11 +10,7 @@ describe('ImageBadge', () => { }; it('should save actual property', () => { - const imageBadge = new ImageBadge( - Object.assign({}, options, { - actual: imageMeta, - }), - ); + const imageBadge = new ImageBadge({ ...options, actual: imageMeta }); const { actual } = imageBadge; @@ -25,11 +21,7 @@ describe('ImageBadge', () => { }); it('should save browser property', () => { - const imageBadge = new ImageBadge( - Object.assign({}, options, { - browser: imageMeta, - }), - ); + const imageBadge = new ImageBadge({ ...options, browser: imageMeta }); const { browser } = imageBadge; @@ -83,11 +75,7 @@ describe('ImageBadge', () => { }); it('should generate browser property', () => { - const imageBadge = new ImageBadge( - Object.assign({}, options, { - imageEl: document.createElement('img'), - }), - ); + const imageBadge = new ImageBadge({ ...options, imageEl: document.createElement('img') }); expect(imageDiffHelper.resizeCoordinatesToImageElement).toHaveBeenCalled(); expect(imageBadge.browser).toEqual(true); diff --git a/spec/javascripts/jobs/components/commit_block_spec.js b/spec/javascripts/jobs/components/commit_block_spec.js index c02f564d01a..4e2d0053831 100644 --- a/spec/javascripts/jobs/components/commit_block_spec.js +++ b/spec/javascripts/jobs/components/commit_block_spec.js @@ -66,7 +66,7 @@ describe('Commit block', () => { describe('without merge request', () => { it('does not render merge request', () => { - const copyProps = Object.assign({}, props); + const copyProps = { ...props }; delete copyProps.mergeRequest; vm = mountComponent(Component, { diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index 740bc3d0491..0c8e2dc3aef 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -20,7 +20,7 @@ describe('Sidebar details block', () => { describe('when there is no retry path retry', () => { it('should not render a retry button', () => { - const copy = Object.assign({}, job); + const copy = { ...job }; delete copy.retry_path; store.dispatch('receiveJobSuccess', copy); @@ -43,10 +43,7 @@ describe('Sidebar details block', () => { describe('with terminal path', () => { it('renders terminal link', () => { - store.dispatch( - 'receiveJobSuccess', - Object.assign({}, job, { terminal_path: 'job/43123/terminal' }), - ); + store.dispatch('receiveJobSuccess', { ...job, terminal_path: 'job/43123/terminal' }); vm = mountComponentWithStore(SidebarComponent, { store, }); diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js deleted file mode 100644 index f876987cd88..00000000000 --- a/spec/javascripts/pipelines/mock_data.js +++ /dev/null @@ -1,423 +0,0 @@ -export const pipelineWithStages = { - id: 20333396, - user: { - id: 128633, - name: 'Rémy Coutable', - username: 'rymai', - state: 'active', - avatar_url: - 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', - web_url: 'https://gitlab.com/rymai', - path: '/rymai', - }, - active: true, - coverage: '58.24', - source: 'push', - created_at: '2018-04-11T14:04:53.881Z', - updated_at: '2018-04-11T14:05:00.792Z', - path: '/gitlab-org/gitlab/pipelines/20333396', - flags: { - latest: true, - stuck: false, - auto_devops: false, - yaml_errors: false, - retryable: false, - cancelable: true, - failure_reason: false, - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', - }, - duration: null, - finished_at: null, - stages: [ - { - name: 'build', - title: 'build: skipped', - status: { - icon: 'status_skipped', - text: 'skipped', - label: 'skipped', - group: 'skipped', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#build', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_skipped-a2eee568a5bffdb494050c7b62dde241de9189280836288ac8923d369f16222d.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#build', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=build', - }, - { - name: 'prepare', - title: 'prepare: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#prepare', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_success-26f59841becbef8c6fe414e9e74471d8bfd6a91b5855c19fe7f5923a40a7da47.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#prepare', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=prepare', - }, - { - name: 'test', - title: 'test: running', - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#test', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#test', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=test', - }, - { - name: 'post-test', - title: 'post-test: created', - status: { - icon: 'status_created', - text: 'created', - label: 'created', - group: 'created', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#post-test', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#post-test', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-test', - }, - { - name: 'pages', - title: 'pages: created', - status: { - icon: 'status_created', - text: 'created', - label: 'created', - group: 'created', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#pages', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#pages', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=pages', - }, - { - name: 'post-cleanup', - title: 'post-cleanup: created', - status: { - icon: 'status_created', - text: 'created', - label: 'created', - group: 'created', - has_details: true, - details_path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup', - favicon: - 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', - }, - path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup', - dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-cleanup', - }, - ], - artifacts: [ - { - name: 'gitlab:assets:compile', - expired: false, - expire_at: '2018-05-12T14:22:54.730Z', - path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/browse', - }, - { - name: 'rspec-mysql 12 28', - expired: false, - expire_at: '2018-05-12T14:22:45.136Z', - path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/browse', - }, - { - name: 'rspec-mysql 6 28', - expired: false, - expire_at: '2018-05-12T14:22:41.523Z', - path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/browse', - }, - { - name: 'rspec-pg geo 0 1', - expired: false, - expire_at: '2018-05-12T14:22:13.287Z', - path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/browse', - }, - { - name: 'rspec-mysql 0 28', - expired: false, - expire_at: '2018-05-12T14:22:06.834Z', - path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/browse', - }, - { - name: 'spinach-mysql 0 2', - expired: false, - expire_at: '2018-05-12T14:21:51.409Z', - path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/browse', - }, - { - name: 'karma', - expired: false, - expire_at: '2018-05-12T14:21:20.934Z', - path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/browse', - }, - { - name: 'spinach-pg 0 2', - expired: false, - expire_at: '2018-05-12T14:20:01.028Z', - path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/browse', - }, - { - name: 'spinach-pg 1 2', - expired: false, - expire_at: '2018-05-12T14:19:04.336Z', - path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/browse', - }, - { - name: 'sast', - expired: null, - expire_at: null, - path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/download', - browse_path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/browse', - }, - { - name: 'code_quality', - expired: false, - expire_at: '2018-04-18T14:16:24.484Z', - path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/browse', - }, - { - name: 'cache gems', - expired: null, - expire_at: null, - path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/download', - browse_path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/browse', - }, - { - name: 'dependency_scanning', - expired: null, - expire_at: null, - path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/download', - browse_path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/browse', - }, - { - name: 'compile-assets', - expired: false, - expire_at: '2018-04-18T14:12:07.638Z', - path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/browse', - }, - { - name: 'setup-test-env', - expired: false, - expire_at: '2018-04-18T14:10:27.024Z', - path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/browse', - }, - { - name: 'retrieve-tests-metadata', - expired: false, - expire_at: '2018-05-12T14:06:35.926Z', - path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/download', - keep_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/keep', - browse_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/browse', - }, - ], - manual_actions: [ - { - name: 'package-and-qa', - path: '/gitlab-org/gitlab/-/jobs/62411330/play', - playable: true, - }, - { - name: 'review-docs-deploy', - path: '/gitlab-org/gitlab/-/jobs/62411332/play', - playable: true, - }, - ], - }, - ref: { - name: 'master', - path: '/gitlab-org/gitlab/commits/master', - tag: false, - branch: true, - }, - commit: { - id: 'e6a2885c503825792cb8a84a8731295e361bd059', - short_id: 'e6a2885c', - title: "Merge branch 'ce-to-ee-2018-04-11' into 'master'", - created_at: '2018-04-11T14:04:39.000Z', - parent_ids: [ - '5d9b5118f6055f72cff1a82b88133609912f2c1d', - '6fdc6ee76a8062fe41b1a33f7c503334a6ebdc02', - ], - message: - "Merge branch 'ce-to-ee-2018-04-11' into 'master'\n\nCE upstream - 2018-04-11 12:26 UTC\n\nSee merge request gitlab-org/gitlab-ee!5326", - author_name: 'Rémy Coutable', - author_email: 'remy@rymai.me', - authored_date: '2018-04-11T14:04:39.000Z', - committer_name: 'Rémy Coutable', - committer_email: 'remy@rymai.me', - committed_date: '2018-04-11T14:04:39.000Z', - author: { - id: 128633, - name: 'Rémy Coutable', - username: 'rymai', - state: 'active', - avatar_url: - 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', - web_url: 'https://gitlab.com/rymai', - path: '/rymai', - }, - author_gravatar_url: - 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', - commit_url: - 'https://gitlab.com/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059', - commit_path: '/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059', - }, - cancel_path: '/gitlab-org/gitlab/pipelines/20333396/cancel', - triggered_by: null, - triggered: [], -}; - -export const stageReply = { - name: 'deploy', - title: 'deploy: running', - latest_statuses: [ - { - id: 928, - name: 'stop staging', - started: false, - build_path: '/twitter/flight/-/jobs/928', - cancel_path: '/twitter/flight/-/jobs/928/cancel', - playable: false, - created_at: '2018-04-04T20:02:02.728Z', - updated_at: '2018-04-04T20:02:02.766Z', - status: { - icon: 'status_pending', - text: 'pending', - label: 'pending', - group: 'pending', - tooltip: 'pending', - has_details: true, - details_path: '/twitter/flight/-/jobs/928', - favicon: - '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico', - action: { - icon: 'cancel', - title: 'Cancel', - path: '/twitter/flight/-/jobs/928/cancel', - method: 'post', - }, - }, - }, - { - id: 926, - name: 'production', - started: false, - build_path: '/twitter/flight/-/jobs/926', - retry_path: '/twitter/flight/-/jobs/926/retry', - play_path: '/twitter/flight/-/jobs/926/play', - playable: true, - created_at: '2018-04-04T20:00:57.202Z', - updated_at: '2018-04-04T20:11:13.110Z', - status: { - icon: 'status_canceled', - text: 'canceled', - label: 'manual play action', - group: 'canceled', - tooltip: 'canceled', - has_details: true, - details_path: '/twitter/flight/-/jobs/926', - favicon: - '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico', - action: { - icon: 'play', - title: 'Play', - path: '/twitter/flight/-/jobs/926/play', - method: 'post', - }, - }, - }, - { - id: 217, - name: 'staging', - started: '2018-03-07T08:41:46.234Z', - build_path: '/twitter/flight/-/jobs/217', - retry_path: '/twitter/flight/-/jobs/217/retry', - playable: false, - created_at: '2018-03-07T14:41:58.093Z', - updated_at: '2018-03-07T14:41:58.093Z', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/twitter/flight/-/jobs/217', - favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', - action: { - icon: 'retry', - title: 'Retry', - path: '/twitter/flight/-/jobs/217/retry', - method: 'post', - }, - }, - }, - ], - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - tooltip: 'running', - has_details: true, - details_path: '/twitter/flight/pipelines/13#deploy', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - path: '/twitter/flight/pipelines/13#deploy', - dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy', -}; diff --git a/spec/javascripts/pipelines/stores/pipeline.json b/spec/javascripts/pipelines/stores/pipeline.json deleted file mode 100644 index 7d5891d3d52..00000000000 --- a/spec/javascripts/pipelines/stores/pipeline.json +++ /dev/null @@ -1,167 +0,0 @@ -{ - "id": 37232567, - "user": { - "id": 113870, - "name": "Phil Hughes", - "username": "iamphill", - "state": "active", - "avatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon", - "web_url": "https://gitlab.com/iamphill", - "status_tooltip_html": null, - "path": "/iamphill" - }, - "active": false, - "coverage": null, - "source": "push", - "created_at": "2018-11-20T10:22:52.617Z", - "updated_at": "2018-11-20T10:24:09.511Z", - "path": "/gitlab-org/gl-vue-cli/pipelines/37232567", - "flags": { - "latest": true, - "stuck": false, - "auto_devops": false, - "yaml_errors": false, - "retryable": false, - "cancelable": false, - "failure_reason": false - }, - "details": { - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gl-vue-cli/pipelines/37232567", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "duration": 65, - "finished_at": "2018-11-20T10:24:09.483Z", - "stages": [ - { - "name": "test", - "title": "test: passed", - "groups": [ - { - "name": "eslint", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 122845352, - "name": "eslint", - "started": "2018-11-20T10:22:53.369Z", - "archived": false, - "build_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352", - "retry_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry", - "playable": false, - "scheduled": false, - "created_at": "2018-11-20T10:22:52.630Z", - "updated_at": "2018-11-20T10:23:58.948Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gl-vue-cli/pipelines/37232567#test", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/gitlab-org/gl-vue-cli/pipelines/37232567#test", - "dropdown_path": "/gitlab-org/gl-vue-cli/pipelines/37232567/stage.json?stage=test" - } - ], - "artifacts": [], - "manual_actions": [], - "scheduled_actions": [] - }, - "ref": { - "name": "master", - "path": "/gitlab-org/gl-vue-cli/commits/master", - "tag": false, - "branch": true - }, - "commit": { - "id": "8f179601d481950bcb67032caeb33d1c24dde6bd", - "short_id": "8f179601", - "title": "Merge branch 'gl-cli-startt' into 'master'", - "created_at": "2018-11-20T10:22:51.000Z", - "parent_ids": [ - "781d78fcd3d6c17ccf208f0cf0ab47c3e5397118", - "d227a0bb858c48eeee7393fcade1a33748f35183" - ], - "message": "Merge branch 'gl-cli-startt' into 'master'\n\nFirst iteration of the CLI\n\nCloses gitlab-foss#53657\n\nSee merge request gitlab-org/gl-vue-cli!2", - "author_name": "Phil Hughes", - "author_email": "me@iamphill.com", - "authored_date": "2018-11-20T10:22:51.000Z", - "committer_name": "Phil Hughes", - "committer_email": "me@iamphill.com", - "committed_date": "2018-11-20T10:22:51.000Z", - "author": { - "id": 113870, - "name": "Phil Hughes", - "username": "iamphill", - "state": "active", - "avatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon", - "web_url": "https://gitlab.com/iamphill", - "status_tooltip_html": null, - "path": "/iamphill" - }, - "author_gravatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon", - "commit_url": "https://gitlab.com/gitlab-org/gl-vue-cli/commit/8f179601d481950bcb67032caeb33d1c24dde6bd", - "commit_path": "/gitlab-org/gl-vue-cli/commit/8f179601d481950bcb67032caeb33d1c24dde6bd" - }, - "triggered_by": null, - "triggered": [] -} diff --git a/spec/javascripts/pipelines/stores/pipeline_with_triggered.json b/spec/javascripts/pipelines/stores/pipeline_with_triggered.json deleted file mode 100644 index 1fa15e45792..00000000000 --- a/spec/javascripts/pipelines/stores/pipeline_with_triggered.json +++ /dev/null @@ -1,381 +0,0 @@ -{ - "id": 23211253, - "user": { - "id": 3585, - "name": "Achilleas Pipinellis", - "username": "axil", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png", - "web_url": "https://gitlab.com/axil", - "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"trumpet\" data-name=\"trumpet\" data-unicode-version=\"6.0\"\u003e🎺\u003c/gl-emoji\u003e\u003c/span\u003e", - "path": "/axil" - }, - "active": false, - "coverage": null, - "source": "push", - "created_at": "2018-06-05T11:31:30.452Z", - "updated_at": "2018-10-31T16:35:31.305Z", - "path": "/gitlab-org/gitlab-runner/pipelines/23211253", - "flags": { - "latest": false, - "stuck": false, - "auto_devops": false, - "yaml_errors": false, - "retryable": false, - "cancelable": false, - "failure_reason": false - }, - "details": { - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "duration": 53, - "finished_at": "2018-10-31T16:35:31.299Z", - "stages": [ - { - "name": "prebuild", - "title": "prebuild: passed", - "groups": [ - { - "name": "review-docs-deploy", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "method": "post", - "button_title": "Trigger this manual action" - } - }, - "jobs": [ - { - "id": 72469032, - "name": "review-docs-deploy", - "started": "2018-10-31T16:34:58.778Z", - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry", - "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "playable": true, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.495Z", - "updated_at": "2018-10-31T16:35:31.251Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "method": "post", - "button_title": "Trigger this manual action" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild" - }, - { - "name": "test", - "title": "test: passed", - "groups": [ - { - "name": "docs check links", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 72469033, - "name": "docs check links", - "started": "2018-06-05T11:31:33.240Z", - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "playable": false, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.627Z", - "updated_at": "2018-06-05T11:31:54.363Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test" - }, - { - "name": "cleanup", - "title": "cleanup: skipped", - "groups": [ - { - "name": "review-docs-cleanup", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual stop action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "stop", - "title": "Stop", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "method": "post", - "button_title": "Stop this environment" - } - }, - "jobs": [ - { - "id": 72469034, - "name": "review-docs-cleanup", - "started": null, - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "playable": true, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.760Z", - "updated_at": "2018-06-05T11:31:56.037Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual stop action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "stop", - "title": "Stop", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "method": "post", - "button_title": "Stop this environment" - } - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup" - } - ], - "artifacts": [], - "manual_actions": [ - { - "name": "review-docs-cleanup", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "playable": true, - "scheduled": false - }, - { - "name": "review-docs-deploy", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "playable": true, - "scheduled": false - } - ], - "scheduled_actions": [] - }, - "ref": { - "name": "docs/add-development-guide-to-readme", - "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme", - "tag": false, - "branch": true - }, - "commit": { - "id": "8083eb0a920572214d0dccedd7981f05d535ad46", - "short_id": "8083eb0a", - "title": "Add link to development guide in readme", - "created_at": "2018-06-05T11:30:48.000Z", - "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"], - "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n", - "author_name": "Achilleas Pipinellis", - "author_email": "axil@gitlab.com", - "authored_date": "2018-06-05T11:30:48.000Z", - "committer_name": "Achilleas Pipinellis", - "committer_email": "axil@gitlab.com", - "committed_date": "2018-06-05T11:30:48.000Z", - "author": { - "id": 3585, - "name": "Achilleas Pipinellis", - "username": "axil", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png", - "web_url": "https://gitlab.com/axil", - "status_tooltip_html": null, - "path": "/axil" - }, - "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon", - "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46", - "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46" - }, - "triggered_by": null, - "triggered": [ - { - "id": 34993051, - "user": { - "id": 376774, - "name": "Alessio Caiazza", - "username": "nolith", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png", - "web_url": "https://gitlab.com/nolith", - "status_tooltip_html": null, - "path": "/nolith" - }, - "active": false, - "coverage": null, - "source": "pipeline", - "path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "details": { - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - } - }, - "project": { - "id": 1794617, - "name": "GitLab Docs", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - } - } - ] -} diff --git a/spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json b/spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json deleted file mode 100644 index 7aeea6f3ebb..00000000000 --- a/spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json +++ /dev/null @@ -1,379 +0,0 @@ -{ - "id": 23211253, - "user": { - "id": 3585, - "name": "Achilleas Pipinellis", - "username": "axil", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png", - "web_url": "https://gitlab.com/axil", - "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"trumpet\" data-name=\"trumpet\" data-unicode-version=\"6.0\"\u003e🎺\u003c/gl-emoji\u003e\u003c/span\u003e", - "path": "/axil" - }, - "active": false, - "coverage": null, - "source": "push", - "created_at": "2018-06-05T11:31:30.452Z", - "updated_at": "2018-10-31T16:35:31.305Z", - "path": "/gitlab-org/gitlab-runner/pipelines/23211253", - "flags": { - "latest": false, - "stuck": false, - "auto_devops": false, - "yaml_errors": false, - "retryable": false, - "cancelable": false, - "failure_reason": false - }, - "details": { - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "duration": 53, - "finished_at": "2018-10-31T16:35:31.299Z", - "stages": [ - { - "name": "prebuild", - "title": "prebuild: passed", - "groups": [ - { - "name": "review-docs-deploy", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "method": "post", - "button_title": "Trigger this manual action" - } - }, - "jobs": [ - { - "id": 72469032, - "name": "review-docs-deploy", - "started": "2018-10-31T16:34:58.778Z", - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry", - "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "playable": true, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.495Z", - "updated_at": "2018-10-31T16:35:31.251Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "method": "post", - "button_title": "Trigger this manual action" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild" - }, - { - "name": "test", - "title": "test: passed", - "groups": [ - { - "name": "docs check links", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 72469033, - "name": "docs check links", - "started": "2018-06-05T11:31:33.240Z", - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "playable": false, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.627Z", - "updated_at": "2018-06-05T11:31:54.363Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test" - }, - { - "name": "cleanup", - "title": "cleanup: skipped", - "groups": [ - { - "name": "review-docs-cleanup", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual stop action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "stop", - "title": "Stop", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "method": "post", - "button_title": "Stop this environment" - } - }, - "jobs": [ - { - "id": 72469034, - "name": "review-docs-cleanup", - "started": null, - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "playable": true, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.760Z", - "updated_at": "2018-06-05T11:31:56.037Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual stop action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "stop", - "title": "Stop", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "method": "post", - "button_title": "Stop this environment" - } - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup" - } - ], - "artifacts": [], - "manual_actions": [ - { - "name": "review-docs-cleanup", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "playable": true, - "scheduled": false - }, - { - "name": "review-docs-deploy", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "playable": true, - "scheduled": false - } - ], - "scheduled_actions": [] - }, - "ref": { - "name": "docs/add-development-guide-to-readme", - "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme", - "tag": false, - "branch": true - }, - "commit": { - "id": "8083eb0a920572214d0dccedd7981f05d535ad46", - "short_id": "8083eb0a", - "title": "Add link to development guide in readme", - "created_at": "2018-06-05T11:30:48.000Z", - "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"], - "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n", - "author_name": "Achilleas Pipinellis", - "author_email": "axil@gitlab.com", - "authored_date": "2018-06-05T11:30:48.000Z", - "committer_name": "Achilleas Pipinellis", - "committer_email": "axil@gitlab.com", - "committed_date": "2018-06-05T11:30:48.000Z", - "author": { - "id": 3585, - "name": "Achilleas Pipinellis", - "username": "axil", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png", - "web_url": "https://gitlab.com/axil", - "status_tooltip_html": null, - "path": "/axil" - }, - "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon", - "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46", - "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46" - }, - "triggered_by": { - "id": 34993051, - "user": { - "id": 376774, - "name": "Alessio Caiazza", - "username": "nolith", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png", - "web_url": "https://gitlab.com/nolith", - "status_tooltip_html": null, - "path": "/nolith" - }, - "active": false, - "coverage": null, - "source": "pipeline", - "path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "details": { - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - } - }, - "project": { - "id": 1794617, - "name": "GitLab Docs", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - } - }, - "triggered": [] -} diff --git a/spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json b/spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json deleted file mode 100644 index 2402cbae6c8..00000000000 --- a/spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "id": 23211253, - "user": { - "id": 3585, - "name": "Achilleas Pipinellis", - "username": "axil", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png", - "web_url": "https://gitlab.com/axil", - "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"trumpet\" data-name=\"trumpet\" data-unicode-version=\"6.0\"\u003e🎺\u003c/gl-emoji\u003e\u003c/span\u003e", - "path": "/axil" - }, - "active": false, - "coverage": null, - "source": "push", - "created_at": "2018-06-05T11:31:30.452Z", - "updated_at": "2018-10-31T16:35:31.305Z", - "path": "/gitlab-org/gitlab-runner/pipelines/23211253", - "flags": { - "latest": false, - "stuck": false, - "auto_devops": false, - "yaml_errors": false, - "retryable": false, - "cancelable": false, - "failure_reason": false - }, - "details": { - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "duration": 53, - "finished_at": "2018-10-31T16:35:31.299Z", - "stages": [ - { - "name": "prebuild", - "title": "prebuild: passed", - "groups": [ - { - "name": "review-docs-deploy", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "method": "post", - "button_title": "Trigger this manual action" - } - }, - "jobs": [ - { - "id": 72469032, - "name": "review-docs-deploy", - "started": "2018-10-31T16:34:58.778Z", - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry", - "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "playable": true, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.495Z", - "updated_at": "2018-10-31T16:35:31.251Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "manual play action", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "play", - "title": "Play", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "method": "post", - "button_title": "Trigger this manual action" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild" - }, - { - "name": "test", - "title": "test: passed", - "groups": [ - { - "name": "docs check links", - "size": 1, - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "method": "post", - "button_title": "Retry this job" - } - }, - "jobs": [ - { - "id": 72469033, - "name": "docs check links", - "started": "2018-06-05T11:31:33.240Z", - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "playable": false, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.627Z", - "updated_at": "2018-06-05T11:31:54.363Z", - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg", - "size": "svg-430", - "title": "This job does not have a trace." - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry", - "method": "post", - "button_title": "Retry this job" - } - } - } - ] - } - ], - "status": { - "icon": "status_success", - "text": "passed", - "label": "passed", - "group": "success", - "tooltip": "passed", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test" - }, - { - "name": "cleanup", - "title": "cleanup: skipped", - "groups": [ - { - "name": "review-docs-cleanup", - "size": 1, - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual stop action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "stop", - "title": "Stop", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "method": "post", - "button_title": "Stop this environment" - } - }, - "jobs": [ - { - "id": 72469034, - "name": "review-docs-cleanup", - "started": null, - "archived": false, - "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "playable": true, - "scheduled": false, - "created_at": "2018-06-05T11:31:30.760Z", - "updated_at": "2018-06-05T11:31:56.037Z", - "status": { - "icon": "status_manual", - "text": "manual", - "label": "manual stop action", - "group": "manual", - "tooltip": "manual action", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034", - "illustration": { - "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg", - "size": "svg-394", - "title": "This job requires a manual action", - "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments" - }, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png", - "action": { - "icon": "stop", - "title": "Stop", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "method": "post", - "button_title": "Stop this environment" - } - } - } - ] - } - ], - "status": { - "icon": "status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "tooltip": "skipped", - "has_details": true, - "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png" - }, - "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup", - "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup" - } - ], - "artifacts": [], - "manual_actions": [ - { - "name": "review-docs-cleanup", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play", - "playable": true, - "scheduled": false - }, - { - "name": "review-docs-deploy", - "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play", - "playable": true, - "scheduled": false - } - ], - "scheduled_actions": [] - }, - "ref": { - "name": "docs/add-development-guide-to-readme", - "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme", - "tag": false, - "branch": true - }, - "commit": { - "id": "8083eb0a920572214d0dccedd7981f05d535ad46", - "short_id": "8083eb0a", - "title": "Add link to development guide in readme", - "created_at": "2018-06-05T11:30:48.000Z", - "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"], - "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n", - "author_name": "Achilleas Pipinellis", - "author_email": "axil@gitlab.com", - "authored_date": "2018-06-05T11:30:48.000Z", - "committer_name": "Achilleas Pipinellis", - "committer_email": "axil@gitlab.com", - "committed_date": "2018-06-05T11:30:48.000Z", - "author": { - "id": 3585, - "name": "Achilleas Pipinellis", - "username": "axil", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png", - "web_url": "https://gitlab.com/axil", - "status_tooltip_html": null, - "path": "/axil" - }, - "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon", - "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46", - "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46" - }, - "triggered_by": { - "id": 34993051, - "user": { - "id": 376774, - "name": "Alessio Caiazza", - "username": "nolith", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png", - "web_url": "https://gitlab.com/nolith", - "status_tooltip_html": null, - "path": "/nolith" - }, - "active": false, - "coverage": null, - "source": "pipeline", - "path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "details": { - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - } - }, - "project": { - "id": 1794617, - "name": "GitLab Docs", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - } - }, - "triggered": [ - { - "id": 349233051, - "user": { - "id": 376774, - "name": "Alessio Caiazza", - "username": "nolith", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png", - "web_url": "https://gitlab.com/nolith", - "status_tooltip_html": null, - "path": "/nolith" - }, - "active": false, - "coverage": null, - "source": "pipeline", - "path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "details": { - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/349233051", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - } - }, - "project": { - "id": 1794617, - "name": "GitLab Docs", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - } - }, - { - "id": 34993023, - "user": { - "id": 376774, - "name": "Alessio Caiazza", - "username": "nolith", - "state": "active", - "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png", - "web_url": "https://gitlab.com/nolith", - "status_tooltip_html": null, - "path": "/nolith" - }, - "active": false, - "coverage": null, - "source": "pipeline", - "path": "/gitlab-com/gitlab-docs/pipelines/34993023", - "details": { - "status": { - "icon": "status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "tooltip": "failed", - "has_details": true, - "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051", - "illustration": null, - "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png" - } - }, - "project": { - "id": 1794617, - "name": "GitLab Docs", - "full_path": "/gitlab-com/gitlab-docs", - "full_name": "GitLab.com / GitLab Docs" - } - } - ] -} diff --git a/spec/javascripts/reports/components/test_issue_body_spec.js b/spec/javascripts/reports/components/test_issue_body_spec.js index 9c1cec4c9bc..a55719a9d36 100644 --- a/spec/javascripts/reports/components/test_issue_body_spec.js +++ b/spec/javascripts/reports/components/test_issue_body_spec.js @@ -40,7 +40,7 @@ describe('Test Issue body', () => { beforeEach(() => { vm = mountComponentWithStore(Component, { store, - props: Object.assign({}, commonProps, { isNew: true }), + props: { ...commonProps, isNew: true }, }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index de1d351677c..3cbaa47c832 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -153,7 +153,7 @@ describe('MRWidgetHeader', () => { beforeEach(() => { vm = mountComponent(Component, { - mr: Object.assign({}, mrDefaultOptions), + mr: { ...mrDefaultOptions }, }); }); @@ -176,7 +176,7 @@ describe('MRWidgetHeader', () => { }); it('renders web ide button in disabled state with no href', () => { - const mr = Object.assign({}, mrDefaultOptions, { canPushToSourceBranch: false }); + const mr = { ...mrDefaultOptions, canPushToSourceBranch: false }; vm = mountComponent(Component, { mr }); const link = vm.$el.querySelector('.js-web-ide'); diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js index 6a83790093a..a8acecdd3fc 100644 --- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -30,11 +30,7 @@ describe('DiffViewer', () => { relative_url_root: '', }; - createComponent( - Object.assign({}, requiredProps, { - projectPath: '', - }), - ); + createComponent({ ...requiredProps, projectPath: '' }); setTimeout(() => { expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe( @@ -50,13 +46,12 @@ describe('DiffViewer', () => { }); it('renders fallback download diff display', done => { - createComponent( - Object.assign({}, requiredProps, { - diffViewerMode: 'added', - newPath: 'test.abc', - oldPath: 'testold.abc', - }), - ); + createComponent({ + ...requiredProps, + diffViewerMode: 'added', + newPath: 'test.abc', + oldPath: 'testold.abc', + }); setTimeout(() => { expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain( @@ -77,28 +72,26 @@ describe('DiffViewer', () => { }); it('renders renamed component', () => { - createComponent( - Object.assign({}, requiredProps, { - diffMode: 'renamed', - diffViewerMode: 'renamed', - newPath: 'test.abc', - oldPath: 'testold.abc', - }), - ); + createComponent({ + ...requiredProps, + diffMode: 'renamed', + diffViewerMode: 'renamed', + newPath: 'test.abc', + oldPath: 'testold.abc', + }); expect(vm.$el.textContent).toContain('File moved'); }); it('renders mode changed component', () => { - createComponent( - Object.assign({}, requiredProps, { - diffMode: 'mode_changed', - newPath: 'test.abc', - oldPath: 'testold.abc', - aMode: '123', - bMode: '321', - }), - ); + createComponent({ + ...requiredProps, + diffMode: 'mode_changed', + newPath: 'test.abc', + oldPath: 'testold.abc', + aMode: '123', + bMode: '321', + }); expect(vm.$el.textContent).toContain('File mode changed from 123 to 321'); }); diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js index 288eb40cc76..a87998aa72f 100644 --- a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js @@ -25,12 +25,7 @@ describe('toolbar', () => { describe('user cannot attach file', () => { beforeEach(() => { - vm = mountComponent( - Toolbar, - Object.assign({}, props, { - canAttachFile: false, - }), - ); + vm = mountComponent(Toolbar, { ...props, canAttachFile: false }); }); it('should not render uploading-container', () => { diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js index c062ee13231..3e044f47a3f 100644 --- a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js +++ b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js @@ -5,18 +5,15 @@ import stackedProgressBarComponent from '~/vue_shared/components/stacked_progres const createComponent = config => { const Component = Vue.extend(stackedProgressBarComponent); - const defaultConfig = Object.assign( - {}, - { - successLabel: 'Synced', - failureLabel: 'Failed', - neutralLabel: 'Out of sync', - successCount: 25, - failureCount: 10, - totalCount: 5000, - }, - config, - ); + const defaultConfig = { + successLabel: 'Synced', + failureLabel: 'Failed', + neutralLabel: 'Out of sync', + successCount: 25, + failureCount: 10, + totalCount: 5000, + ...config, + }; return mountComponent(Component, defaultConfig); }; diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index d206d31eb96..c98fde543a1 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -45,22 +45,36 @@ describe Gitlab::ProjectSearchResults do expect(results.formatted_count(scope)).to eq(expected) end end + + context 'blobs' do + it "limits the search to #{described_class::COUNT_LIMIT} items" do + expect(results).to receive(:blobs).with(limit: described_class::COUNT_LIMIT).and_call_original + expect(results.formatted_count('blobs')).to eq('0') + end + end + + context 'wiki_blobs' do + it "limits the search to #{described_class::COUNT_LIMIT} items" do + expect(results).to receive(:wiki_blobs).with(limit: described_class::COUNT_LIMIT).and_call_original + expect(results.formatted_count('wiki_blobs')).to eq('0') + end + end end - shared_examples 'general blob search' do |entity_type, blob_kind| + shared_examples 'general blob search' do |entity_type, blob_type| let(:query) { 'files' } subject(:results) { described_class.new(user, project, query).objects(blob_type) } context "when #{entity_type} is disabled" do let(:project) { disabled_project } - it "hides #{blob_kind} from members" do + it "hides #{blob_type} from members" do project.add_reporter(user) is_expected.to be_empty end - it "hides #{blob_kind} from non-members" do + it "hides #{blob_type} from non-members" do is_expected.to be_empty end end @@ -68,13 +82,13 @@ describe Gitlab::ProjectSearchResults do context "when #{entity_type} is internal" do let(:project) { private_project } - it "finds #{blob_kind} for members" do + it "finds #{blob_type} for members" do project.add_reporter(user) is_expected.not_to be_empty end - it "hides #{blob_kind} from non-members" do + it "hides #{blob_type} from non-members" do is_expected.to be_empty end end @@ -96,7 +110,7 @@ describe Gitlab::ProjectSearchResults do end end - shared_examples 'blob search repository ref' do |entity_type| + shared_examples 'blob search repository ref' do |entity_type, blob_type| let(:query) { 'files' } let(:file_finder) { double } let(:project_branch) { 'project_branch' } @@ -139,9 +153,41 @@ describe Gitlab::ProjectSearchResults do end end + shared_examples 'blob search pagination' do |blob_type| + let(:per_page) { 20 } + let(:count_limit) { described_class::COUNT_LIMIT } + let(:file_finder) { instance_double('Gitlab::FileFinder') } + let(:results) { described_class.new(user, project, query) } + let(:repository_ref) { 'master' } + + before do + allow(file_finder).to receive(:find).and_return([]) + expect(Gitlab::FileFinder).to receive(:new).with(project, repository_ref).and_return(file_finder) + end + + it 'limits search results based on the first page' do + expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit) + results.objects(blob_type, page: 1, per_page: per_page) + end + + it 'limits search results based on the second page' do + expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + per_page) + results.objects(blob_type, page: 2, per_page: per_page) + end + + it 'limits search results based on the third page' do + expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + per_page * 2) + results.objects(blob_type, page: 3, per_page: per_page) + end + + it 'uses the per_page value when passed' do + expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + 10 * 2) + results.objects(blob_type, page: 3, per_page: 10) + end + end + describe 'blob search' do let(:project) { create(:project, :public, :repository) } - let(:blob_type) { 'blobs' } it_behaves_like 'general blob search', 'repository', 'blobs' do let(:disabled_project) { create(:project, :public, :repository, :repository_disabled) } @@ -150,37 +196,11 @@ describe Gitlab::ProjectSearchResults do let(:expected_file_by_content) { 'CHANGELOG' } end - it_behaves_like 'blob search repository ref', 'project' do + it_behaves_like 'blob search repository ref', 'project', 'blobs' do let(:entity) { project } end - context 'pagination' do - let(:per_page) { 20 } - let(:count_limit) { described_class::COUNT_LIMIT } - let(:file_finder) { instance_double('Gitlab::FileFinder') } - let(:results) { described_class.new(user, project, query, per_page: per_page) } - let(:repository_ref) { 'master' } - - before do - allow(file_finder).to receive(:find).and_return([]) - expect(Gitlab::FileFinder).to receive(:new).with(project, repository_ref).and_return(file_finder) - end - - it 'limits search results based on the first page' do - expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit) - results.objects(blob_type, 1) - end - - it 'limits search results based on the second page' do - expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + per_page) - results.objects(blob_type, 2) - end - - it 'limits search results based on the third page' do - expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + per_page * 2) - results.objects(blob_type, 3) - end - end + it_behaves_like 'blob search pagination', 'blobs' end describe 'wiki search' do @@ -192,7 +212,7 @@ describe Gitlab::ProjectSearchResults do wiki.create_page('CHANGELOG', 'Files example') end - it_behaves_like 'general blob search', 'wiki', 'wiki blobs' do + it_behaves_like 'general blob search', 'wiki', 'wiki_blobs' do let(:blob_type) { 'wiki_blobs' } let(:disabled_project) { create(:project, :public, :wiki_repo, :wiki_disabled) } let(:private_project) { create(:project, :public, :wiki_repo, :wiki_private) } @@ -200,10 +220,11 @@ describe Gitlab::ProjectSearchResults do let(:expected_file_by_content) { 'CHANGELOG.md' } end - it_behaves_like 'blob search repository ref', 'wiki' do - let(:blob_type) { 'wiki_blobs' } + it_behaves_like 'blob search repository ref', 'wiki', 'wiki_blobs' do let(:entity) { project.wiki } end + + it_behaves_like 'blob search pagination', 'wiki_blobs' end it 'does not list issues on private projects' do diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 86dde15cc8a..ab14602a468 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -28,7 +28,15 @@ describe Gitlab::SearchResults do end it 'returns with counts collection when requested' do - expect(results.objects('projects', 1, false)).not_to be_kind_of(Kaminari::PaginatableWithoutCount) + expect(results.objects('projects', page: 1, per_page: 1, without_count: false)).not_to be_kind_of(Kaminari::PaginatableWithoutCount) + end + + it 'uses page and per_page to paginate results' do + project2 = create(:project, name: 'foo') + + expect(results.objects('projects', page: 1, per_page: 1).to_a).to eq([project]) + expect(results.objects('projects', page: 2, per_page: 1).to_a).to eq([project2]) + expect(results.objects('projects', page: 1, per_page: 2).count).to eq(2) end end diff --git a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb index 8c55cc21f2c..283140d7fdf 100644 --- a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb @@ -34,7 +34,8 @@ describe Gitlab::SidekiqLogging::JSONFormatter do 'started_at' => timestamp_iso8601, 'retried_at' => timestamp_iso8601, 'failed_at' => timestamp_iso8601, - 'completed_at' => timestamp_iso8601 + 'completed_at' => timestamp_iso8601, + 'retry' => 0 } ) @@ -57,6 +58,26 @@ describe Gitlab::SidekiqLogging::JSONFormatter do expect(subject['args']).to eq(["1", "test", "2", %({"test"=>1})]) end + + context 'when the job has a non-integer value for retry' do + using RSpec::Parameterized::TableSyntax + + where(:retry_in_job, :retry_in_logs) do + 3 | 3 + true | 25 + false | 0 + nil | 0 + 'string' | -1 + end + + with_them do + it 'logs as the correct integer' do + hash_input['retry'] = retry_in_job + + expect(subject['retry']).to eq(retry_in_logs) + end + end + end end describe 'with a String' do diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb index 483283fd093..a41be0eaa95 100644 --- a/spec/lib/gitlab/snippet_search_results_spec.rb +++ b/spec/lib/gitlab/snippet_search_results_spec.rb @@ -20,4 +20,14 @@ describe Gitlab::SnippetSearchResults do expect(results.formatted_count('snippet_titles')).to eq(max_limited_count) end end + + describe '#objects' do + it 'uses page and per_page to paginate results' do + snippet2 = create(:snippet, :public, content: 'foo', file_name: 'foo') + + expect(results.objects('snippet_titles', page: 1, per_page: 1).to_a).to eq([snippet2]) + expect(results.objects('snippet_titles', page: 2, per_page: 1).to_a).to eq([snippet]) + expect(results.objects('snippet_titles', page: 1, per_page: 2).count).to eq(2) + end + end end diff --git a/spec/policies/alert_management/alert_policy_spec.rb b/spec/policies/alert_management/alert_policy_spec.rb index 523464d8ff1..0d7624a0142 100644 --- a/spec/policies/alert_management/alert_policy_spec.rb +++ b/spec/policies/alert_management/alert_policy_spec.rb @@ -10,16 +10,16 @@ describe AlertManagement::AlertPolicy, :models do subject(:policy) { described_class.new(user, alert) } describe 'rules' do - it { is_expected.to be_disallowed :read_alert_management_alerts } - it { is_expected.to be_disallowed :update_alert_management_alerts } + it { is_expected.to be_disallowed :read_alert_management_alert } + it { is_expected.to be_disallowed :update_alert_management_alert } context 'when developer' do before do project.add_developer(user) end - it { is_expected.to be_allowed :read_alert_management_alerts } - it { is_expected.to be_allowed :update_alert_management_alerts } + it { is_expected.to be_allowed :read_alert_management_alert } + it { is_expected.to be_allowed :update_alert_management_alert } end end end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 0bdb9ea6bf9..90060c3f55a 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -15,10 +15,36 @@ describe API::Search do it { expect(json_response.size).to eq(size) } end - describe 'GET /search' do + shared_examples 'pagination' do |scope:, search: ''| + it 'returns a different result for each page' do + get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 } + first = json_response.first + + get api(endpoint, user), params: { scope: scope, search: search, page: 2, per_page: 1 } + second = Gitlab::Json.parse(response.body).first + + expect(first).not_to eq(second) + end + + it 'returns 1 result when per_page is 1' do + get api(endpoint, user), params: { scope: scope, search: search, per_page: 1 } + + expect(json_response.count).to eq(1) + end + + it 'returns 2 results when per_page is 2' do + get api(endpoint, user), params: { scope: scope, search: search, per_page: 2 } + + expect(Gitlab::Json.parse(response.body).count).to eq(2) + end + end + + describe 'GET /search' do + let(:endpoint) { '/search' } + context 'when user is not authenticated' do it 'returns 401 error' do - get api('/search'), params: { scope: 'projects', search: 'awesome' } + get api(endpoint), params: { scope: 'projects', search: 'awesome' } expect(response).to have_gitlab_http_status(:unauthorized) end @@ -26,7 +52,7 @@ describe API::Search do context 'when scope is not supported' do it 'returns 400 error' do - get api('/search', user), params: { scope: 'unsupported', search: 'awesome' } + get api(endpoint, user), params: { scope: 'unsupported', search: 'awesome' } expect(response).to have_gitlab_http_status(:bad_request) end @@ -34,7 +60,7 @@ describe API::Search do context 'when scope is missing' do it 'returns 400 error' do - get api('/search', user), params: { search: 'awesome' } + get api(endpoint, user), params: { search: 'awesome' } expect(response).to have_gitlab_http_status(:bad_request) end @@ -43,30 +69,48 @@ describe API::Search do context 'with correct params' do context 'for projects scope' do before do - get api('/search', user), params: { scope: 'projects', search: 'awesome' } + get api(endpoint, user), params: { scope: 'projects', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/projects' + + it_behaves_like 'pagination', scope: :projects end context 'for issues scope' do before do create(:issue, project: project, title: 'awesome issue') - get api('/search', user), params: { scope: 'issues', search: 'awesome' } + get api(endpoint, user), params: { scope: 'issues', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/issues' + + describe 'pagination' do + before do + create(:issue, project: project, title: 'another issue') + end + + include_examples 'pagination', scope: :issues + end end context 'for merge_requests scope' do before do create(:merge_request, source_project: repo_project, title: 'awesome mr') - get api('/search', user), params: { scope: 'merge_requests', search: 'awesome' } + get api(endpoint, user), params: { scope: 'merge_requests', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests' + + describe 'pagination' do + before do + create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') + end + + include_examples 'pagination', scope: :merge_requests + end end context 'for milestones scope' do @@ -76,10 +120,18 @@ describe API::Search do context 'when user can read project milestones' do before do - get api('/search', user), params: { scope: 'milestones', search: 'awesome' } + get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' + + describe 'pagination' do + before do + create(:milestone, project: project, title: 'another milestone') + end + + include_examples 'pagination', scope: :milestones + end end context 'when user cannot read project milestones' do @@ -89,7 +141,7 @@ describe API::Search do end it 'returns empty array' do - get api('/search', user), params: { scope: 'milestones', search: 'awesome' } + get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' } milestones = json_response @@ -102,16 +154,18 @@ describe API::Search do before do create(:user, name: 'billy') - get api('/search', user), params: { scope: 'users', search: 'billy' } + get api(endpoint, user), params: { scope: 'users', search: 'billy' } end it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + it_behaves_like 'pagination', scope: :users + context 'when users search feature is disabled' do before do allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) - get api('/search', user), params: { scope: 'users', search: 'billy' } + get api(endpoint, user), params: { scope: 'users', search: 'billy' } end it 'returns 400 error' do @@ -124,18 +178,28 @@ describe API::Search do before do create(:snippet, :public, title: 'awesome snippet', content: 'snippet content') - get api('/search', user), params: { scope: 'snippet_titles', search: 'awesome' } + get api(endpoint, user), params: { scope: 'snippet_titles', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/snippets' + + describe 'pagination' do + before do + create(:snippet, :public, title: 'another snippet', content: 'snippet content') + end + + include_examples 'pagination', scope: :snippet_titles + end end end end describe "GET /groups/:id/search" do + let(:endpoint) { "/groups/#{group.id}/-/search" } + context 'when user is not authenticated' do it 'returns 401 error' do - get api("/groups/#{group.id}/search"), params: { scope: 'projects', search: 'awesome' } + get api(endpoint), params: { scope: 'projects', search: 'awesome' } expect(response).to have_gitlab_http_status(:unauthorized) end @@ -143,7 +207,7 @@ describe API::Search do context 'when scope is not supported' do it 'returns 400 error' do - get api("/groups/#{group.id}/search", user), params: { scope: 'unsupported', search: 'awesome' } + get api(endpoint, user), params: { scope: 'unsupported', search: 'awesome' } expect(response).to have_gitlab_http_status(:bad_request) end @@ -151,7 +215,7 @@ describe API::Search do context 'when scope is missing' do it 'returns 400 error' do - get api("/groups/#{group.id}/search", user), params: { search: 'awesome' } + get api(endpoint, user), params: { search: 'awesome' } expect(response).to have_gitlab_http_status(:bad_request) end @@ -178,40 +242,66 @@ describe API::Search do context 'with correct params' do context 'for projects scope' do before do - get api("/groups/#{group.id}/search", user), params: { scope: 'projects', search: 'awesome' } + get api(endpoint, user), params: { scope: 'projects', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/projects' + + it_behaves_like 'pagination', scope: :projects end context 'for issues scope' do before do create(:issue, project: project, title: 'awesome issue') - get api("/groups/#{group.id}/search", user), params: { scope: 'issues', search: 'awesome' } + get api(endpoint, user), params: { scope: 'issues', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/issues' + + describe 'pagination' do + before do + create(:issue, project: project, title: 'another issue') + end + + include_examples 'pagination', scope: :issues + end end context 'for merge_requests scope' do before do create(:merge_request, source_project: repo_project, title: 'awesome mr') - get api("/groups/#{group.id}/search", user), params: { scope: 'merge_requests', search: 'awesome' } + get api(endpoint, user), params: { scope: 'merge_requests', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests' + + describe 'pagination' do + before do + create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') + end + + include_examples 'pagination', scope: :merge_requests + end end context 'for milestones scope' do before do create(:milestone, project: project, title: 'awesome milestone') - get api("/groups/#{group.id}/search", user), params: { scope: 'milestones', search: 'awesome' } + get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' + + describe 'pagination' do + before do + create(:milestone, project: project, title: 'another milestone') + end + + include_examples 'pagination', scope: :milestones + end end context 'for milestones scope with group path as id' do @@ -231,16 +321,24 @@ describe API::Search do user = create(:user, name: 'billy') create(:group_member, :developer, user: user, group: group) - get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' } + get api(endpoint, user), params: { scope: 'users', search: 'billy' } end it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + describe 'pagination' do + before do + create(:group_member, :developer, group: group) + end + + include_examples 'pagination', scope: :users + end + context 'when users search feature is disabled' do before do allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) - get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' } + get api(endpoint, user), params: { scope: 'users', search: 'billy' } end it 'returns 400 error' do @@ -263,9 +361,11 @@ describe API::Search do end describe "GET /projects/:id/search" do + let(:endpoint) { "/projects/#{project.id}/search" } + context 'when user is not authenticated' do it 'returns 401 error' do - get api("/projects/#{project.id}/search"), params: { scope: 'issues', search: 'awesome' } + get api(endpoint), params: { scope: 'issues', search: 'awesome' } expect(response).to have_gitlab_http_status(:unauthorized) end @@ -273,7 +373,7 @@ describe API::Search do context 'when scope is not supported' do it 'returns 400 error' do - get api("/projects/#{project.id}/search", user), params: { scope: 'unsupported', search: 'awesome' } + get api(endpoint, user), params: { scope: 'unsupported', search: 'awesome' } expect(response).to have_gitlab_http_status(:bad_request) end @@ -281,7 +381,7 @@ describe API::Search do context 'when scope is missing' do it 'returns 400 error' do - get api("/projects/#{project.id}/search", user), params: { search: 'awesome' } + get api(endpoint, user), params: { search: 'awesome' } expect(response).to have_gitlab_http_status(:bad_request) end @@ -299,7 +399,7 @@ describe API::Search do it 'returns 404 error' do project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - get api("/projects/#{project.id}/search", user), params: { scope: 'issues', search: 'awesome' } + get api(endpoint, user), params: { scope: 'issues', search: 'awesome' } expect(response).to have_gitlab_http_status(:not_found) end @@ -310,20 +410,38 @@ describe API::Search do before do create(:issue, project: project, title: 'awesome issue') - get api("/projects/#{project.id}/search", user), params: { scope: 'issues', search: 'awesome' } + get api(endpoint, user), params: { scope: 'issues', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/issues' + + describe 'pagination' do + before do + create(:issue, project: project, title: 'another issue') + end + + include_examples 'pagination', scope: :issues + end end context 'for merge_requests scope' do + let(:endpoint) { "/projects/#{repo_project.id}/search" } + before do create(:merge_request, source_project: repo_project, title: 'awesome mr') - get api("/projects/#{repo_project.id}/search", user), params: { scope: 'merge_requests', search: 'awesome' } + get api(endpoint, user), params: { scope: 'merge_requests', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests' + + describe 'pagination' do + before do + create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') + end + + include_examples 'pagination', scope: :merge_requests + end end context 'for milestones scope' do @@ -333,10 +451,18 @@ describe API::Search do context 'when user can read milestones' do before do - get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' } + get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' + + describe 'pagination' do + before do + create(:milestone, project: project, title: 'another milestone') + end + + include_examples 'pagination', scope: :milestones + end end context 'when user cannot read project milestones' do @@ -346,7 +472,7 @@ describe API::Search do end it 'returns empty array' do - get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' } + get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' } milestones = json_response @@ -360,16 +486,24 @@ describe API::Search do user1 = create(:user, name: 'billy') create(:project_member, :developer, user: user1, project: project) - get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' } + get api(endpoint, user), params: { scope: 'users', search: 'billy' } end it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + describe 'pagination' do + before do + create(:project_member, :developer, project: project) + end + + include_examples 'pagination', scope: :users + end + context 'when users search feature is disabled' do before do allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) - get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' } + get api(endpoint, user), params: { scope: 'users', search: 'billy' } end it 'returns 400 error' do @@ -382,29 +516,51 @@ describe API::Search do before do create(:note_on_merge_request, project: project, note: 'awesome note') - get api("/projects/#{project.id}/search", user), params: { scope: 'notes', search: 'awesome' } + get api(endpoint, user), params: { scope: 'notes', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/notes' + + describe 'pagination' do + before do + mr = create(:merge_request, source_project: project, target_branch: 'another_branch') + create(:note, project: project, noteable: mr, note: 'another note') + end + + include_examples 'pagination', scope: :notes + end end context 'for wiki_blobs scope' do + let(:wiki) { create(:project_wiki, project: project) } + before do - wiki = create(:project_wiki, project: project) create(:wiki_page, wiki: wiki, title: 'home', content: "Awesome page") - get api("/projects/#{project.id}/search", user), params: { scope: 'wiki_blobs', search: 'awesome' } + get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' } end it_behaves_like 'response is correct', schema: 'public_api/v4/blobs' + + describe 'pagination' do + before do + create(:wiki_page, wiki: wiki, title: 'home 2', content: 'Another page') + end + + include_examples 'pagination', scope: :wiki_blobs, search: 'page' + end end context 'for commits scope' do + let(:endpoint) { "/projects/#{repo_project.id}/search" } + before do - get api("/projects/#{repo_project.id}/search", user), params: { scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6' } + get api(endpoint, user), params: { scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6' } end it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details' + + it_behaves_like 'pagination', scope: :commits, search: 'merge' end context 'for commits scope with project path as id' do @@ -416,15 +572,19 @@ describe API::Search do end context 'for blobs scope' do + let(:endpoint) { "/projects/#{repo_project.id}/search" } + before do - get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'monitors' } + get api(endpoint, user), params: { scope: 'blobs', search: 'monitors' } end it_behaves_like 'response is correct', schema: 'public_api/v4/blobs', size: 2 + it_behaves_like 'pagination', scope: :blobs, search: 'monitors' + context 'filters' do it 'by filename' do - get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'mon filename:PROCESS.md' } + get api(endpoint, user), params: { scope: 'blobs', search: 'mon filename:PROCESS.md' } expect(response).to have_gitlab_http_status(:ok) expect(json_response.size).to eq(2) @@ -433,21 +593,21 @@ describe API::Search do end it 'by path' do - get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'mon path:markdown' } + get api(endpoint, user), params: { scope: 'blobs', search: 'mon path:markdown' } expect(response).to have_gitlab_http_status(:ok) expect(json_response.size).to eq(8) end it 'by extension' do - get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'mon extension:md' } + get api(endpoint, user), params: { scope: 'blobs', search: 'mon extension:md' } expect(response).to have_gitlab_http_status(:ok) expect(json_response.size).to eq(11) end it 'by ref' do - get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'This file is used in tests for ci_environments_status', ref: 'pages-deploy' } + get api(endpoint, user), params: { scope: 'blobs', search: 'This file is used in tests for ci_environments_status', ref: 'pages-deploy' } expect(response).to have_gitlab_http_status(:ok) expect(json_response.size).to eq(1) diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 77c160ccce2..0333eb85fb6 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -18,7 +18,9 @@ describe SearchService do let(:group_project) { create(:project, group: accessible_group, name: 'group_project') } let(:public_project) { create(:project, :public, name: 'public_project') } - subject(:search_service) { described_class.new(user, search: search, scope: scope, page: 1) } + let(:per_page) { described_class::DEFAULT_PER_PAGE } + + subject(:search_service) { described_class.new(user, search: search, scope: scope, page: 1, per_page: per_page) } before do accessible_project.add_maintainer(user) @@ -240,6 +242,76 @@ describe SearchService do end describe '#search_objects' do + context 'handling per_page param' do + let(:search) { '' } + let(:scope) { nil } + + context 'when nil' do + let(:per_page) { nil } + + it "defaults to #{described_class::DEFAULT_PER_PAGE}" do + expect_any_instance_of(Gitlab::SearchResults) + .to receive(:objects) + .with(anything, hash_including(per_page: described_class::DEFAULT_PER_PAGE)) + .and_call_original + + subject.search_objects + end + end + + context 'when empty string' do + let(:per_page) { '' } + + it "defaults to #{described_class::DEFAULT_PER_PAGE}" do + expect_any_instance_of(Gitlab::SearchResults) + .to receive(:objects) + .with(anything, hash_including(per_page: described_class::DEFAULT_PER_PAGE)) + .and_call_original + + subject.search_objects + end + end + + context 'when negative' do + let(:per_page) { '-1' } + + it "defaults to #{described_class::DEFAULT_PER_PAGE}" do + expect_any_instance_of(Gitlab::SearchResults) + .to receive(:objects) + .with(anything, hash_including(per_page: described_class::DEFAULT_PER_PAGE)) + .and_call_original + + subject.search_objects + end + end + + context 'when present' do + let(:per_page) { '50' } + + it "converts to integer and passes to search results" do + expect_any_instance_of(Gitlab::SearchResults) + .to receive(:objects) + .with(anything, hash_including(per_page: 50)) + .and_call_original + + subject.search_objects + end + end + + context "when greater than #{described_class::MAX_PER_PAGE}" do + let(:per_page) { described_class::MAX_PER_PAGE + 1 } + + it "passes #{described_class::MAX_PER_PAGE}" do + expect_any_instance_of(Gitlab::SearchResults) + .to receive(:objects) + .with(anything, hash_including(per_page: described_class::MAX_PER_PAGE)) + .and_call_original + + subject.search_objects + end + end + end + context 'with accessible project_id' do it 'returns objects in the project' do search_objects = described_class.new( diff --git a/yarn.lock b/yarn.lock index 859fc145342..8d39687eab5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -782,15 +782,15 @@ eslint-plugin-vue "^6.2.1" vue-eslint-parser "^7.0.0" -"@gitlab/svgs@1.125.0": - version "1.125.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.125.0.tgz#59c667dae8f7e4c80b482f5f6cc35367c016387b" - integrity sha512-MKfFYa8f+9P2tJ/JN/E9oDBSSo/gRz2zuGui4XHQPoaw/DkIMn7EyAzeSpRgbgs1LgMcEqqKsIEx+spCga3jsQ== - -"@gitlab/ui@14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.0.0.tgz#d478b1454659c0f54b72cdecce1c2014fc5f8564" - integrity sha512-R+unP0mOBYQ+uRJLm/tI+2znsbsHY2rumSYtMqM3vGCXasteySQIMZ8huWGa5Cf4ZUdy1lNa0J/zxKj6TLdjCQ== +"@gitlab/svgs@1.127.0": + version "1.127.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.127.0.tgz#1f7ffdffe44d6a82b372535f93d78f3a895d1960" + integrity sha512-Uv52DqkG2KwCB0VRlXUEHFZxJ/7Ql0t1YTdzICpXmDjltuUBrysFcdmWPVO6PgXQxk2ahryNsUjSOziMYTeSiw== + +"@gitlab/ui@14.2.1": + version "14.2.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.2.1.tgz#7972ce8e258357a41a354bda9213b144593ed912" + integrity sha512-jdwhtnjVW38wfeqibIgIqDqeYbKwq6kWfs0LRHhpoh+A+zgjgzUxjSkcz9Wg+Yipy08PmY/NSKEkuK33/fIqQA== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |