diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-03 09:08:53 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-03 09:08:53 +0000 |
commit | 1c75400c24137f603678d0ee3d497b0c9280e7f7 (patch) | |
tree | ccf2e8584d8b7efd3c648a276ebe5b456639da3b | |
parent | 1f23012963babbcc586e7025cc28e62385813fb6 (diff) | |
download | gitlab-ce-1c75400c24137f603678d0ee3d497b0c9280e7f7.tar.gz |
Add latest changes from gitlab-org/gitlab@master
126 files changed, 1736 insertions, 1118 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue index 59c74d8cd27..44602882be2 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_list.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue @@ -85,6 +85,13 @@ export default { sortable: true, }, { + key: 'issue', + label: s__('AlertManagement|Issue'), + thClass: 'gl-w-12 gl-pointer-events-none', + tdClass, + sortable: false, + }, + { key: 'assignees', label: s__('AlertManagement|Assignees'), thClass: 'gl-w-eighth gl-pointer-events-none', @@ -278,6 +285,9 @@ export default { ? assignees.nodes[0]?.username : s__('AlertManagement|Unassigned'); }, + getIssueLink(item) { + return joinPaths('/', this.projectPath, '-', 'issues', item.issueIid); + }, handlePageChange(page) { const { startCursor, endCursor } = this.alerts.pageInfo; @@ -402,6 +412,13 @@ export default { <div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div> </template> + <template #cell(issue)="{ item }"> + <gl-link v-if="item.issueIid" data-testid="issueField" :href="getIssueLink(item)"> + #{{ item.issueIid }} + </gl-link> + <div v-else data-testid="issueField">{{ s__('AlertManagement|None') }}</div> + </template> + <template #cell(assignees)="{ item }"> <div class="gl-max-w-full text-truncate" data-testid="assigneesField"> {{ getAssignees(item.assignees) }} diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index eb12617a66e..e9d66954622 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -5,10 +5,11 @@ import { GlLabel, GlTooltip, GlIcon, + GlSprintf, GlTooltipDirective, } from '@gitlab/ui'; import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits'; -import { s__, __, sprintf } from '~/locale'; +import { n__, s__ } from '~/locale'; import AccessorUtilities from '../../lib/utils/accessor'; import BoardDelete from './board_delete'; import IssueCount from './issue_count.vue'; @@ -25,6 +26,7 @@ export default { GlLabel, GlTooltip, GlIcon, + GlSprintf, IssueCount, }, directives: { @@ -82,10 +84,20 @@ export default { this.listType !== ListType.promotion ); }, - issuesTooltip() { + showMilestoneListDetails() { + return ( + this.list.type === 'milestone' && + this.list.milestone && + (this.list.isExpanded || !this.isSwimlanesHeader) + ); + }, + showAssigneeListDetails() { + return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader); + }, + issuesTooltipLabel() { const { issuesSize } = this.list; - return sprintf(__('%{issuesSize} issues'), { issuesSize }); + return n__(`%d issue`, `%d issues`, issuesSize); }, chevronTooltip() { return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand'); @@ -111,6 +123,9 @@ export default { // eslint-disable-next-line @gitlab/require-i18n-strings return `boards.${this.boardId}.${this.listType}.${this.list.id}`; }, + collapsedTooltipTitle() { + return this.listTitle || this.listAssignee; + }, }, methods: { showScopedLabels(label) { @@ -147,7 +162,7 @@ export default { 'has-border': list.label && list.label.color, 'gl-relative': list.isExpanded, 'gl-h-full': !list.isExpanded, - 'board-inner gl-rounded-base gl-border-b-0': isSwimlanesHeader, + 'board-inner gl-rounded-base': isSwimlanesHeader, }" :style="{ borderTopColor: list.label && list.label.color ? list.label.color : null }" class="board-header gl-relative" @@ -157,7 +172,9 @@ export default { <h3 :class="{ 'user-can-drag': !disabled && !list.preset, - 'gl-border-b-0': !list.isExpanded, + 'gl-py-3': !list.isExpanded && !isSwimlanesHeader, + 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader, + 'gl-py-2': !list.isExpanded && isSwimlanesHeader, }" class="board-title gl-m-0 gl-display-flex js-board-handle" > @@ -167,21 +184,17 @@ export default { :aria-label="chevronTooltip" :title="chevronTooltip" :icon="chevronIcon" - class="board-title-caret no-drag" + class="board-title-caret no-drag gl-cursor-pointer " variant="link" @click="toggleExpanded" /> <!-- The following is only true in EE and if it is a milestone --> - <span - v-if="list.type === 'milestone' && list.milestone" - aria-hidden="true" - class="gl-mr-2 milestone-icon" - > + <span v-if="showMilestoneListDetails" aria-hidden="true" class="gl-mr-2 milestone-icon"> <gl-icon name="timer" /> </span> <a - v-if="list.type === 'assignee'" + v-if="showAssigneeListDetails" :href="list.assignee.path" class="user-avatar-link js-no-trigger" > @@ -195,7 +208,10 @@ export default { width="20" /> </a> - <div class="board-title-text"> + <div + class="board-title-text" + :class="{ 'gl-display-none': !list.isExpanded && isSwimlanesHeader }" + > <span v-if="list.type !== 'label'" v-gl-tooltip.hover @@ -208,7 +224,7 @@ export default { {{ list.title }} </span> <span v-if="list.type === 'assignee'" class="board-title-sub-text gl-ml-2"> - @{{ list.assignee.username }} + @{{ listAssignee }} </span> <gl-label v-if="list.type === 'label'" @@ -220,6 +236,33 @@ export default { :title="list.label.title" /> </div> + + <span + v-if="isSwimlanesHeader && !list.isExpanded" + ref="collapsedInfo" + aria-hidden="true" + class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-700" + > + <gl-icon name="information" /> + </span> + <gl-tooltip v-if="isSwimlanesHeader && !list.isExpanded" :target="() => $refs.collapsedInfo"> + <div class="gl-font-weight-bold gl-pb-2">{{ collapsedTooltipTitle }}</div> + <div v-if="list.maxIssueCount !== 0"> + • + <gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')"> + <template #issuesSize>{{ issuesTooltipLabel }}</template> + <template #maxIssueCount>{{ list.maxIssueCount }}</template> + </gl-sprintf> + </div> + <div v-else>• {{ issuesTooltipLabel }}</div> + <div v-if="weightFeatureAvailable"> + • + <gl-sprintf :message="__('%{totalWeight} total weight')"> + <template #totalWeight>{{ list.totalWeight }}</template> + </gl-sprintf> + </div> + </gl-tooltip> + <board-delete v-if="canAdminList && !list.preset && list.id" :list="list" @@ -229,7 +272,7 @@ export default { v-gl-tooltip.hover.bottom :class="{ 'gl-display-none': !list.isExpanded }" :aria-label="__('Delete list')" - class="board-delete no-drag gl-pr-0 gl-shadow-none gl-mr-3" + class="board-delete no-drag gl-pr-0 gl-shadow-none! gl-mr-3" :title="__('Delete list')" icon="remove" size="small" @@ -239,9 +282,10 @@ export default { <div v-if="showBoardListAndBoardInfo" class="issue-count-badge gl-pr-0 no-drag text-secondary" + :class="{ 'gl-display-none': !list.isExpanded && isSwimlanesHeader }" > <span class="gl-display-inline-flex"> - <gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltip" /> + <gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" /> <span ref="issueCount" class="issue-count-badge-count"> <gl-icon class="gl-mr-2" name="issues" /> <issue-count :issues-size="list.issuesSize" :max-issue-count="list.maxIssueCount" /> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 3c52368acdd..a27c6aa0d1b 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -164,7 +164,7 @@ export default { ]), ...mapGetters('monitoringDashboard', ['selectedDashboard', 'getMetricStates']), shouldShowVariablesSection() { - return Object.keys(this.variables).length > 0; + return Boolean(this.variables.length); }, shouldShowLinksSection() { return Object.keys(this.links).length > 0; diff --git a/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue index d79b8284a65..4e48292c48d 100644 --- a/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue +++ b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue @@ -34,7 +34,7 @@ export default { }, methods: { onUpdate(value) { - this.$emit('onUpdate', this.name, value); + this.$emit('input', value); }, }, }; diff --git a/app/assets/javascripts/monitoring/components/variables/text_field.vue b/app/assets/javascripts/monitoring/components/variables/text_field.vue index ce0d19760e2..a0418806e5f 100644 --- a/app/assets/javascripts/monitoring/components/variables/text_field.vue +++ b/app/assets/javascripts/monitoring/components/variables/text_field.vue @@ -22,7 +22,7 @@ export default { }, methods: { onUpdate(event) { - this.$emit('onUpdate', this.name, event.target.value); + this.$emit('input', event.target.value); }, }, }; diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue index 9d3159dfb6e..25d900b07ad 100644 --- a/app/assets/javascripts/monitoring/components/variables_section.vue +++ b/app/assets/javascripts/monitoring/components/variables_section.vue @@ -16,10 +16,9 @@ export default { methods: { ...mapActions('monitoringDashboard', ['updateVariablesAndFetchData']), refreshDashboard(variable, value) { - if (this.variables[variable].value !== value) { - const changedVariable = { key: variable, value }; + if (variable.value !== value) { + this.updateVariablesAndFetchData({ name: variable.name, value }); // update the Vuex store - this.updateVariablesAndFetchData(changedVariable); // the below calls can ideally be moved out of the // component and into the actions and let the // mutation respond directly. @@ -39,15 +38,15 @@ export default { </script> <template> <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section"> - <div v-for="(variable, key) in variables" :key="key" class="mb-1 pr-2 d-flex d-sm-block"> + <div v-for="variable in variables" :key="variable.name" class="mb-1 pr-2 d-flex d-sm-block"> <component :is="variableField(variable.type)" class="mb-0 flex-grow-1" :label="variable.label" :value="variable.value" - :name="key" + :name="variable.name" :options="variable.options" - @onUpdate="refreshDashboard" + @input="refreshDashboard(variable, $event)" /> </div> </div> diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index aaa7865f30a..2ed114f7868 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -77,10 +77,6 @@ export const setTimeRange = ({ commit }, timeRange) => { commit(types.SET_TIME_RANGE, timeRange); }; -export const setVariables = ({ commit }, variables) => { - commit(types.SET_VARIABLES, variables); -}; - export const filterEnvironments = ({ commit, dispatch }, searchTerm) => { commit(types.SET_ENVIRONMENTS_FILTER, searchTerm); dispatch('fetchEnvironmentsData'); @@ -235,7 +231,7 @@ export const fetchPrometheusMetric = ( queryParams.step = metric.step; } - if (Object.keys(state.variables).length > 0) { + if (state.variables.length > 0) { queryParams = { ...queryParams, ...getters.getCustomVariablesParams, @@ -480,7 +476,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery const { start_time, end_time } = defaultQueryParams; const optionsRequests = []; - Object.entries(state.variables).forEach(([key, variable]) => { + state.variables.forEach(variable => { if (variable.type === VARIABLE_TYPES.metric_label_values) { const { prometheusEndpointPath, label } = variable.options; @@ -496,7 +492,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery .catch(() => { createFlash( sprintf(s__('Metrics|There was an error getting options for variable "%{name}".'), { - name: key, + name: variable.name, }), ); }); diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js index 3003fda634c..3aa711a0509 100644 --- a/app/assets/javascripts/monitoring/stores/getters.js +++ b/app/assets/javascripts/monitoring/stores/getters.js @@ -133,8 +133,8 @@ export const linksWithMetadata = state => { }; /** - * Maps an variables object to an array along with stripping - * the variable prefix. + * Maps a variables array to an object for replacement in + * prometheus queries. * * This method outputs an object in the below format * @@ -147,14 +147,17 @@ export const linksWithMetadata = state => { * user-defined variables coming through the URL and differentiate * from other variables used for Prometheus API endpoint. * - * @param {Object} variables - Custom variables provided by the user - * @returns {Array} The custom variables array to be send to the API + * @param {Object} state - State containing variables provided by the user + * @returns {Array} The custom variables object to be send to the API * in the format of {variables[key1]=value1, variables[key2]=value2} */ export const getCustomVariablesParams = state => - Object.keys(state.variables).reduce((acc, variable) => { - acc[addPrefixToCustomVariableParams(variable)] = state.variables[variable]?.value; + state.variables.reduce((acc, variable) => { + const { name, value } = variable; + if (value !== null) { + acc[addPrefixToCustomVariableParams(name)] = value; + } return acc; }, {}); diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index d43065749c1..72b48c251ae 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -2,7 +2,6 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD'; export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS'; export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE'; -export const SET_VARIABLES = 'SET_VARIABLES'; export const UPDATE_VARIABLE_VALUE = 'UPDATE_VARIABLE_VALUE'; export const UPDATE_VARIABLE_METRIC_LABEL_VALUES = 'UPDATE_VARIABLE_METRIC_LABEL_VALUES'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index 53c8029e46b..2de1d40e258 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -203,14 +203,13 @@ export default { state.expandedPanel.group = group; state.expandedPanel.panel = panel; }, - [types.SET_VARIABLES](state, variables) { - state.variables = variables; - }, - [types.UPDATE_VARIABLE_VALUE](state, { key, value }) { - Object.assign(state.variables[key], { - ...state.variables[key], - value, - }); + [types.UPDATE_VARIABLE_VALUE](state, { name, value }) { + const variable = state.variables.find(v => v.name === name); + if (variable) { + Object.assign(variable, { + value, + }); + } }, [types.UPDATE_VARIABLE_METRIC_LABEL_VALUES](state, { variable, label, data = [] }) { const values = optionsFromSeriesData({ label, data }); diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index ebe5e24b8db..6568555b9b0 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -47,7 +47,7 @@ export default () => ({ * User-defined custom variables are passed * via the dashboard yml file. */ - variables: {}, + variables: [], /** * User-defined custom links are passed * via the dashboard yml file. diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 59b345d74ff..a09d792c058 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -289,7 +289,7 @@ export const mapToDashboardViewModel = ({ }) => { return { dashboard, - variables: mergeURLVariables(parseTemplatingVariables(templating)), + variables: mergeURLVariables(parseTemplatingVariables(templating.variables)), links: links.map(mapLinksToViewModel), panelGroups: panel_groups.map(mapToPanelGroupViewModel), }; @@ -453,10 +453,10 @@ export const normalizeQueryResponseData = data => { * * This is currently only used by getters/getCustomVariablesParams * - * @param {String} key Variable key that needs to be prefixed + * @param {String} name Variable key that needs to be prefixed * @returns {String} */ -export const addPrefixToCustomVariableParams = key => `variables[${key}]`; +export const addPrefixToCustomVariableParams = name => `variables[${name}]`; /** * Normalize custom dashboard paths. This method helps support diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js index 0b268402992..9245ffdb3b9 100644 --- a/app/assets/javascripts/monitoring/stores/variable_mapping.js +++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js @@ -46,7 +46,7 @@ const textAdvancedVariableParser = advTextVar => ({ * @param {Object} custom variable option * @returns {Object} normalized custom variable options */ -const normalizeVariableValues = ({ default: defaultOpt = false, text, value }) => ({ +const normalizeVariableValues = ({ default: defaultOpt = false, text, value = null }) => ({ default: defaultOpt, text: text || value, value, @@ -68,10 +68,10 @@ const customAdvancedVariableParser = advVariable => { return { type: VARIABLE_TYPES.custom, label: advVariable.label, - value: defaultValue?.value, options: { values, }, + value: defaultValue?.value || null, }; }; @@ -100,27 +100,24 @@ const customSimpleVariableParser = simpleVar => { const values = (simpleVar || []).map(parseSimpleCustomValues); return { type: VARIABLE_TYPES.custom, - value: values[0].value, label: null, + value: values[0].value || null, options: { values: values.map(normalizeVariableValues), }, }; }; -const metricLabelValuesVariableParser = variable => { - const { label, options = {} } = variable; - return { - type: VARIABLE_TYPES.metric_label_values, - value: null, - label, - options: { - prometheusEndpointPath: options.prometheus_endpoint_path || '', - label: options.label || null, - values: [], // values are initially empty - }, - }; -}; +const metricLabelValuesVariableParser = ({ label, options = {} }) => ({ + type: VARIABLE_TYPES.metric_label_values, + label, + value: null, + options: { + prometheusEndpointPath: options.prometheus_endpoint_path || '', + label: options.label || null, + values: [], // values are initially empty + }, +}); /** * Utility method to determine if a custom variable is @@ -161,29 +158,26 @@ const getVariableParser = variable => { * for the user to edit. The values from input elements are relayed to * backend and eventually Prometheus API. * - * This method currently is not used anywhere. Once the issue - * https://gitlab.com/gitlab-org/gitlab/-/issues/214536 is completed, - * this method will have been used by the monitoring dashboard. - * - * @param {Object} templating templating variables from the dashboard yml file - * @returns {Object} a map of processed templating variables + * @param {Object} templating variables from the dashboard yml file + * @returns {array} An array of variables to display as inputs */ -export const parseTemplatingVariables = ({ variables = {} } = {}) => - Object.entries(variables).reduce((acc, [key, variable]) => { +export const parseTemplatingVariables = (ymlVariables = {}) => + Object.entries(ymlVariables).reduce((acc, [name, ymlVariable]) => { // get the parser - const parser = getVariableParser(variable); + const parser = getVariableParser(ymlVariable); // parse the variable - const parsedVar = parser(variable); + const variable = parser(ymlVariable); // for simple custom variable label is null and it should be // replace with key instead - if (parsedVar) { - acc[key] = { - ...parsedVar, - label: parsedVar.label || key, - }; + if (variable) { + acc.push({ + ...variable, + name, + label: variable.label || name, + }); } return acc; - }, {}); + }, []); /** * Custom variables are defined in the dashboard yml file @@ -201,23 +195,18 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) => * This method can be improved further. See the below issue * https://gitlab.com/gitlab-org/gitlab/-/issues/217713 * - * @param {Object} varsFromYML template variables from yml file + * @param {array} parsedYmlVariables - template variables from yml file * @returns {Object} */ -export const mergeURLVariables = (varsFromYML = {}) => { +export const mergeURLVariables = (parsedYmlVariables = []) => { const varsFromURL = templatingVariablesFromUrl(); - const variables = {}; - Object.keys(varsFromYML).forEach(key => { - if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) { - variables[key] = { - ...varsFromYML[key], - value: varsFromURL[key], - }; - } else { - variables[key] = varsFromYML[key]; + parsedYmlVariables.forEach(variable => { + const { name } = variable; + if (Object.prototype.hasOwnProperty.call(varsFromURL, name)) { + Object.assign(variable, { value: varsFromURL[name] }); } }); - return variables; + return parsedYmlVariables; }; /** diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index 561d1da9710..6206611adf7 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -201,8 +201,10 @@ export const removePrefixFromLabel = label => * @returns {Object} */ export const convertVariablesForURL = variables => - Object.keys(variables || {}).reduce((acc, key) => { - acc[addPrefixToLabel(key)] = variables[key]?.value; + variables.reduce((acc, { name, value }) => { + if (value !== null) { + acc[addPrefixToLabel(name)] = value; + } return acc; }, {}); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 3e680c59910..c1f5b3a3c7b 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -82,7 +82,6 @@ } .board-title-caret { - cursor: pointer; border-radius: $border-radius-default; line-height: $gl-spacing-scale-5; height: $gl-spacing-scale-5; @@ -109,7 +108,6 @@ .board-title { flex-direction: column; height: 100%; - padding: $gl-padding-8 0; } .board-title-caret { @@ -203,8 +201,7 @@ flex-grow: 1; } -.board-delete { - color: $gray-darkest; +.board-delete.gl-button { background-color: transparent; outline: 0; @@ -581,5 +578,29 @@ .board-epics-swimlanes { overflow-x: auto; - min-height: 600px; + min-height: calc(100vh - #{$issue-board-list-difference-xs}); + + @include media-breakpoint-only(sm) { + min-height: calc(100vh - #{$issue-board-list-difference-sm}); + } + + @include media-breakpoint-up(md) { + min-height: calc(100vh - #{$issue-board-list-difference-md}); + } + + .with-performance-bar & { + min-height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height}); + + @include media-breakpoint-only(sm) { + min-height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height}); + } + + @include media-breakpoint-up(md) { + min-height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height}); + } + } +} + +.board-header-collapsed-info-icon:hover { + color: $gray-900; } diff --git a/app/helpers/notify_helper.rb b/app/helpers/notify_helper.rb new file mode 100644 index 00000000000..fb68029928c --- /dev/null +++ b/app/helpers/notify_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module NotifyHelper + def merge_request_reference_link(entity, *args) + link_to(entity.to_reference, merge_request_url(entity, *args)) + end + + def issue_reference_link(entity, *args) + link_to(entity.to_reference, issue_url(entity, *args)) + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 8df3b8356e7..2470312d8ac 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -750,6 +750,10 @@ module ProjectsHelper ::Feature.enabled?(:resource_access_token, project) end + + def render_service_desk_menu? + false + end end ProjectsHelper.prepend_if_ee('EE::ProjectsHelper') diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 4e14bb4e92c..3f960af24d5 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -18,7 +18,7 @@ class SystemNoteMetadata < ApplicationRecord designs_added designs_modified designs_removed designs_discussion_added title time_tracking branch milestone discussion task moved opened closed merged duplicate locked unlocked outdated - tag due_date pinned_embed cherry_pick health_status + tag due_date pinned_embed cherry_pick health_status approved ].freeze validates :note, presence: true diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index fdf777f2bbe..7f71906bc89 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -138,6 +138,10 @@ class EventCreateService event end + def approve_mr(merge_request, current_user) + create_record_event(merge_request, current_user, :approved) + end + private def existing_wiki_event(wiki_page_meta, action) diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb new file mode 100644 index 00000000000..0fe165303f2 --- /dev/null +++ b/app/services/merge_requests/approval_service.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module MergeRequests + class ApprovalService < MergeRequests::BaseService + def execute(merge_request) + approval = merge_request.approvals.new(user: current_user) + + return unless save_approval(approval) + + reset_approvals_cache(merge_request) + create_event(merge_request) + create_approval_note(merge_request) + mark_pending_todos_as_done(merge_request) + execute_approval_hooks(merge_request, current_user) + end + + private + + def reset_approvals_cache(merge_request) + merge_request.approvals.reset + end + + def execute_approval_hooks(merge_request, current_user) + # Only one approval is required for a merge request to be approved + execute_hooks(merge_request, 'approved') + end + + def save_approval(approval) + Approval.safe_ensure_unique do + approval.save + end + end + + def create_approval_note(merge_request) + SystemNoteService.approve_mr(merge_request, current_user) + end + + def mark_pending_todos_as_done(merge_request) + todo_service.resolve_todos_for_target(merge_request, current_user) + end + + def create_event(merge_request) + event_service.approve_mr(merge_request, current_user) + end + end +end + +MergeRequests::ApprovalService.prepend_if_ee('EE::MergeRequests::ApprovalService') diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 6bf04c55415..41e9d624ec6 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -273,6 +273,26 @@ module SystemNoteService ::SystemNotes::DesignManagementService.new(noteable: design.issue, project: design.project, author: discussion_note.author).design_discussion_added(discussion_note) end + + # Called when the merge request is approved by user + # + # noteable - Noteable object + # user - User performing approve + # + # Example Note text: + # + # "approved this merge request" + # + # Returns the created Note object + def approve_mr(noteable, user) + merge_requests_service(noteable, noteable.project, user).approve_mr + end + + private + + def merge_requests_service(noteable, project, author) + ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author) + end end SystemNoteService.prepend_if_ee('EE::SystemNoteService') diff --git a/app/services/system_notes/merge_requests_service.rb b/app/services/system_notes/merge_requests_service.rb index baf26245eb9..0c7016c0d23 100644 --- a/app/services/system_notes/merge_requests_service.rb +++ b/app/services/system_notes/merge_requests_service.rb @@ -150,6 +150,19 @@ module SystemNotes create_note(summary) end + + # Called when the merge request is approved by user + # + # Example Note text: + # + # "approved this merge request" + # + # Returns the created Note object + def approve_mr + body = "approved this merge request" + + create_note(NoteSummary.new(noteable, project, author, body, action: 'approved')) + end end end diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml index 2aa753e0d55..6caa0e59e8f 100644 --- a/app/views/notify/closed_merge_request_email.html.haml +++ b/app/views/notify/closed_merge_request_email.html.haml @@ -1,2 +1,3 @@ %p - Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)} + Merge Request #{merge_request_reference_link(@merge_request)} + was closed by #{sanitize_name(@updated_by.name)} diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index 6e84f9fb355..8546da2d7f0 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,6 +1,6 @@ Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)} -Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} +Merge Request URL: #{project_merge_request_url(@merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml index ffb416abf72..a15c5a752d4 100644 --- a/app/views/notify/merge_request_status_email.html.haml +++ b/app/views/notify/merge_request_status_email.html.haml @@ -1,2 +1,3 @@ %p - Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)} + Merge Request #{merge_request_reference_link(@merge_request)} + was #{@mr_status} by #{sanitize_name(@updated_by.name)} diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index e3b24bbd405..3d7115856d4 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -1,6 +1,6 @@ Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)} -Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} +Merge Request URL: #{project_merge_request_url(@merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') diff --git a/app/views/notify/merge_request_unmergeable_email.html.haml b/app/views/notify/merge_request_unmergeable_email.html.haml index 7ec0c1ef390..ee459a26551 100644 --- a/app/views/notify/merge_request_unmergeable_email.html.haml +++ b/app/views/notify/merge_request_unmergeable_email.html.haml @@ -1,2 +1,2 @@ %p - Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to conflict. + Merge Request #{merge_request_reference_link(@merge_request)} can no longer be merged due to conflict. diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml index e9708a297d7..412a0887186 100644 --- a/app/views/notify/merge_request_unmergeable_email.text.haml +++ b/app/views/notify/merge_request_unmergeable_email.text.haml @@ -1,6 +1,6 @@ Merge Request #{@merge_request.to_reference} can no longer be merged due to conflict. -Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} +Merge Request URL: #{project_merge_request_url(@merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml index 341aa6f8103..c84c0d1d14b 100644 --- a/app/views/notify/merged_merge_request_email.html.haml +++ b/app/views/notify/merged_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} was merged + Merge Request #{merge_request_reference_link(@merge_request)} was merged diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index 52e110a98f6..7f0a50e9248 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,5 +1,5 @@ %p.details - #{link_to @issue.author_name, user_url(@issue.author)} created an issue #{link_to @issue.to_reference(full: false), issue_url(@issue)}: + #{link_to @issue.author_name, user_url(@issue.author)} created an issue #{issue_reference_link(@issue)}: - if @issue.assignees.any? %p diff --git a/app/views/notify/new_mention_in_merge_request_email.html.haml b/app/views/notify/new_mention_in_merge_request_email.html.haml index b061f9c106e..ddcf287e501 100644 --- a/app/views/notify/new_mention_in_merge_request_email.html.haml +++ b/app/views/notify/new_mention_in_merge_request_email.html.haml @@ -1,4 +1,4 @@ %p - You have been mentioned in Merge Request #{@merge_request.to_reference} + You have been mentioned in Merge Request #{merge_request_reference_link(@merge_request)} = render template: 'notify/new_merge_request_email' diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml index 97258833cfc..3e9f9b442e0 100644 --- a/app/views/notify/push_to_merge_request_email.html.haml +++ b/app/views/notify/push_to_merge_request_email.html.haml @@ -1,7 +1,7 @@ %h3 = sanitize_name(@updated_by_user.name) pushed new commits to merge request - = link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)) + = merge_request_reference_link(@merge_request) - if @existing_commits.any? - count = @existing_commits.size diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml index 10c8e158846..55cbd62b7e8 100644 --- a/app/views/notify/push_to_merge_request_email.text.haml +++ b/app/views/notify/push_to_merge_request_email.text.haml @@ -1,6 +1,4 @@ -#{sanitize_name(@updated_by_user.name)} pushed new commits to merge request #{@merge_request.to_reference} -\ -#{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))} +#{sanitize_name(@updated_by_user.name)} pushed new commits to merge request #{merge_request_reference_link(@merge_request)} \ - if @existing_commits.any? - count = @existing_commits.size diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml index 502b8f21e35..0b3c56c9bd1 100644 --- a/app/views/notify/resolved_all_discussions_email.html.haml +++ b/app/views/notify/resolved_all_discussions_email.html.haml @@ -1,2 +1,3 @@ %p - All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{sanitize_name(@resolved_by.name)} + All discussions on Merge Request #{merge_request_reference_link(@merge_request)} + were resolved by #{sanitize_name(@resolved_by.name)} diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index a3083fa2081..eeff91f631c 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -7,13 +7,13 @@ .col-form-label.col-sm-2 = f.label :title, _('Title') .col-sm-10 - = f.text_field :title, maxlength: 255, class: 'qa-milestone-title form-control', required: true, autofocus: true + = f.text_field :title, maxlength: 255, class: 'form-control', data: { qa_selector: 'milestone_title_field' }, required: true, autofocus: true .form-group.row.milestone-description .col-form-label.col-sm-2 = f.label :description, _('Description') .col-sm-10 = render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do - = render 'shared/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: _('Write milestone description...') + = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: _('Write milestone description...') = render 'shared/notes/hints' .clearfix .error-alert @@ -21,7 +21,7 @@ .form-actions - if @milestone.new_record? - = f.submit _('Create milestone'), class: 'btn-success btn qa-milestone-create-button' + = f.submit _('Create milestone'), class: 'btn-success btn', data: { qa_selector: 'create_milestone_button' } = link_to _('Cancel'), project_milestones_path(@project), class: 'btn btn-cancel' - else = f.submit _('Save changes'), class: 'btn-success btn' diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index c89566dac90..2bab2a0fb03 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -7,7 +7,7 @@ = render 'shared/milestones/search_form' = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) - = link_to new_project_milestone_path(@project), class: 'btn btn-success qa-new-project-milestone', title: _('New milestone') do + = link_to new_project_milestone_path(@project), class: 'btn btn-success', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone') do = _('New milestone') .milestones diff --git a/app/views/shared/milestones/_description.html.haml b/app/views/shared/milestones/_description.html.haml index 5ff110bf94b..76d6c765ed6 100644 --- a/app/views/shared/milestones/_description.html.haml +++ b/app/views/shared/milestones/_description.html.haml @@ -1,8 +1,9 @@ .detail-page-description.milestone-detail - %h2.title + %h2{ data: { qa_selector: "milestone_title_content" } } + .title = markdown_field(milestone, :title) - if milestone.try(:description).present? - %div + %div{ data: { qa_selector: "milestone_description_content" } } .description.md = markdown_field(milestone, :description) diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml index 6dbc460d9bf..46e96cf8edf 100644 --- a/app/views/shared/milestones/_form_dates.html.haml +++ b/app/views/shared/milestones/_form_dates.html.haml @@ -3,11 +3,11 @@ .col-form-label.col-sm-2 = f.label :start_date, _('Start Date') .col-sm-10 - = f.text_field :start_date, class: "datepicker form-control", placeholder: _('Select start date'), autocomplete: 'off' + = f.text_field :start_date, class: "datepicker form-control", data: { qa_selector: "start_date_field" }, placeholder: _('Select start date'), autocomplete: 'off' %a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" }= _('Clear start date') .form-group.row .col-form-label.col-sm-2 = f.label :due_date, _('Due Date') .col-sm-10 - = f.text_field :due_date, class: "datepicker form-control", placeholder: _('Select due date'), autocomplete: 'off' + = f.text_field :due_date, class: "datepicker form-control", data: { qa_selector: "due_date_field" }, placeholder: _('Select due date'), autocomplete: 'off' %a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" }= _('Clear due date') diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 6460d1a46e8..ae5bf9572bd 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -6,7 +6,8 @@ .row .col-sm-6 .gl-mb-2 - %strong= link_to truncate(milestone.title, length: 100), milestone_path(milestone) + %strong{ data: { qa_selector: "milestone_link", qa_milestone_title: milestone.title } } + = link_to truncate(milestone.title, length: 100), milestone_path(milestone) - if @group = " - #{milestone_type}" diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 160f6487439..7fd657ec2dd 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -24,7 +24,7 @@ - if @project && can?(current_user, :admin_milestone, @project) = link_to s_('MilestoneSidebar|Edit'), edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right' .value - %span.value-content + %span.value-content{ data: { qa_selector: 'start_date_content' } } - if milestone.start_date %span.bold= milestone.start_date.to_s(:medium) - else @@ -60,7 +60,7 @@ - if @project && can?(current_user, :admin_milestone, @project) = link_to s_('MilestoneSidebar|Edit'), edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right' .value.hide-collapsed - %span.value-content + %span.value-content{ data: { qa_selector: 'due_date_content' } } - if milestone.due_date %span.bold= milestone.due_date.to_s(:medium) - else diff --git a/changelogs/unreleased/31099-MR-API-allow-NOT-params.yml b/changelogs/unreleased/31099-MR-API-allow-NOT-params.yml new file mode 100644 index 00000000000..2e7f9994cbe --- /dev/null +++ b/changelogs/unreleased/31099-MR-API-allow-NOT-params.yml @@ -0,0 +1,5 @@ +--- +title: Add 'not' params to MergeRequests API endpoint +merge_request: 35391 +author: +type: added diff --git a/changelogs/unreleased/add-link-to-mrs-references.yml b/changelogs/unreleased/add-link-to-mrs-references.yml new file mode 100644 index 00000000000..13686060b14 --- /dev/null +++ b/changelogs/unreleased/add-link-to-mrs-references.yml @@ -0,0 +1,5 @@ +--- +title: Render Merge request reference as link in email templates +merge_request: 33316 +author: +type: changed diff --git a/changelogs/unreleased/tr-alert-issue-link.yml b/changelogs/unreleased/tr-alert-issue-link.yml new file mode 100644 index 00000000000..3ec3a3a5ce1 --- /dev/null +++ b/changelogs/unreleased/tr-alert-issue-link.yml @@ -0,0 +1,5 @@ +--- +title: Add issue column to alert list +merge_request: 35291 +author: +type: added diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index 1e233b890a2..2a00f7dc4e8 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -165,54 +165,54 @@ To use an external Prometheus server: ```yaml scrape_configs: - - job_name: nginx - static_configs: - - targets: - - 1.1.1.1:8060 - - job_name: redis - static_configs: - - targets: - - 1.1.1.1:9121 - - job_name: postgres - static_configs: - - targets: - - 1.1.1.1:9187 - - job_name: node - static_configs: - - targets: - - 1.1.1.1:9100 - - job_name: gitlab-workhorse - static_configs: - - targets: - - 1.1.1.1:9229 - - job_name: gitlab-rails - metrics_path: "/-/metrics" - static_configs: - - targets: - - 1.1.1.1:8080 - - job_name: gitlab-sidekiq - static_configs: - - targets: - - 1.1.1.1:8082 - - job_name: gitlab_exporter_database - metrics_path: "/database" - static_configs: - - targets: - - 1.1.1.1:9168 - - job_name: gitlab_exporter_sidekiq - metrics_path: "/sidekiq" - static_configs: - - targets: - - 1.1.1.1:9168 - - job_name: gitlab_exporter_process - metrics_path: "/process" - static_configs: - - targets: - - 1.1.1.1:9168 - - job_name: gitaly - static_configs: - - targets: - - 1.1.1.1:9236 + - job_name: nginx + static_configs: + - targets: + - 1.1.1.1:8060 + - job_name: redis + static_configs: + - targets: + - 1.1.1.1:9121 + - job_name: postgres + static_configs: + - targets: + - 1.1.1.1:9187 + - job_name: node + static_configs: + - targets: + - 1.1.1.1:9100 + - job_name: gitlab-workhorse + static_configs: + - targets: + - 1.1.1.1:9229 + - job_name: gitlab-rails + metrics_path: "/-/metrics" + static_configs: + - targets: + - 1.1.1.1:8080 + - job_name: gitlab-sidekiq + static_configs: + - targets: + - 1.1.1.1:8082 + - job_name: gitlab_exporter_database + metrics_path: "/database" + static_configs: + - targets: + - 1.1.1.1:9168 + - job_name: gitlab_exporter_sidekiq + metrics_path: "/sidekiq" + static_configs: + - targets: + - 1.1.1.1:9168 + - job_name: gitlab_exporter_process + metrics_path: "/process" + static_configs: + - targets: + - 1.1.1.1:9168 + - job_name: gitaly + static_configs: + - targets: + - 1.1.1.1:9236 ``` 1. Reload the Prometheus server. diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md index 12f785a3e3d..402170fba37 100644 --- a/doc/api/geo_nodes.md +++ b/doc/api/geo_nodes.md @@ -367,8 +367,9 @@ Example response: "package_files_count": 10, "package_files_checksummed_count": 10, "package_files_checksum_failed_count": 0, - "package_files_synced_count": 10, - "package_files_failed_count": 5 + "package_files_registry_count": 10, + "package_files_synced_count": 6, + "package_files_failed_count": 3 }, { "geo_node_id": 2, @@ -440,8 +441,9 @@ Example response: "package_files_count": 10, "package_files_checksummed_count": 10, "package_files_checksum_failed_count": 0, - "package_files_synced_count": 10, - "package_files_failed_count": 5 + "package_files_registry_count": 10, + "package_files_synced_count": 6, + "package_files_failed_count": 3 } ] ``` diff --git a/doc/api/issues.md b/doc/api/issues.md index b216be03ac3..6078c77e45a 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -70,7 +70,7 @@ GET /issues?confidential=true | `updated_after` | datetime | no | Return issues updated on or after the given time | | `updated_before` | datetime | no | Return issues updated on or before the given time | | `confidential` | boolean | no | Filter confidential or public issues. | -| `not` | Hash | no | Return issues that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji`, `search`, `in` | +| `not` | Hash | no | Return issues that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji` | | `non_archived` | boolean | no | Return issues only from non-archived projects. If `false`, response will return issues from both archived and non-archived projects. Default is `true`. _(Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/197170))_ | ```shell diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 41c0428485f..5f28ff8d69e 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -64,6 +64,7 @@ Parameters: | `search` | string | no | Search merge requests against their `title` and `description` | | `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | | `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests | +| `not` | Hash | no | Return merge requests that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji` | NOTE: **Note:** [Starting in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890), diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md index 392dd4fdeba..b6bd01ecf58 100644 --- a/doc/ci/caching/index.md +++ b/doc/ci/caching/index.md @@ -226,14 +226,14 @@ image: node:latest cache: key: ${CI_COMMIT_REF_SLUG} paths: - - .npm/ + - .npm/ before_script: - npm ci --cache .npm --prefer-offline test_async: script: - - node ./specs/start.js ./specs/async.spec.js + - node ./specs/start.js ./specs/async.spec.js ``` ### Caching PHP dependencies @@ -253,16 +253,16 @@ image: php:7.2 cache: key: ${CI_COMMIT_REF_SLUG} paths: - - vendor/ + - vendor/ before_script: -# Install and run Composer -- curl --show-error --silent https://getcomposer.org/installer | php -- php composer.phar install + # Install and run Composer + - curl --show-error --silent https://getcomposer.org/installer | php + - php composer.phar install test: script: - - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never + - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never ``` ### Caching Python dependencies @@ -301,9 +301,9 @@ before_script: test: script: - - python setup.py test - - pip install flake8 - - flake8 . + - python setup.py test + - pip install flake8 + - flake8 . ``` ### Caching Ruby dependencies @@ -330,7 +330,7 @@ before_script: rspec: script: - - rspec spec + - rspec spec ``` ### Caching Go dependencies @@ -354,7 +354,7 @@ test: image: golang:1.13 extends: .go-cache script: - - go test ./... -v -short + - go test ./... -v -short ``` ## Availability of the cache @@ -391,28 +391,28 @@ stages: ```yaml stages: -- build -- test + - build + - test before_script: -- echo "Hello" + - echo "Hello" job A: stage: build script: - - mkdir vendor/ - - echo "build" > vendor/hello.txt + - mkdir vendor/ + - echo "build" > vendor/hello.txt cache: key: build-cache paths: - - vendor/ + - vendor/ after_script: - - echo "World" + - echo "World" job B: stage: test script: - - cat vendor/hello.txt + - cat vendor/hello.txt cache: key: build-cache ``` @@ -483,8 +483,8 @@ cache when the pipeline is run for a second time. ```yaml stages: -- build -- test + - build + - test job A: stage: build @@ -492,7 +492,7 @@ job A: cache: key: same-key paths: - - public/ + - public/ job B: stage: test @@ -500,7 +500,7 @@ job B: cache: key: same-key paths: - - vendor/ + - vendor/ ``` 1. `job A` runs. @@ -520,8 +520,8 @@ will be different): ```yaml stages: -- build -- test + - build + - test job A: stage: build @@ -529,7 +529,7 @@ job A: cache: key: keyA paths: - - vendor/ + - vendor/ job B: stage: test @@ -537,7 +537,7 @@ job B: cache: key: keyB paths: - - vendor/ + - vendor/ ``` In that case, even if the `key` is different (no fear of overwriting), you diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 2448bb536ab..692ad12eb59 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -149,14 +149,14 @@ the job will fail: ```yaml job: services: - - php:7 - - node:latest - - golang:1.10 + - php:7 + - node:latest + - golang:1.10 image: alpine:3.7 script: - - php -v - - node -v - - go version + - php -v + - node -v + - go version ``` If you need to have `php`, `node` and `go` available for your script, you should @@ -176,7 +176,7 @@ You can then use for example the [tutum/wordpress](https://hub.docker.com/r/tutu ```yaml services: -- tutum/wordpress:latest + - tutum/wordpress:latest ``` If you don't [specify a service alias](#available-settings-for-services), @@ -219,7 +219,7 @@ default: test: script: - - bundle exec rake spec + - bundle exec rake spec ``` The image name must be in one of the following formats: @@ -238,16 +238,16 @@ default: test:2.6: image: ruby:2.6 services: - - postgres:11.7 + - postgres:11.7 script: - - bundle exec rake spec + - bundle exec rake spec test:2.7: image: ruby:2.7 services: - - postgres:12.2 + - postgres:12.2 script: - - bundle exec rake spec + - bundle exec rake spec ``` Or you can pass some [extended configuration options](#extended-docker-configuration-options) @@ -260,17 +260,17 @@ default: entrypoint: ["/bin/bash"] services: - - name: my-postgres:11.7 - alias: db-postgres - entrypoint: ["/usr/local/bin/db-postgres"] - command: ["start"] + - name: my-postgres:11.7 + alias: db-postgres + entrypoint: ["/usr/local/bin/db-postgres"] + command: ["start"] before_script: - - bundle install + - bundle install test: script: - - bundle exec rake spec + - bundle exec rake spec ``` ## Passing environment variables to services @@ -292,21 +292,21 @@ variables: POSTGRES_INITDB_ARGS: "--encoding=UTF8 --data-checksums" services: -- name: postgres:11.7 - alias: db - entrypoint: ["docker-entrypoint.sh"] - command: ["postgres"] + - name: postgres:11.7 + alias: db + entrypoint: ["docker-entrypoint.sh"] + command: ["postgres"] image: name: ruby:2.6 entrypoint: ["/bin/bash"] before_script: -- bundle install + - bundle install test: script: - - bundle exec rake spec + - bundle exec rake spec ``` ## Extended Docker configuration options @@ -330,8 +330,8 @@ For example, the following two definitions are equal: image: "registry.example.com/my/image:latest" services: - - postgresql:9.4 - - redis:latest + - postgresql:9.4 + - redis:latest ``` 1. Using a map as an option to `image` and `services`. The use of `image:name` is @@ -342,8 +342,8 @@ For example, the following two definitions are equal: name: "registry.example.com/my/image:latest" services: - - name: postgresql:9.4 - - name: redis:latest + - name: postgresql:9.4 + - name: redis:latest ``` ### Available settings for `image` @@ -378,8 +378,8 @@ would not work properly: ```yaml services: -- mysql:latest -- mysql:latest + - mysql:latest + - mysql:latest ``` The Runner would start two containers using the `mysql:latest` image, but both @@ -392,10 +392,10 @@ look like: ```yaml services: -- name: mysql:latest - alias: mysql-1 -- name: mysql:latest - alias: mysql-2 + - name: mysql:latest + alias: mysql-1 + - name: mysql:latest + alias: mysql-2 ``` The Runner will still start two containers using the `mysql:latest` image, @@ -427,7 +427,7 @@ CMD ["/usr/bin/super-sql", "run"] # .gitlab-ci.yml services: -- my-super-sql:latest + - my-super-sql:latest ``` After the new extended Docker configuration options, you can now simply @@ -437,8 +437,8 @@ set a `command` in `.gitlab-ci.yml`, like: # .gitlab-ci.yml services: -- name: super/sql:latest - command: ["/usr/bin/super-sql", "run"] + - name: super/sql:latest + command: ["/usr/bin/super-sql", "run"] ``` As you can see, the syntax of `command` is similar to [Dockerfile's `CMD`](https://docs.docker.com/engine/reference/builder/#cmd). diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index fa070ec68f6..501ebafba07 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -94,7 +94,7 @@ deploy_staging: name: staging url: https://staging.example.com only: - - master + - master ``` We have defined three [stages](../yaml/README.md#stages): @@ -259,7 +259,7 @@ deploy_staging: name: staging url: https://staging.example.com only: - - master + - master deploy_prod: stage: deploy @@ -270,7 +270,7 @@ deploy_prod: url: https://example.com when: manual only: - - master + - master ``` The `when: manual` action: @@ -402,7 +402,7 @@ deploy: kubernetes: namespace: production only: - - master + - master ``` When deploying to a Kubernetes cluster using GitLab's Kubernetes integration, @@ -483,7 +483,7 @@ deploy_staging: name: staging url: https://staging.example.com only: - - master + - master deploy_prod: stage: deploy @@ -494,7 +494,7 @@ deploy_prod: url: https://example.com when: manual only: - - master + - master ``` A more realistic example would also include copying files to a location where a diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md index 87cee1820bc..e0d4f3f2402 100644 --- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md @@ -61,10 +61,10 @@ content: ```yaml --- applications: -- name: gitlab-hello-world - random-route: true - memory: 1G - path: target/demo-0.0.1-SNAPSHOT.jar + - name: gitlab-hello-world + random-route: true + memory: 1G + path: target/demo-0.0.1-SNAPSHOT.jar ``` ## Configure GitLab CI/CD to deploy your application @@ -96,11 +96,11 @@ build: production: stage: deploy script: - - curl --location "https://cli.run.pivotal.io/stable?release=linux64-binary&source=github" | tar zx - - ./cf login -u $CF_USERNAME -p $CF_PASSWORD -a api.run.pivotal.io - - ./cf push + - curl --location "https://cli.run.pivotal.io/stable?release=linux64-binary&source=github" | tar zx + - ./cf login -u $CF_USERNAME -p $CF_PASSWORD -a api.run.pivotal.io + - ./cf push only: - - master + - master ``` We've used the `java:8` [Docker diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md index ec02fb6dd43..3192b365c83 100644 --- a/doc/ci/examples/deployment/README.md +++ b/doc/ci/examples/deployment/README.md @@ -45,8 +45,8 @@ All possible parameters can be found here: <https://github.com/travis-ci/dpl#her staging: stage: deploy script: - - gem install dpl - - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY + - gem install dpl + - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY ``` In the above example we use Dpl to deploy `my-app-staging` to Heroku server with API key stored in `HEROKU_STAGING_API_KEY` secure variable. @@ -64,12 +64,12 @@ You will have to install it: staging: stage: deploy script: - - apt-get update -yq - - apt-get install -y ruby-dev - - gem install dpl - - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY + - apt-get update -yq + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY only: - - master + - master ``` The first line `apt-get update -yq` updates the list of available packages, @@ -89,18 +89,18 @@ The final `.gitlab-ci.yml` for that setup would look like this: staging: stage: deploy script: - - gem install dpl - - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY + - gem install dpl + - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY only: - - master + - master production: stage: deploy script: - - gem install dpl - - dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY + - gem install dpl + - dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY only: - - tags + - tags ``` We created two deploy jobs that are executed on different events: diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md index cea6f26181f..067d92c2275 100644 --- a/doc/ci/examples/deployment/composer-npm-deploy.md +++ b/doc/ci/examples/deployment/composer-npm-deploy.md @@ -150,7 +150,7 @@ before_script: stage_deploy: artifacts: paths: - - build/ + - build/ only: - dev script: diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md index 51d9f169939..35a35d97a4b 100644 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md @@ -274,19 +274,19 @@ just pack them up in the cache. Here is the full `build` job: ```yaml build: - stage: build - script: - - npm i gulp -g - - npm i - - gulp - - gulp build-test - cache: - policy: push - paths: - - node_modules - artifacts: - paths: - - built + stage: build + script: + - npm i gulp -g + - npm i + - gulp + - gulp build-test + cache: + policy: push + paths: + - node_modules + artifacts: + paths: + - built ``` ### Test your game with GitLab CI/CD @@ -301,18 +301,18 @@ Following the YAML structure, the `test` job should look like this: ```yaml test: - stage: test - script: - - npm i gulp -g - - npm i - - gulp run-test - cache: - policy: push - paths: - - node_modules/ - artifacts: - paths: - - built/ + stage: test + script: + - npm i gulp -g + - npm i + - gulp run-test + cache: + policy: push + paths: + - node_modules/ + artifacts: + paths: + - built/ ``` We have added unit tests for a `Weapon` class that shoots on a specified interval. @@ -325,33 +325,33 @@ Our entire `.gitlab-ci.yml` file should now look like this: image: node:10 build: - stage: build - script: - - npm i gulp -g - - npm i - - gulp - - gulp build-test - cache: - policy: push - paths: - - node_modules/ - artifacts: - paths: - - built/ + stage: build + script: + - npm i gulp -g + - npm i + - gulp + - gulp build-test + cache: + policy: push + paths: + - node_modules/ + artifacts: + paths: + - built/ test: - stage: test - script: - - npm i gulp -g - - npm i - - gulp run-test - cache: - policy: pull - paths: - - node_modules/ - artifacts: - paths: - - built/ + stage: test + script: + - npm i gulp -g + - npm i + - gulp run-test + cache: + policy: pull + paths: + - node_modules/ + artifacts: + paths: + - built/ ``` ### Run your CI/CD pipeline @@ -445,18 +445,18 @@ trigger the `deploy` job of our pipeline. Put these together to get the followin ```yaml deploy: - stage: deploy - variables: - AWS_ACCESS_KEY_ID: "$AWS_KEY_ID" - AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET" - script: - - apt-get update - - apt-get install -y python3-dev python3-pip - - easy_install3 -U pip - - pip3 install --upgrade awscli - - aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete - only: - - master + stage: deploy + variables: + AWS_ACCESS_KEY_ID: "$AWS_KEY_ID" + AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET" + script: + - apt-get update + - apt-get install -y python3-dev python3-pip + - easy_install3 -U pip + - pip3 install --upgrade awscli + - aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete + only: + - master ``` Be sure to update the region and S3 URL in that last script command to fit your setup. @@ -466,46 +466,46 @@ Our final configuration file `.gitlab-ci.yml` looks like: image: node:10 build: - stage: build - script: - - npm i gulp -g - - npm i - - gulp - - gulp build-test - cache: - policy: push - paths: - - node_modules/ - artifacts: - paths: - - built/ + stage: build + script: + - npm i gulp -g + - npm i + - gulp + - gulp build-test + cache: + policy: push + paths: + - node_modules/ + artifacts: + paths: + - built/ test: - stage: test - script: - - npm i gulp -g - - gulp run-test - cache: - policy: pull - paths: - - node_modules/ - artifacts: - paths: - - built/ + stage: test + script: + - npm i gulp -g + - gulp run-test + cache: + policy: pull + paths: + - node_modules/ + artifacts: + paths: + - built/ deploy: - stage: deploy - variables: - AWS_ACCESS_KEY_ID: "$AWS_KEY_ID" - AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET" - script: - - apt-get update - - apt-get install -y python3-dev python3-pip - - easy_install3 -U pip - - pip3 install --upgrade awscli - - aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete - only: - - master + stage: deploy + variables: + AWS_ACCESS_KEY_ID: "$AWS_KEY_ID" + AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET" + script: + - apt-get update + - apt-get install -y python3-dev python3-pip + - easy_install3 -U pip + - pip3 install --upgrade awscli + - aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete + only: + - master ``` ## Conclusion diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index e7768868c15..cc62e9316f2 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -76,7 +76,7 @@ environment, let's add it in `.gitlab-ci.yml`: ... before_script: -- bash ci/docker_install.sh > /dev/null + - bash ci/docker_install.sh > /dev/null ... ``` @@ -88,7 +88,7 @@ Last step, run the actual tests using `phpunit`: test:app: script: - - phpunit --configuration phpunit_myapp.xml + - phpunit --configuration phpunit_myapp.xml ... ``` @@ -104,11 +104,11 @@ image: php:5.6 before_script: # Install dependencies -- bash ci/docker_install.sh > /dev/null + - bash ci/docker_install.sh > /dev/null test:app: script: - - phpunit --configuration phpunit_myapp.xml + - phpunit --configuration phpunit_myapp.xml ``` ### Test against different PHP versions in Docker builds @@ -119,19 +119,19 @@ with a different Docker image version and the runner will do the rest: ```yaml before_script: # Install dependencies -- bash ci/docker_install.sh > /dev/null + - bash ci/docker_install.sh > /dev/null # We test PHP5.6 test:5.6: image: php:5.6 script: - - phpunit --configuration phpunit_myapp.xml + - phpunit --configuration phpunit_myapp.xml # We test PHP7.0 (good luck with that) test:7.0: image: php:7.0 script: - - phpunit --configuration phpunit_myapp.xml + - phpunit --configuration phpunit_myapp.xml ``` ### Custom PHP configuration in Docker builds @@ -142,7 +142,7 @@ add a `before_script` action: ```yaml before_script: -- cp my_php.ini /usr/local/etc/php/conf.d/test.ini + - cp my_php.ini /usr/local/etc/php/conf.d/test.ini ``` Of course, `my_php.ini` must be present in the root directory of your repository. @@ -166,7 +166,7 @@ Next, add the following snippet to your `.gitlab-ci.yml`: ```yaml test:app: script: - - phpunit --configuration phpunit_myapp.xml + - phpunit --configuration phpunit_myapp.xml ``` Finally, push to GitLab and let the tests begin! @@ -217,11 +217,11 @@ you can use [atoum](https://github.com/atoum/atoum): ```yaml before_script: -- wget http://downloads.atoum.org/nightly/mageekguy.atoum.phar + - wget http://downloads.atoum.org/nightly/mageekguy.atoum.phar test:atoum: script: - - php mageekguy.atoum.phar + - php mageekguy.atoum.phar ``` ### Using Composer @@ -238,16 +238,16 @@ following in your `.gitlab-ci.yml`: # your git repository. cache: paths: - - vendor/ + - vendor/ before_script: # Install composer dependencies -- wget https://composer.github.io/installer.sig -O - -q | tr -d '\n' > installer.sig -- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" -- php -r "if (hash_file('SHA384', 'composer-setup.php') === file_get_contents('installer.sig')) { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" -- php composer-setup.php -- php -r "unlink('composer-setup.php'); unlink('installer.sig');" -- php composer.phar install + - wget https://composer.github.io/installer.sig -O - -q | tr -d '\n' > installer.sig + - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + - php -r "if (hash_file('SHA384', 'composer-setup.php') === file_get_contents('installer.sig')) { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" + - php composer-setup.php + - php -r "unlink('composer-setup.php'); unlink('installer.sig');" + - php composer.phar install ... ``` diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md index 30a64e65607..d01e9663795 100644 --- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -23,32 +23,32 @@ stages: test: stage: test script: - # this configures Django application to use attached postgres database that is run on `postgres` host - - export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app - - apt-get update -qy - - apt-get install -y python-dev python-pip - - pip install -r requirements.txt - - python manage.py test + # this configures Django application to use attached postgres database that is run on `postgres` host + - export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app + - apt-get update -qy + - apt-get install -y python-dev python-pip + - pip install -r requirements.txt + - python manage.py test staging: stage: deploy script: - - apt-get update -qy - - apt-get install -y ruby-dev - - gem install dpl - - dpl --provider=heroku --app=gitlab-ci-python-test-staging --api-key=$HEROKU_STAGING_API_KEY + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-python-test-staging --api-key=$HEROKU_STAGING_API_KEY only: - - master + - master production: stage: deploy script: - - apt-get update -qy - - apt-get install -y ruby-dev - - gem install dpl - - dpl --provider=heroku --app=gitlab-ci-python-test-prod --api-key=$HEROKU_PRODUCTION_API_KEY + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-python-test-prod --api-key=$HEROKU_PRODUCTION_API_KEY only: - - tags + - tags ``` This project has three jobs: diff --git a/doc/ci/jenkins/index.md b/doc/ci/jenkins/index.md index 2c327181461..1d029dcdd14 100644 --- a/doc/ci/jenkins/index.md +++ b/doc/ci/jenkins/index.md @@ -181,8 +181,8 @@ pdf: script: xelatex mycv.tex artifacts: paths: - - ./mycv.pdf - - ./output/ + - ./mycv.pdf + - ./output/ expire_in: 1 week ``` diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md index 683edc50cb7..444569a6eaf 100644 --- a/doc/ci/merge_request_pipelines/index.md +++ b/doc/ci/merge_request_pipelines/index.md @@ -65,19 +65,19 @@ build: stage: build script: ./build only: - - master + - master test: stage: test script: ./test only: - - merge_requests + - merge_requests deploy: stage: deploy script: ./deploy only: - - master + - master ``` #### Excluding certain jobs diff --git a/doc/ci/migration/circleci.md b/doc/ci/migration/circleci.md index f6868abc334..625b15ca4fb 100644 --- a/doc/ci/migration/circleci.md +++ b/doc/ci/migration/circleci.md @@ -250,14 +250,14 @@ image: node:latest cache: key: $CI_COMMIT_REF_SLUG paths: - - .npm/ + - .npm/ before_script: - npm ci --cache .npm --prefer-offline test_async: script: - - node ./specs/start.js ./specs/async.spec.js + - node ./specs/start.js ./specs/async.spec.js ``` ## Contexts and variables diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index 48aaebc3456..18b3fe10bec 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -348,17 +348,17 @@ For example, these three jobs will be in a group named `build ruby`: build ruby 1/3: stage: build script: - - echo "ruby1" + - echo "ruby1" build ruby 2/3: stage: build script: - - echo "ruby2" + - echo "ruby2" build ruby 3/3: stage: build script: - - echo "ruby3" + - echo "ruby3" ``` In the pipeline, the result is a group named `build ruby` with three jobs: diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md index 24eab4f5c61..b56c3ce7ded 100644 --- a/doc/ci/pipelines/job_artifacts.md +++ b/doc/ci/pipelines/job_artifacts.md @@ -33,7 +33,7 @@ pdf: script: xelatex mycv.tex artifacts: paths: - - mycv.pdf + - mycv.pdf expire_in: 1 week ``` @@ -87,8 +87,8 @@ Below is an example of collecting a JUnit XML file from Ruby's RSpec test tool: rspec: stage: test script: - - bundle install - - rspec --format RspecJunitFormatter --out rspec.xml + - bundle install + - rspec --format RspecJunitFormatter --out rspec.xml artifacts: reports: junit: rspec.xml diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index c6ac87ef888..47f11a6228c 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -62,9 +62,9 @@ and it creates a dependent pipeline relation visible on the build_docs: stage: deploy script: - - curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline + - curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline only: - - tags + - tags ``` Pipelines triggered that way also expose a special variable: @@ -86,11 +86,11 @@ build_submodule: image: debian stage: test script: - - apt update && apt install -y unzip - - curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test&job_token=$CI_JOB_TOKEN" - - unzip artifacts.zip + - apt update && apt install -y unzip + - curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test&job_token=$CI_JOB_TOKEN" + - unzip artifacts.zip only: - - tags + - tags ``` This allows you to use that for multi-project pipelines and download artifacts @@ -179,9 +179,9 @@ need to add in project A's `.gitlab-ci.yml`: build_docs: stage: deploy script: - - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline" + - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline" only: - - tags + - tags ``` This means that whenever a new tag is pushed on project A, the job will run and the @@ -235,24 +235,24 @@ variable is non-zero, `make upload` is run. ```yaml stages: -- test -- build -- package + - test + - build + - package run_tests: stage: test script: - - make test + - make test build_package: stage: build script: - - make build + - make build upload_package: stage: package script: - - if [ -n "${UPLOAD_TO_S3}" ]; then make upload; fi + - if [ -n "${UPLOAD_TO_S3}" ]; then make upload; fi ``` You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 72d10dfbf2b..b6dfcfa6aa5 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -2914,10 +2914,10 @@ For example, to match a single file: ```yaml test: - script: [ 'echo 1' ] + script: [ "echo 'test' > file.txt" ] artifacts: expose_as: 'artifact 1' - paths: ['path/to/file.txt'] + paths: ['file.txt'] ``` With this configuration, GitLab will add a link **artifact 1** to the relevant merge request @@ -2927,10 +2927,10 @@ An example that will match an entire directory: ```yaml test: - script: [ 'echo 1' ] + script: [ "mkdir test && echo 'test' > test/file.txt" ] artifacts: expose_as: 'artifact 1' - paths: ['path/to/directory/'] + paths: ['test/'] ``` Note the following: @@ -3744,9 +3744,9 @@ Combining the individual examples given above for `release`, we'd have the follo ```yaml stages: -- build -- test -- release-stg + - build + - test + - release-stg release_job: stage: release @@ -4332,11 +4332,11 @@ Example: ```yaml .something_before: &something_before -- echo 'something before' + - echo 'something before' .something_after: &something_after -- echo 'something after' -- echo 'another thing after' + - echo 'something after' + - echo 'another thing after' job_name: before_script: @@ -4358,7 +4358,7 @@ For example: ```yaml .something: &something -- echo 'something' + - echo 'something' job_name: script: diff --git a/doc/development/documentation/site_architecture/index.md b/doc/development/documentation/site_architecture/index.md index 942b202a3ec..027c383bc6c 100644 --- a/doc/development/documentation/site_architecture/index.md +++ b/doc/development/documentation/site_architecture/index.md @@ -152,9 +152,9 @@ Suppose we have the `content/_data/versions.yaml` file with the content: ```yaml versions: -- 10.6 -- 10.5 -- 10.4 + - 10.6 + - 10.5 + - 10.4 ``` We can then loop over the `versions` array with something like: diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index e2cbcd6cc22..ac544113cbd 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -900,27 +900,79 @@ export default { </template> ``` -#### For JS code that is EE only, like props, computed properties, methods, etc, we will keep the current approach +#### For JS code that is EE only, like props, computed properties, methods, etc -- Since we [can't async load a mixin](https://github.com/vuejs/vue-loader/issues/418#issuecomment-254032223) we will use the [`ee_else_ce`](../development/ee_features.md#javascript-code-in-assetsjavascripts) alias we already have for webpack. - - This means all the EE specific props, computed properties, methods, etc that are EE only should be in a mixin in the `ee/` folder and we need to create a CE counterpart of the mixin +- Please do not use mixins unless ABSOLUTELY NECESSARY. Please try to find an alternative pattern. -##### Example +##### Reccomended alternative approach (named/scoped slots) -```javascript -import mixin from 'ee_else_ce/path/mixin'; +- We can use slots and/or scoped slots to achieve the same thing as we did with mixins. If you only need an EE component there is no need to create the CE component. + +1. First, we have a CE component that can render a slot incase we need EE template and functionality to be decorated on top of the CE base. + +```vue +// ./ce/my_component.vue + +<script> +export default { + props: { + tooltipDefaultText: { + type: String, + }, + }, + computed: { + tooltipText() { + return this.tooltipDefaultText || "5 issues please"; + } + }, +} +</script> + +<template> + <span v-gl-tooltip :title="tooltipText" class="ce-text">Community Edition Only Text</span> + <slot name="ee-specific-component"> +</template> +``` + +1. Next, we render the EE component, and inside of the EE component we render the CE component and add additional content in the slot. -{ - mixins: [mixin] +```vue +// ./ee/my_component.vue + +<script> +export default { + computed: { + tooltipText() { + if (this.weight) { + return "5 issues with weight 10"; + } + } + }, + methods: { + submit() { + // do something. + } + }, } +</script> + +<template> + <my-component :tooltipDefaultText="tooltipText"> + <template #ee-specific-component> + <span class="some-ee-specific">EE Specific Value</span> + <button @click="submit">Click Me</button> + </template> + </my-component> +</template> ``` -- Computed Properties/methods and getters only used in the child import still need a counterpart in CE +1. Finally, wherever the component is needed we can require it like so + +`import MyComponent from 'ee_else_ce/path/my_component'.vue` -- For store modules, we will need a CE counterpart too. -- You can see an MR with an example [here](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9762) +- this way the correct component will be included for either the ce or ee implementation -#### `template` tag +**For EE components that need different results for the same computed values, we can pass in props to the CE wrapper as seen in the example.** - **EE Child components** - Since we are using the async loading to check which component to load, we'd still use the component's name, check [this example](#child-component-only-used-in-ee). diff --git a/doc/development/prometheus_metrics.md b/doc/development/prometheus_metrics.md index 004b1884bf0..024da5cc943 100644 --- a/doc/development/prometheus_metrics.md +++ b/doc/development/prometheus_metrics.md @@ -11,16 +11,16 @@ The requirement for adding a new metric is to make each query to have an unique ```yaml - group: Response metrics (NGINX Ingress) metrics: - - title: "Throughput" - y_axis: - name: "Requests / Sec" - format: "number" - precision: 2 - queries: - - id: response_metrics_nginx_ingress_throughput_status_code - query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)' - unit: req / sec - label: Status Code + - title: "Throughput" + y_axis: + name: "Requests / Sec" + format: "number" + precision: 2 + queries: + - id: response_metrics_nginx_ingress_throughput_status_code + query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)' + unit: req / sec + label: Status Code ``` ### Update existing metrics diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index d82f0b8de4a..a8e56feb34f 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -245,7 +245,7 @@ This will delete your existing indexes. If the database size is less than 500 MiB, and the size of all hosted repos is less than 5 GiB: -1. [Enable **Elasticsearch indexing** and configure your host and port](#enabling-elasticsearch). +1. [Configure your Elasticsearch host and port](#enabling-elasticsearch). 1. Index your data: ```shell @@ -424,7 +424,7 @@ The following are some available Rake tasks: | Task | Description | |:--------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Wrapper task for `gitlab:elastic:create_empty_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_projects`, and `gitlab:elastic:index_snippets`. | +| [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Enables Elasticsearch Indexing and run `gitlab:elastic:create_empty_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_projects`, and `gitlab:elastic:index_snippets`. | | [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects and queues Sidekiq jobs to index them in the background. | | [`sudo gitlab-rake gitlab:elastic:index_projects_status`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100. | | [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. | diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md index 679edbdfe40..57851565e23 100644 --- a/doc/topics/autodevops/customize.md +++ b/doc/topics/autodevops/customize.md @@ -451,7 +451,7 @@ QA testing: environment: name: qa script: - - deploy foo + - deploy foo ``` The track `foo` being referenced must also be defined in the application's Helm chart, like: diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md index 0c7c4919431..3058afe9b50 100644 --- a/doc/topics/autodevops/stages.md +++ b/doc/topics/autodevops/stages.md @@ -469,16 +469,16 @@ workers: sidekiq: replicaCount: 1 command: - - /bin/herokuish - - procfile - - exec - - sidekiq + - /bin/herokuish + - procfile + - exec + - sidekiq preStopCommand: - - /bin/herokuish - - procfile - - exec - - sidekiqctl - - quiet + - /bin/herokuish + - procfile + - exec + - sidekiqctl + - quiet terminationGracePeriodSeconds: 60 ``` @@ -524,12 +524,12 @@ networkPolicy: matchLabels: app.gitlab.com/env: staging ingress: - - from: - - podSelector: - matchLabels: {} - - namespaceSelector: - matchLabels: - app.gitlab.com/managed_by: gitlab + - from: + - podSelector: + matchLabels: {} + - namespaceSelector: + matchLabels: + app.gitlab.com/managed_by: gitlab ``` For more information on installing Network Policies, see diff --git a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png Binary files differindex d935af96212..44fa8dc0a58 100644 --- a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png +++ b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png diff --git a/doc/user/clusters/crossplane.md b/doc/user/clusters/crossplane.md index 3a430ad55bd..e3c71f9f313 100644 --- a/doc/user/clusters/crossplane.md +++ b/doc/user/clusters/crossplane.md @@ -55,18 +55,18 @@ export REGION=us-central1 # the GCP region where the GKE cluster is provisioned. labels: rbac.authorization.k8s.io/aggregate-to-edit: "true" rules: - - apiGroups: - - database.crossplane.io - resources: - - postgresqlinstances - verbs: - - get - - list - - create - - update - - delete - - patch - - watch + - apiGroups: + - database.crossplane.io + resources: + - postgresqlinstances + verbs: + - get + - list + - create + - update + - delete + - patch + - watch ``` 1. Apply the cluster role to the cluster: diff --git a/doc/user/clusters/management_project.md b/doc/user/clusters/management_project.md index c8755af29a3..892d2bce184 100644 --- a/doc/user/clusters/management_project.md +++ b/doc/user/clusters/management_project.md @@ -97,7 +97,7 @@ Development, Staging, and Production cluster respectively. ```yaml stages: -- deploy + - deploy configure development cluster: stage: deploy diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index a59b9cf80e5..ae16e176bab 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -337,9 +337,9 @@ Windows Shared Runners: ```yaml .shared_windows_runners: tags: - - shared-windows - - windows - - windows-1809 + - shared-windows + - windows + - windows-1809 stages: - build @@ -352,17 +352,17 @@ before_script: build: extends: - - .shared_windows_runners + - .shared_windows_runners stage: build script: - - echo "running scripts in the build job" + - echo "running scripts in the build job" test: extends: - - .shared_windows_runners + - .shared_windows_runners stage: test script: - - echo "running scripts in the test job" + - echo "running scripts in the test job" ``` #### Limitations and known issues diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index 5cdac7ae892..8dcc08bce46 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -127,8 +127,8 @@ And the following environments are set in [`.gitlab-ci.yml`](../../../ci/yaml/RE ```yaml stages: -- test -- deploy + - test + - deploy test: stage: test diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md index fbd2814ea75..d2de512c62b 100644 --- a/doc/user/project/clusters/add_remove_clusters.md +++ b/doc/user/project/clusters/add_remove_clusters.md @@ -227,9 +227,9 @@ To add a Kubernetes cluster to your project, group, or instance: kind: ClusterRole name: cluster-admin subjects: - - kind: ServiceAccount - name: gitlab-admin - namespace: kube-system + - kind: ServiceAccount + name: gitlab-admin + namespace: kube-system ``` 1. Apply the service account and cluster role binding to your cluster: diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 2c532ac24c8..16d78751f40 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -99,8 +99,8 @@ And the following environments are set in ```yaml stages: -- test -- deploy + - test + - deploy test: stage: test diff --git a/doc/user/project/clusters/serverless/aws.md b/doc/user/project/clusters/serverless/aws.md index 15f7e14fda9..595d8fb3895 100644 --- a/doc/user/project/clusters/serverless/aws.md +++ b/doc/user/project/clusters/serverless/aws.md @@ -392,29 +392,19 @@ want to store your package: image: python:latest stages: - - deploy production: - stage: deploy - before_script: - - pip3 install awscli --upgrade - - pip3 install aws-sam-cli --upgrade - script: - - sam build - - sam package --output-template-file packaged.yaml --s3-bucket <S3_bucket_name> - - sam deploy --template-file packaged.yaml --stack-name gitlabpoc --s3-bucket <S3_bucket_name> --capabilities CAPABILITY_IAM --region us-east-1 - environment: production - ``` +``` Let’s examine the configuration file more closely: diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index d00f05fca66..71d03653a32 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -143,24 +143,24 @@ You must do the following: labels: rbac.authorization.k8s.io/aggregate-to-edit: "true" rules: - - apiGroups: - - serving.knative.dev - resources: - - configurations - - configurationgenerations - - routes - - revisions - - revisionuids - - autoscalers - - services - verbs: - - get - - list - - create - - update - - delete - - patch - - watch + - apiGroups: + - serving.knative.dev + resources: + - configurations + - configurationgenerations + - routes + - revisions + - revisionuids + - autoscalers + - services + verbs: + - get + - list + - create + - update + - delete + - patch + - watch ``` Then run the following command: diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 337f9461e9a..1ebba7b2871 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -290,17 +290,17 @@ For example: panel_groups: - group: 'Group Title' panels: - - type: area-chart - title: "Chart Title" - y_label: "Y-Axis" - y_axis: - format: number - precision: 0 - metrics: - - id: my_metric_id - query_range: 'http_requests_total' - label: "Instance: {{instance}}, method: {{method}}" - unit: "count" + - type: area-chart + title: "Chart Title" + y_label: "Y-Axis" + y_axis: + format: number + precision: 0 + metrics: + - id: my_metric_id + query_range: 'http_requests_total' + label: "Instance: {{instance}}, method: {{method}}" + unit: "count" ``` The above sample dashboard would display a single area chart. Each file should @@ -641,25 +641,24 @@ To add a stacked column panel type to a dashboard, look at the following sample dashboard: 'Dashboard title' priority: 1 panel_groups: -- group: 'Group Title' - priority: 5 - panels: - - type: 'stacked-column' - title: "Stacked column" - y_label: "y label" - x_label: 'x label' - metrics: - - id: memory_1 - query_range: 'memory_query' - label: "memory query 1" - unit: "count" - series_name: 'group 1' - - id: memory_2 - query_range: 'memory_query_2' - label: "memory query 2" - unit: "count" - series_name: 'group 2' - + - group: 'Group Title' + priority: 5 + panels: + - type: 'stacked-column' + title: "Stacked column" + y_label: "y label" + x_label: 'x label' + metrics: + - id: memory_1 + query_range: 'memory_query' + label: "memory query 1" + unit: "count" + series_name: 'group 1' + - id: memory_2 + query_range: 'memory_query_2' + label: "memory query 2" + unit: "count" + series_name: 'group 2' ``` ![stacked column panel type](img/prometheus_dashboard_stacked_column_panel_type_v12_8.png) @@ -681,10 +680,10 @@ panel_groups: - title: "Single Stat" type: "single-stat" metrics: - - id: 10 - query: 'max(go_memstats_alloc_bytes{job="prometheus"})' - unit: MB - label: "Total" + - id: 10 + query: 'max(go_memstats_alloc_bytes{job="prometheus"})' + unit: MB + label: "Total" ``` Note the following properties: @@ -711,10 +710,10 @@ panel_groups: type: "single-stat" max_value: 100 metrics: - - id: 10 - query: 'max(go_memstats_alloc_bytes{job="prometheus"})' - unit: '%' - label: "Total" + - id: 10 + query: 'max(go_memstats_alloc_bytes{job="prometheus"})' + unit: '%' + label: "Total" ``` For example, if you have a query value of `53.6`, adding `%` as the unit results in a single stat value of `53.6%`, but if the maximum expected value of the query is `120`, the value would be `44.6%`. Adding the `max_value` causes the correct percentage value to display. @@ -733,10 +732,10 @@ panel_groups: - title: "Heatmap" type: "heatmap" metrics: - - id: 10 - query: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)' - unit: req/sec - label: "Status code" + - id: 10 + query: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)' + unit: req/sec + label: "Status code" ``` Note the following properties: @@ -846,11 +845,11 @@ templating: type: custom options: values: - - value: 'value option 1' # The value that will replace the variable in queries. - text: 'Option 1' # (Optional) Text that will appear in the UI dropdown. - - value: 'value_option_2' - text: 'Option 2' - default: true # (Optional) This option should be the default value of this variable. + - value: 'value option 1' # The value that will replace the variable in queries. + text: 'Option 1' # (Optional) Text that will appear in the UI dropdown. + - value: 'value_option_2' + text: 'Option 2' + default: true # (Optional) This option should be the default value of this variable. ``` ##### `metric_label_values` variable type @@ -1030,10 +1029,10 @@ To send GitLab alert notifications, copy the *URL* and *Authorization Key* into receivers: name: gitlab webhook_configs: - - http_config: - bearer_token: 9e1cbfcd546896a9ea8be557caf13a76 - send_resolved: true - url: http://192.168.178.31:3001/root/manual_prometheus/prometheus/alerts/notify.json + - http_config: + bearer_token: 9e1cbfcd546896a9ea8be557caf13a76 + send_resolved: true + url: http://192.168.178.31:3001/root/manual_prometheus/prometheus/alerts/notify.json ... ``` diff --git a/doc/user/project/operations/alert_management.md b/doc/user/project/operations/alert_management.md index 7b6e40c7179..75ca5a7e74b 100644 --- a/doc/user/project/operations/alert_management.md +++ b/doc/user/project/operations/alert_management.md @@ -85,6 +85,7 @@ Each alert contains the following metrics: - **Start time** - How long ago the alert fired. This field uses the standard GitLab pattern of `X time ago`, but is supported by a granular date/time tooltip depending on the user's locale. - **Alert description** - The description of the alert, which attempts to capture the most meaningful data. - **Event count** - The number of times that an alert has fired. +- **Issue** - A link to the incident issue that has been created for the alert. - **Status** - The [current status](#alert-management-statuses) of the alert. ### Alert Management list sorting diff --git a/doc/user/project/operations/img/alert_list_v13_1.png b/doc/user/project/operations/img/alert_list_v13_1.png Binary files differindex 7cda00a25ad..7a1a5f5191e 100644 --- a/doc/user/project/operations/img/alert_list_v13_1.png +++ b/doc/user/project/operations/img/alert_list_v13_1.png diff --git a/doc/user/project/pages/getting_started/pages_from_scratch.md b/doc/user/project/pages/getting_started/pages_from_scratch.md index 23d22e9fa66..86523ab9d10 100644 --- a/doc/user/project/pages/getting_started/pages_from_scratch.md +++ b/doc/user/project/pages/getting_started/pages_from_scratch.md @@ -101,9 +101,9 @@ with GitLab Pages: ```yaml pages: script: - - gem install bundler - - bundle install - - bundle exec jekyll build + - gem install bundler + - bundle install + - bundle exec jekyll build ``` ## Specify the `public` directory for output @@ -116,9 +116,9 @@ Jekyll uses destination (`-d`) to specify an output directory for the built webs ```yaml pages: script: - - gem install bundler - - bundle install - - bundle exec jekyll build -d public + - gem install bundler + - bundle install + - bundle exec jekyll build -d public ``` ## Specify the `public` directory for artifacts @@ -130,12 +130,12 @@ in the `public` directory: ```yaml pages: script: - - gem install bundler - - bundle install - - bundle exec jekyll build -d public + - gem install bundler + - bundle install + - bundle exec jekyll build -d public artifacts: paths: - - public + - public ``` Paste this into `.gitlab-ci.yml` file, so it now looks like this: @@ -145,12 +145,12 @@ image: ruby:2.7 pages: script: - - gem install bundler - - bundle install - - bundle exec jekyll build -d public + - gem install bundler + - bundle install + - bundle exec jekyll build -d public artifacts: paths: - - public + - public ``` Now save and commit the `.gitlab-ci.yml` file. You can watch the pipeline run @@ -181,12 +181,12 @@ workflow: pages: script: - - gem install bundler - - bundle install - - bundle exec jekyll build -d public + - gem install bundler + - bundle install + - bundle exec jekyll build -d public artifacts: paths: - - public + - public ``` Then configure the pipeline to run the job for the master branch only. @@ -200,12 +200,12 @@ workflow: pages: script: - - gem install bundler - - bundle install - - bundle exec jekyll build -d public + - gem install bundler + - bundle install + - bundle exec jekyll build -d public artifacts: paths: - - public + - public rules: - if: '$CI_COMMIT_BRANCH == "master"' ``` @@ -232,12 +232,12 @@ workflow: pages: stage: deploy script: - - gem install bundler - - bundle install - - bundle exec jekyll build -d public + - gem install bundler + - bundle install + - bundle exec jekyll build -d public artifacts: paths: - - public + - public rules: - if: '$CI_COMMIT_BRANCH == "master"' ``` @@ -255,24 +255,24 @@ workflow: pages: stage: deploy script: - - gem install bundler - - bundle install - - bundle exec jekyll build -d public + - gem install bundler + - bundle install + - bundle exec jekyll build -d public artifacts: paths: - - public + - public rules: - if: '$CI_COMMIT_BRANCH == "master"' test: stage: test script: - - gem install bundler - - bundle install - - bundle exec jekyll build -d test + - gem install bundler + - bundle install + - bundle exec jekyll build -d test artifacts: paths: - - test + - test rules: - if: '$CI_COMMIT_BRANCH != "master"' ``` @@ -310,20 +310,20 @@ before_script: pages: stage: deploy script: - - bundle exec jekyll build -d public + - bundle exec jekyll build -d public artifacts: paths: - - public + - public rules: - if: '$CI_COMMIT_BRANCH == "master"' test: stage: test script: - - bundle exec jekyll build -d test + - bundle exec jekyll build -d test artifacts: paths: - - test + - test rules: - if: '$CI_COMMIT_BRANCH != "master"' ``` @@ -345,7 +345,7 @@ workflow: cache: paths: - - vendor/ + - vendor/ before_script: - gem install bundler @@ -354,20 +354,20 @@ before_script: pages: stage: deploy script: - - bundle exec jekyll build -d public + - bundle exec jekyll build -d public artifacts: paths: - - public + - public rules: - if: '$CI_COMMIT_BRANCH == "master"' test: stage: test script: - - bundle exec jekyll build -d test + - bundle exec jekyll build -d test artifacts: paths: - - test + - test rules: - if: '$CI_COMMIT_BRANCH != "master"' ``` diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index 6177a81dbea..a6923779f24 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -118,14 +118,14 @@ is so `cp` doesn't also copy `public/` to itself in an infinite loop: ```yaml pages: script: - - mkdir .public - - cp -r * .public - - mv .public public + - mkdir .public + - cp -r * .public + - mv .public public artifacts: paths: - - public + - public only: - - master + - master ``` ### `.gitlab-ci.yml` for a static site generator @@ -161,13 +161,13 @@ image: ruby:2.6 pages: script: - - gem install jekyll - - jekyll build -d public/ + - gem install jekyll + - jekyll build -d public/ artifacts: paths: - - public + - public only: - - pages + - pages ``` See an example that has different files in the [`master` branch](https://gitlab.com/pages/jekyll-branched/tree/master) diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md index 53eda76aa37..ae22dbc7e72 100644 --- a/doc/user/project/requirements/index.md +++ b/doc/user/project/requirements/index.md @@ -162,9 +162,9 @@ requirements, add a rule which checks `CI_HAS_OPEN_REQUIREMENTS` CI variable. ```yaml requirements_confirmation: rules: - - if: "$CI_HAS_OPEN_REQUIREMENTS" == "true" - when: manual - - when: never + - if: "$CI_HAS_OPEN_REQUIREMENTS" == "true" + when: manual + - when: never allow_failure: false script: - mkdir tmp diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb index 939f29a04b3..4d5350498a7 100644 --- a/lib/api/helpers/merge_requests_helpers.rb +++ b/lib/api/helpers/merge_requests_helpers.rb @@ -5,7 +5,30 @@ module API module MergeRequestsHelpers extend Grape::API::Helpers + params :merge_requests_negatable_params do + optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID' + optional :author_username, type: String, desc: 'Return merge requests which are authored by the user with the given username' + mutually_exclusive :author_id, :author_username + + optional :assignee_id, + types: [Integer, String], + integer_none_any: true, + desc: 'Return merge requests which are assigned to the user with the given ID' + optional :assignee_username, type: Array[String], check_assignees_count: true, + coerce_with: Validations::Validators::CheckAssigneesCount.coerce, + desc: 'Return merge requests which are assigned to the user with the given username' + mutually_exclusive :assignee_id, :assignee_username + + optional :labels, + type: Array[String], + coerce_with: Validations::Types::CommaSeparatedToArray.coerce, + desc: 'Comma-separated list of label names' + optional :milestone, type: String, desc: 'Return merge requests for a specific milestone' + optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' + end + params :merge_requests_base_params do + use :merge_requests_negatable_params optional :state, type: String, values: %w[opened closed locked merged all], @@ -21,11 +44,6 @@ module API values: %w[asc desc], default: 'desc', desc: 'Return merge requests sorted in `asc` or `desc` order.' - optional :milestone, type: String, desc: 'Return merge requests for a specific milestone' - optional :labels, - type: Array[String], - coerce_with: Validations::Types::CommaSeparatedToArray.coerce, - desc: 'Comma-separated list of label names' optional :with_labels_details, type: Boolean, desc: 'Return titles of labels and other details', default: false optional :with_merge_status_recheck, type: Boolean, desc: 'Request that stale merge statuses be rechecked asynchronously', default: false optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time' @@ -37,19 +55,10 @@ module API values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request' - optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID' - optional :author_username, type: String, desc: 'Return merge requests which are authored by the user with the given username' - mutually_exclusive :author_id, :author_username - - optional :assignee_id, - types: [Integer, String], - integer_none_any: true, - desc: 'Return merge requests which are assigned to the user with the given ID' optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`' - optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' optional :source_branch, type: String, desc: 'Return merge requests with the given source branch' optional :source_project_id, type: Integer, desc: 'Return merge requests with the given source project id' optional :target_branch, type: String, desc: 'Return merge requests with the given target branch' @@ -58,6 +67,9 @@ module API desc: 'Search merge requests for text present in the title, description, or any combination of these' optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma' optional :wip, type: String, values: %w[yes no], desc: 'Search merge requests for WIP in the title' + optional :not, type: Hash, desc: 'Parameters to negate' do + use :merge_requests_negatable_params + end end params :optional_scope_param do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 82f103ff0ae..49493b36d10 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -44,7 +44,9 @@ module API def find_merge_requests(args = {}) args = declared_params.merge(args) args[:milestone_title] = args.delete(:milestone) + args[:not][:milestone_title] = args[:not]&.delete(:milestone) args[:label_name] = args.delete(:labels) + args[:not][:label_name] = args[:not]&.delete(:labels) args[:scope] = args[:scope].underscore if args[:scope] merge_requests = MergeRequestsFinder.new(current_user, args).execute diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index 8b116c1c30d..f36f199ab77 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -90,6 +90,10 @@ module Gitlab metrics.pipeline_size_histogram .observe({ source: pipeline.source.to_s }, pipeline.total_size) end + + def dangling_build? + %i[ondemand_dast_scan webide].include?(source) + end end end end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/parameter.rb b/lib/gitlab/ci/pipeline/chain/config/content/parameter.rb index b93479b4142..3dd216b33d1 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content/parameter.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content/parameter.rb @@ -7,9 +7,12 @@ module Gitlab module Config class Content class Parameter < Source + UnsupportedSourceError = Class.new(StandardError) + def content strong_memoize(:content) do next unless command.content.present? + raise UnsupportedSourceError, "#{command.source} not a dangling build" unless command.dangling_build? command.content end diff --git a/lib/gitlab/danger/sidekiq_queues.rb b/lib/gitlab/danger/sidekiq_queues.rb index 5bf6057d5ba..726b6134abf 100644 --- a/lib/gitlab/danger/sidekiq_queues.rb +++ b/lib/gitlab/danger/sidekiq_queues.rb @@ -14,7 +14,7 @@ module Gitlab def changed_queue_names @changed_queue_names ||= (new_queues.values_at(*old_queues.keys) - old_queues.values) - .map { |queue| queue[:name] } + .compact.map { |queue| queue[:name] } end private diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3c5670d61c7..d8bbaa33b40 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -177,6 +177,11 @@ msgid_plural "%d issues" msgstr[0] "" msgstr[1] "" +msgid "%d issue in this group" +msgid_plural "%d issues in this group" +msgstr[0] "" +msgstr[1] "" + msgid "%d issue selected" msgid_plural "%d issues selected" msgstr[0] "" @@ -389,13 +394,10 @@ msgstr "" msgid "%{issuableType} will be removed! Are you sure?" msgstr "" -msgid "%{issuesCount} issues in this group" -msgstr "" - -msgid "%{issuesSize} issues" +msgid "%{issuesSize} issues with a limit of %{maxIssueCount}" msgstr "" -msgid "%{issuesSize} issues with a limit of %{maxIssueCount}" +msgid "%{issuesSize} with a limit of %{maxIssueCount}" msgstr "" msgid "%{labelStart}Class:%{labelEnd} %{class}" @@ -1966,6 +1968,9 @@ msgstr "" msgid "AlertManagement|Info" msgstr "" +msgid "AlertManagement|Issue" +msgstr "" + msgid "AlertManagement|Low" msgstr "" @@ -4762,6 +4767,9 @@ msgstr "" msgid "Closed" msgstr "" +msgid "Closed %{epicTimeagoDate}" +msgstr "" + msgid "Closed issues" msgstr "" @@ -11015,6 +11023,9 @@ msgstr "" msgid "Go to environments" msgstr "" +msgid "Go to epic" +msgstr "" + msgid "Go to file" msgstr "" @@ -12707,6 +12718,9 @@ msgstr "" msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities" msgstr "" +msgid "Issues with no epics assigned" +msgstr "" + msgid "Issues, merge requests, pushes, and comments." msgstr "" @@ -15832,6 +15846,9 @@ msgstr "" msgid "Opened" msgstr "" +msgid "Opened %{epicTimeagoDate}" +msgstr "" + msgid "Opened MRs" msgstr "" @@ -20247,7 +20264,7 @@ msgstr "" msgid "SecurityReports|Scan details" msgstr "" -msgid "SecurityReports|Scanner type" +msgid "SecurityReports|Scanner" msgstr "" msgid "SecurityReports|Security Dashboard" @@ -25609,10 +25626,10 @@ msgstr "" msgid "Vulnerability|Project" msgstr "" -msgid "Vulnerability|Scanner Provider" +msgid "Vulnerability|Scanner" msgstr "" -msgid "Vulnerability|Scanner Type" +msgid "Vulnerability|Scanner Provider" msgstr "" msgid "Vulnerability|Severity" @@ -26843,7 +26860,7 @@ msgstr "" msgid "ciReport|All projects" msgstr "" -msgid "ciReport|All scanner types" +msgid "ciReport|All scanners" msgstr "" msgid "ciReport|All severities" @@ -320,6 +320,7 @@ module QA module Milestone autoload :New, 'qa/page/project/milestone/new' autoload :Index, 'qa/page/project/milestone/index' + autoload :Show, 'qa/page/project/milestone/show' end module Operations diff --git a/qa/qa/page/project/milestone/index.rb b/qa/qa/page/project/milestone/index.rb index 6895c44f72f..20f73fdd545 100644 --- a/qa/qa/page/project/milestone/index.rb +++ b/qa/qa/page/project/milestone/index.rb @@ -6,11 +6,23 @@ module QA module Milestone class Index < Page::Base view 'app/views/projects/milestones/index.html.haml' do - element :new_project_milestone + element :new_project_milestone_link end - def click_new_milestone - click_element :new_project_milestone + view 'app/views/shared/milestones/_milestone.html.haml' do + element :milestone_link + end + + def click_new_milestone_link + click_element :new_project_milestone_link + end + + def has_milestone?(milestone) + has_element? :milestone_link, milestone_title: milestone.title + end + + def click_milestone(milestone) + click_element :milestone_link, milestone_title: milestone.title end end end diff --git a/qa/qa/page/project/milestone/new.rb b/qa/qa/page/project/milestone/new.rb index 751fb141684..9453585f199 100644 --- a/qa/qa/page/project/milestone/new.rb +++ b/qa/qa/page/project/milestone/new.rb @@ -6,21 +6,34 @@ module QA module Milestone class New < Page::Base view 'app/views/projects/milestones/_form.html.haml' do - element :milestone_create_button - element :milestone_title - element :milestone_description + element :create_milestone_button + element :milestone_description_field + element :milestone_title_field + end + + view 'app/views/shared/milestones/_form_dates.html.haml' do + element :due_date_field + element :start_date_field + end + + def click_create_milestone_button + click_element :create_milestone_button end def set_title(title) - fill_element :milestone_title, title + fill_element :milestone_title_field, title end def set_description(description) - fill_element :milestone_description, description + fill_element :milestone_description_field, description + end + + def set_due_date(due_date) + fill_element :due_date_field, due_date.to_s + "\n" end - def click_milestone_create_button - click_element :milestone_create_button + def set_start_date(start_date) + fill_element :start_date_field, start_date.to_s + "\n" end end end diff --git a/qa/qa/page/project/milestone/show.rb b/qa/qa/page/project/milestone/show.rb new file mode 100644 index 00000000000..ebcf9347113 --- /dev/null +++ b/qa/qa/page/project/milestone/show.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Milestone + class Show < ::QA::Page::Base + include Support::Dates + + view 'app/views/shared/milestones/_description.html.haml' do + element :milestone_title_content, required: true + element :milestone_description_content + end + + view 'app/views/shared/milestones/_sidebar.html.haml' do + element :due_date_content + element :start_date_content + end + + def has_due_date?(due_date) + formatted_due_date = format_date(due_date) + has_element?(:due_date_content, text: formatted_due_date) + end + + def has_start_date?(start_date) + formatted_start_date = format_date(start_date) + has_element?(:start_date_content, text: formatted_start_date) + end + end + end + end + end +end + +QA::Page::Project::Milestone::Show.prepend_if_ee('QA::EE::Page::Project::Milestone::Show') diff --git a/qa/qa/page/project/sub_menus/issues.rb b/qa/qa/page/project/sub_menus/issues.rb index c15a8ec4cc7..124faf0d346 100644 --- a/qa/qa/page/project/sub_menus/issues.rb +++ b/qa/qa/page/project/sub_menus/issues.rb @@ -50,6 +50,14 @@ module QA end end + def go_to_milestones + hover_issues do + within_submenu do + click_element(:milestones_link) + end + end + end + private def hover_issues diff --git a/qa/qa/resource/project_milestone.rb b/qa/qa/resource/project_milestone.rb index 385b9f0c96b..c9218e03e35 100644 --- a/qa/qa/resource/project_milestone.rb +++ b/qa/qa/resource/project_milestone.rb @@ -7,6 +7,7 @@ module QA attribute :id attribute :title + attribute :description attribute :project do Project.fabricate_via_api! do |resource| @@ -16,6 +17,7 @@ module QA def initialize @title = "project-milestone-#{SecureRandom.hex(4)}" + @description = "My awesome project milestone." end def api_get_path @@ -28,12 +30,28 @@ module QA def api_post_body { - title: title + title: title, + description: description }.tap do |hash| hash[:start_date] = @start_date if @start_date hash[:due_date] = @due_date if @due_date end end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:go_to_milestones) + Page::Project::Milestone::Index.perform(&:click_new_milestone_link) + + Page::Project::Milestone::New.perform do |new_milestone| + new_milestone.set_title(@title) + new_milestone.set_description(@description) + new_milestone.set_start_date(@start_date) if @start_date + new_milestone.set_due_date(@due_date) if @due_date + new_milestone.click_create_milestone_button + end + end end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/milestone/create_project_milestone_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/milestone/create_project_milestone_spec.rb new file mode 100644 index 00000000000..b62d7a83eea --- /dev/null +++ b/qa/qa/specs/features/browser_ui/2_plan/milestone/create_project_milestone_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module QA + context 'Plan' do + describe 'Project milestone' do + include Support::Dates + + let(:title) { 'Project milestone' } + let(:description) { 'This issue tests out project milestones.' } + let(:start_date) { current_date_yyyy_mm_dd } + let(:due_date) { next_month_yyyy_mm_dd } + + before do + Flow::Login.sign_in + end + + it 'creates a project milestone' do + project_milestone = Resource::ProjectMilestone.fabricate_via_browser_ui! do |milestone| + milestone.title = title + milestone.description = description + milestone.start_date = start_date + milestone.due_date = due_date + end + + Page::Project::Menu.perform(&:go_to_milestones) + Page::Project::Milestone::Index.perform do |milestone_list| + expect(milestone_list).to have_milestone(project_milestone) + + milestone_list.click_milestone(project_milestone) + end + + Page::Project::Milestone::Show.perform do |milestone| + expect(milestone).to have_element(:milestone_title_content, text: title) + expect(milestone).to have_element(:milestone_description_content, text: description) + expect(milestone).to have_start_date(start_date) + expect(milestone).to have_due_date(due_date) + end + end + end + end +end diff --git a/qa/qa/support/dates.rb b/qa/qa/support/dates.rb index 47fc721afc1..3d1f146730b 100644 --- a/qa/qa/support/dates.rb +++ b/qa/qa/support/dates.rb @@ -11,6 +11,11 @@ module QA current_date.next_month.strftime("%Y/%m/%d") end + def format_date(date) + new_date = DateTime.strptime(date, "%Y/%m/%d") + new_date.strftime("%b %-d, %Y") + end + private def current_date diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index 94d79d60aeb..0193712aeea 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -12,7 +12,16 @@ RSpec.describe 'Project navbar' do let_it_be(:project) { create(:project, :repository) } before do - stub_licensed_features(service_desk: false) + # TODO - This can be moved into 'project navbar structure' shared + # context when service desk feature gets moved to core. + # More information in: https://gitlab.com/gitlab-org/gitlab/-/issues/215364 + if Gitlab.ee? + insert_after_sub_nav_item( + _('Labels'), + within: _('Issues'), + new_sub_nav_item_name: _('Service Desk') + ) + end project.add_maintainer(user) sign_in(user) diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index f76110e3d85..e3643698012 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -53,6 +53,21 @@ RSpec.describe MergeRequestsFinder do expect(merge_requests).to be_empty end + context 'filtering by not author ID' do + let(:params) { { not: { author_id: user2.id } } } + + before do + merge_request2.update!(author: user2) + merge_request3.update!(author: user2) + end + + it 'returns merge requests not created by that user' do + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5) + end + end + it 'filters by projects' do params = { projects: [project2.id, project3.id] } @@ -258,6 +273,11 @@ RSpec.describe MergeRequestsFinder do let(:expected_issuables) { [merge_request1, merge_request2] } end + it_behaves_like 'assignee NOT ID filter' do + let(:params) { { not: { assignee_id: user.id } } } + let(:expected_issuables) { [merge_request3, merge_request4, merge_request5] } + end + it_behaves_like 'assignee username filter' do before do project2.add_developer(user3) @@ -269,6 +289,15 @@ RSpec.describe MergeRequestsFinder do let(:expected_issuables) { [merge_request3] } end + it_behaves_like 'assignee NOT username filter' do + before do + merge_request2.assignees = [user2] + end + + let(:params) { { not: { assignee_username: [user.username, user2.username] } } } + let(:expected_issuables) { [merge_request4, merge_request5] } + end + it_behaves_like 'no assignee filter' do let_it_be(:user3) { create(:user) } let(:expected_issuables) { [merge_request4, merge_request5] } @@ -294,6 +323,16 @@ RSpec.describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(merge_request2, merge_request3) end + + context 'using NOT' do + let(:params) { { not: { milestone_title: group_milestone.title } } } + + it 'returns MRs not assigned to that group milestone' do + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5) + end + end end end diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js index 284fd078ab3..e2dc7d79a85 100644 --- a/spec/frontend/alert_management/components/alert_management_list_spec.js +++ b/spec/frontend/alert_management/components/alert_management_list_spec.js @@ -48,6 +48,7 @@ describe('AlertManagementList', () => { const findSeverityColumnHeader = () => wrapper.findAll('th').at(0); const findPagination = () => wrapper.find(GlPagination); const findSearch = () => wrapper.find(GlSearchBoxByType); + const findIssueFields = () => wrapper.findAll('[data-testid="issueField"]'); const alertsCount = { open: 14, triggered: 10, @@ -278,6 +279,37 @@ describe('AlertManagementList', () => { expect(visitUrl).toHaveBeenCalledWith('/1527542/details'); }); + describe('alert issue links', () => { + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + }); + + it('shows "None" when no link exists', () => { + expect( + findIssueFields() + .at(0) + .text(), + ).toBe('None'); + }); + + it('renders a link when one exists', () => { + expect( + findIssueFields() + .at(1) + .text(), + ).toBe('#1'); + expect( + findIssueFields() + .at(1) + .attributes('href'), + ).toBe('/gitlab-org/gitlab/-/issues/1'); + }); + }); + describe('handle date fields', () => { it('should display time ago dates when values provided', () => { mountComponent({ diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json index 312d1756790..34eed7ae024 100644 --- a/spec/frontend/alert_management/mocks/alerts.json +++ b/spec/frontend/alert_management/mocks/alerts.json @@ -20,6 +20,7 @@ "endedAt": "2020-04-17T23:18:14.996Z", "status": "ACKNOWLEDGED", "assignees": { "nodes": [{ "username": "root" }] }, + "issueIid": "1", "notes": { "nodes": [ { diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index f894b2af5cf..cbcd8305c10 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -26,10 +26,9 @@ import { setMetricResult, setupStoreWithData, setupStoreWithDataForPanelCount, - setupStoreWithVariable, setupStoreWithLinks, } from '../store_utils'; -import { environmentData, dashboardGitResponse } from '../mock_data'; +import { environmentData, dashboardGitResponse, storeVariables } from '../mock_data'; import { metricsDashboardViewModel, metricsDashboardPanelCount, @@ -604,8 +603,7 @@ describe('Dashboard', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true }); setupStoreWithData(store); - setupStoreWithVariable(store); - + store.state.monitoringDashboard.variables = storeVariables; return wrapper.vm.$nextTick(); }); diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js index 623125d18f1..cc384aef231 100644 --- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js +++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js @@ -59,7 +59,7 @@ describe('Custom variable component', () => { .vm.$emit('click'); return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'env', 'canary'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary'); }); }); }); diff --git a/spec/frontend/monitoring/components/variables/text_field_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js index 68bfb8ec695..99c6facac38 100644 --- a/spec/frontend/monitoring/components/variables/text_field_spec.js +++ b/spec/frontend/monitoring/components/variables/text_field_spec.js @@ -40,7 +40,7 @@ describe('Text variable component', () => { findInput().trigger('keyup.enter'); return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'prod-pod'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'prod-pod'); }); }); @@ -53,7 +53,7 @@ describe('Text variable component', () => { findInput().trigger('blur'); return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'canary-pod'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary-pod'); }); }); }); diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js index 9fb93a18e0b..d8e63917eec 100644 --- a/spec/frontend/monitoring/components/variables_section_spec.js +++ b/spec/frontend/monitoring/components/variables_section_spec.js @@ -6,8 +6,7 @@ import TextField from '~/monitoring/components/variables/text_field.vue'; import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility'; import { createStore } from '~/monitoring/stores'; import { convertVariablesForURL } from '~/monitoring/utils'; -import * as types from '~/monitoring/stores/mutation_types'; -import { mockTemplatingDataResponses } from '../mock_data'; +import { storeVariables } from '../mock_data'; jest.mock('~/lib/utils/url_utility', () => ({ updateHistory: jest.fn(), @@ -17,12 +16,6 @@ jest.mock('~/lib/utils/url_utility', () => ({ describe('Metrics dashboard/variables section component', () => { let store; let wrapper; - const sampleVariables = { - label1: mockTemplatingDataResponses.simpleText.simpleText, - label2: mockTemplatingDataResponses.advText.advText, - label3: mockTemplatingDataResponses.simpleCustom.simpleCustom, - label4: mockTemplatingDataResponses.metricLabelValues.simple, - }; const createShallowWrapper = () => { wrapper = shallowMount(VariablesSection, { @@ -48,22 +41,23 @@ describe('Metrics dashboard/variables section component', () => { describe('when variables are set', () => { beforeEach(() => { + store.state.monitoringDashboard.variables = storeVariables; createShallowWrapper(); - store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables); + return wrapper.vm.$nextTick; }); it('shows the variables section', () => { const allInputs = findTextInputs().length + findCustomInputs().length; - expect(allInputs).toBe(Object.keys(sampleVariables).length); + expect(allInputs).toBe(storeVariables.length); }); it('shows the right custom variable inputs', () => { const customInputs = findCustomInputs(); - expect(customInputs.at(0).props('name')).toBe('label3'); - expect(customInputs.at(1).props('name')).toBe('label4'); + expect(customInputs.at(0).props('name')).toBe('customSimple'); + expect(customInputs.at(1).props('name')).toBe('customAdvanced'); }); }); @@ -77,7 +71,7 @@ describe('Metrics dashboard/variables section component', () => { namespaced: true, state: { showEmptyState: false, - variables: sampleVariables, + variables: storeVariables, }, actions: { updateVariablesAndFetchData, @@ -92,12 +86,12 @@ describe('Metrics dashboard/variables section component', () => { it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => { const firstInput = findTextInputs().at(0); - firstInput.vm.$emit('onUpdate', 'label1', 'test'); + firstInput.vm.$emit('input', 'test'); return wrapper.vm.$nextTick(() => { expect(updateVariablesAndFetchData).toHaveBeenCalled(); expect(mergeUrlParams).toHaveBeenCalledWith( - convertVariablesForURL(sampleVariables), + convertVariablesForURL(storeVariables), window.location.href, ); expect(updateHistory).toHaveBeenCalled(); @@ -107,12 +101,12 @@ describe('Metrics dashboard/variables section component', () => { it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => { const firstInput = findCustomInputs().at(0); - firstInput.vm.$emit('onUpdate', 'label1', 'test'); + firstInput.vm.$emit('input', 'test'); return wrapper.vm.$nextTick(() => { expect(updateVariablesAndFetchData).toHaveBeenCalled(); expect(mergeUrlParams).toHaveBeenCalledWith( - convertVariablesForURL(sampleVariables), + convertVariablesForURL(storeVariables), window.location.href, ); expect(updateHistory).toHaveBeenCalled(); @@ -122,7 +116,7 @@ describe('Metrics dashboard/variables section component', () => { it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => { const firstInput = findTextInputs().at(0); - firstInput.vm.$emit('onUpdate', 'label1', 'Simple text'); + firstInput.vm.$emit('input', 'My default value'); expect(updateVariablesAndFetchData).not.toHaveBeenCalled(); expect(mergeUrlParams).not.toHaveBeenCalled(); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index e5b25a37976..8ccae1228e8 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -627,81 +627,79 @@ export const mockLinks = [ }, ]; -const templatingVariableTypes = { +export const templatingVariablesExamples = { text: { - simple: 'Simple text', - advanced: { - label: 'Variable 4', + textSimple: 'My default value', + textAdvanced: { + label: 'Advanced text variable', type: 'text', options: { - default_value: 'default', + default_value: 'A default value', }, }, }, custom: { - simple: ['value1', 'value2', 'value3'], - advanced: { - normal: { - label: 'Advanced Var', - type: 'custom', - options: { - values: [ - { value: 'value1', text: 'Var 1 Option 1' }, - { - value: 'value2', - text: 'Var 1 Option 2', - default: true, - }, - ], - }, - }, - withoutOpts: { - type: 'custom', - options: {}, + customSimple: ['value1', 'value2', 'value3'], + customAdvanced: { + label: 'Advanced Var', + type: 'custom', + options: { + values: [ + { value: 'value1', text: 'Var 1 Option 1' }, + { + value: 'value2', + text: 'Var 1 Option 2', + default: true, + }, + ], }, - withoutLabel: { - type: 'custom', - options: { - values: [ - { value: 'value1', text: 'Var 1 Option 1' }, - { - value: 'value2', - text: 'Var 1 Option 2', - default: true, - }, - ], - }, + }, + customAdvancedWithoutOpts: { + type: 'custom', + options: {}, + }, + customAdvancedWithoutLabel: { + type: 'custom', + options: { + values: [ + { value: 'value1', text: 'Var 1 Option 1' }, + { + value: 'value2', + text: 'Var 1 Option 2', + default: true, + }, + ], }, - withoutType: { - label: 'Variable 2', - options: { - values: [ - { value: 'value1', text: 'Var 1 Option 1' }, - { - value: 'value2', - text: 'Var 1 Option 2', - default: true, - }, - ], - }, + }, + customAdvancedWithoutType: { + label: 'Variable 2', + options: { + values: [ + { value: 'value1', text: 'Var 1 Option 1' }, + { + value: 'value2', + text: 'Var 1 Option 2', + default: true, + }, + ], }, - withoutOptText: { - label: 'Options without text', - type: 'custom', - options: { - values: [ - { value: 'value1' }, - { - value: 'value2', - default: true, - }, - ], - }, + }, + customAdvancedWithoutOptText: { + label: 'Options without text', + type: 'custom', + options: { + values: [ + { value: 'value1' }, + { + value: 'value2', + default: true, + }, + ], }, }, }, metricLabelValues: { - simple: { + metricLabelValuesSimple: { label: 'Metric Label Values', type: 'metric_label_values', options: { @@ -713,205 +711,92 @@ const templatingVariableTypes = { }, }; -const generateMockTemplatingData = data => { - const vars = data - ? { - variables: { - ...data, - }, - } - : {}; - return { - dashboard: { - templating: vars, - }, - }; -}; - -const responseForSimpleTextVariable = { - simpleText: { - label: 'simpleText', +export const storeTextVariables = [ + { type: 'text', - value: 'Simple text', + name: 'textSimple', + label: 'textSimple', + value: 'My default value', }, -}; - -const responseForAdvTextVariable = { - advText: { - label: 'Variable 4', + { type: 'text', - value: 'default', + name: 'textAdvanced', + label: 'Advanced text variable', + value: 'A default value', }, -}; +]; -const responseForSimpleCustomVariable = { - simpleCustom: { - label: 'simpleCustom', - value: 'value1', +export const storeCustomVariables = [ + { + type: 'custom', + name: 'customSimple', + label: 'customSimple', options: { values: [ - { - default: false, - text: 'value1', - value: 'value1', - }, - { - default: false, - text: 'value2', - value: 'value2', - }, - { - default: false, - text: 'value3', - value: 'value3', - }, + { default: false, text: 'value1', value: 'value1' }, + { default: false, text: 'value2', value: 'value2' }, + { default: false, text: 'value3', value: 'value3' }, ], }, - type: 'custom', + value: 'value1', }, -}; - -const responseForAdvancedCustomVariableWithoutOptions = { - advCustomWithoutOpts: { - label: 'advCustomWithoutOpts', + { + type: 'custom', + name: 'customAdvanced', + label: 'Advanced Var', options: { - values: [], + values: [ + { default: false, text: 'Var 1 Option 1', value: 'value1' }, + { default: true, text: 'Var 1 Option 2', value: 'value2' }, + ], }, + value: 'value2', + }, + { type: 'custom', + name: 'customAdvancedWithoutOpts', + label: 'customAdvancedWithoutOpts', + options: { values: [] }, + value: null, }, -}; - -const responseForAdvancedCustomVariableWithoutLabel = { - advCustomWithoutLabel: { - label: 'advCustomWithoutLabel', + { + type: 'custom', + name: 'customAdvancedWithoutLabel', + label: 'customAdvancedWithoutLabel', value: 'value2', options: { values: [ - { - default: false, - text: 'Var 1 Option 1', - value: 'value1', - }, - { - default: true, - text: 'Var 1 Option 2', - value: 'value2', - }, + { default: false, text: 'Var 1 Option 1', value: 'value1' }, + { default: true, text: 'Var 1 Option 2', value: 'value2' }, ], }, - type: 'custom', }, -}; - -const responseForAdvancedCustomVariableWithoutOptText = { - advCustomWithoutOptText: { + { + type: 'custom', + name: 'customAdvancedWithoutOptText', label: 'Options without text', - value: 'value2', options: { values: [ - { - default: false, - text: 'value1', - value: 'value1', - }, - { - default: true, - text: 'value2', - value: 'value2', - }, + { default: false, text: 'value1', value: 'value1' }, + { default: true, text: 'value2', value: 'value2' }, ], }, - type: 'custom', + value: 'value2', }, -}; +]; -const responseForMetricLabelValues = { - simple: { - label: 'Metric Label Values', +export const storeMetricLabelValuesVariables = [ + { type: 'metric_label_values', + name: 'metricLabelValuesSimple', + label: 'Metric Label Values', + options: { prometheusEndpointPath: '/series', label: 'backend', values: [] }, value: null, - options: { - prometheusEndpointPath: '/series', - label: 'backend', - values: [], - }, }, -}; - -const responseForAdvancedCustomVariable = { - ...responseForSimpleCustomVariable, - advCustomNormal: { - label: 'Advanced Var', - value: 'value2', - options: { - values: [ - { - default: false, - text: 'Var 1 Option 1', - value: 'value1', - }, - { - default: true, - text: 'Var 1 Option 2', - value: 'value2', - }, - ], - }, - type: 'custom', - }, -}; - -const responsesForAllVariableTypes = { - ...responseForSimpleTextVariable, - ...responseForAdvTextVariable, - ...responseForSimpleCustomVariable, - ...responseForAdvancedCustomVariable, -}; - -export const mockTemplatingData = { - emptyTemplatingProp: generateMockTemplatingData(), - emptyVariablesProp: generateMockTemplatingData({}), - simpleText: generateMockTemplatingData({ simpleText: templatingVariableTypes.text.simple }), - advText: generateMockTemplatingData({ advText: templatingVariableTypes.text.advanced }), - simpleCustom: generateMockTemplatingData({ simpleCustom: templatingVariableTypes.custom.simple }), - advCustomWithoutOpts: generateMockTemplatingData({ - advCustomWithoutOpts: templatingVariableTypes.custom.advanced.withoutOpts, - }), - advCustomWithoutType: generateMockTemplatingData({ - advCustomWithoutType: templatingVariableTypes.custom.advanced.withoutType, - }), - advCustomWithoutLabel: generateMockTemplatingData({ - advCustomWithoutLabel: templatingVariableTypes.custom.advanced.withoutLabel, - }), - advCustomWithoutOptText: generateMockTemplatingData({ - advCustomWithoutOptText: templatingVariableTypes.custom.advanced.withoutOptText, - }), - simpleAndAdv: generateMockTemplatingData({ - simpleCustom: templatingVariableTypes.custom.simple, - advCustomNormal: templatingVariableTypes.custom.advanced.normal, - }), - metricLabelValues: generateMockTemplatingData({ - simple: templatingVariableTypes.metricLabelValues.simple, - }), - allVariableTypes: generateMockTemplatingData({ - simpleText: templatingVariableTypes.text.simple, - advText: templatingVariableTypes.text.advanced, - simpleCustom: templatingVariableTypes.custom.simple, - advCustomNormal: templatingVariableTypes.custom.advanced.normal, - }), -}; +]; -export const mockTemplatingDataResponses = { - emptyTemplatingProp: {}, - emptyVariablesProp: {}, - simpleText: responseForSimpleTextVariable, - advText: responseForAdvTextVariable, - simpleCustom: responseForSimpleCustomVariable, - advCustomWithoutOpts: responseForAdvancedCustomVariableWithoutOptions, - advCustomWithoutType: {}, - advCustomWithoutLabel: responseForAdvancedCustomVariableWithoutLabel, - advCustomWithoutOptText: responseForAdvancedCustomVariableWithoutOptText, - simpleAndAdv: responseForAdvancedCustomVariable, - allVariableTypes: responsesForAllVariableTypes, - metricLabelValues: responseForMetricLabelValues, -}; +export const storeVariables = [ + ...storeTextVariables, + ...storeCustomVariables, + ...storeMetricLabelValuesVariables, +]; diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index 0e28b70a760..ad01e4c3a9b 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -44,7 +44,6 @@ import { deploymentData, environmentData, annotationsData, - mockTemplatingData, dashboardGitResponse, mockDashboardsErrorResponse, } from '../mock_data'; @@ -305,32 +304,6 @@ describe('Monitoring store actions', () => { expect(dispatch).toHaveBeenCalledWith('fetchDashboardData'); }); - it('stores templating variables', () => { - const response = { - ...metricsDashboardResponse.dashboard, - ...mockTemplatingData.allVariableTypes.dashboard, - }; - - receiveMetricsDashboardSuccess( - { state, commit, dispatch }, - { - response: { - ...metricsDashboardResponse, - dashboard: { - ...metricsDashboardResponse.dashboard, - ...mockTemplatingData.allVariableTypes.dashboard, - }, - }, - }, - ); - - expect(commit).toHaveBeenCalledWith( - types.RECEIVE_METRICS_DASHBOARD_SUCCESS, - - response, - ); - }); - it('sets the dashboards loaded from the repository', () => { const params = {}; const response = metricsDashboardResponse; @@ -1144,11 +1117,13 @@ describe('Monitoring store actions', () => { describe('fetchVariableMetricLabelValues', () => { const variable = { type: 'metric_label_values', + name: 'label1', options: { - prometheusEndpointPath: '/series', + prometheusEndpointPath: '/series?match[]=metric_name', label: 'job', }, }; + const defaultQueryParams = { start_time: '2019-08-06T12:40:02.184Z', end_time: '2019-08-06T20:40:02.184Z', @@ -1158,9 +1133,7 @@ describe('Monitoring store actions', () => { state = { ...state, timeRange: defaultTimeRange, - variables: { - label1: variable, - }, + variables: [variable], }; }); @@ -1176,7 +1149,7 @@ describe('Monitoring store actions', () => { }, ]; - mock.onGet('/series').reply(200, { + mock.onGet('/series?match[]=metric_name').reply(200, { status: 'success', data, }); @@ -1196,7 +1169,7 @@ describe('Monitoring store actions', () => { }); it('should notify the user that dynamic options were not loaded', () => { - mock.onGet('/series').reply(500); + mock.onGet('/series?match[]=metric_name').reply(500); return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then( () => { diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js index 1275686de58..a69f5265ea7 100644 --- a/spec/frontend/monitoring/store/getters_spec.js +++ b/spec/frontend/monitoring/store/getters_spec.js @@ -8,7 +8,7 @@ import { environmentData, metricsResult, dashboardGitResponse, - mockTemplatingDataResponses, + storeVariables, mockLinks, } from '../mock_data'; import { @@ -344,19 +344,21 @@ describe('Monitoring store Getters', () => { }); it('transforms the variables object to an array in the [variable, variable_value] format for all variable types', () => { - mutations[types.SET_VARIABLES](state, mockTemplatingDataResponses.allVariableTypes); + state.variables = storeVariables; const variablesArray = getters.getCustomVariablesParams(state); expect(variablesArray).toEqual({ - 'variables[advCustomNormal]': 'value2', - 'variables[advText]': 'default', - 'variables[simpleCustom]': 'value1', - 'variables[simpleText]': 'Simple text', + 'variables[textSimple]': 'My default value', + 'variables[textAdvanced]': 'A default value', + 'variables[customSimple]': 'value1', + 'variables[customAdvanced]': 'value2', + 'variables[customAdvancedWithoutLabel]': 'value2', + 'variables[customAdvancedWithoutOptText]': 'value2', }); }); it('transforms the variables object to an empty array when no keys are present', () => { - mutations[types.SET_VARIABLES](state, {}); + state.variables = []; const variablesArray = getters.getCustomVariablesParams(state); expect(variablesArray).toEqual({}); diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index fb5e6156daf..37da5ea96d9 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -5,7 +5,7 @@ import * as types from '~/monitoring/stores/mutation_types'; import state from '~/monitoring/stores/state'; import { metricStates } from '~/monitoring/constants'; -import { deploymentData, dashboardGitResponse } from '../mock_data'; +import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data'; import { metricsDashboardPayload } from '../fixture_data'; describe('Monitoring mutations', () => { @@ -427,30 +427,12 @@ describe('Monitoring mutations', () => { }); }); - describe('SET_VARIABLES', () => { - it('stores an empty variables array when no custom variables are given', () => { - mutations[types.SET_VARIABLES](stateCopy, {}); - - expect(stateCopy.variables).toEqual({}); - }); - - it('stores variables in the key key_value format in the array', () => { - mutations[types.SET_VARIABLES](stateCopy, { pod: 'POD', stage: 'main ops' }); - - expect(stateCopy.variables).toEqual({ pod: 'POD', stage: 'main ops' }); - }); - }); - describe('UPDATE_VARIABLE_VALUE', () => { - afterEach(() => { - mutations[types.SET_VARIABLES](stateCopy, {}); - }); - it('updates only the value of the variable in variables', () => { - mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } }); - mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, { key: 'environment', value: 'new prod' }); + stateCopy.variables = storeTextVariables; + mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, { name: 'textSimple', value: 'New Value' }); - expect(stateCopy.variables).toEqual({ environment: { value: 'new prod', type: 'text' } }); + expect(stateCopy.variables[0].value).toEqual('New Value'); }); }); diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index 8ee37a529dd..b97948fa1bf 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -22,7 +22,7 @@ describe('mapToDashboardViewModel', () => { dashboard: '', panelGroups: [], links: [], - variables: {}, + variables: [], }); }); @@ -52,7 +52,7 @@ describe('mapToDashboardViewModel', () => { expect(mapToDashboardViewModel(response)).toEqual({ dashboard: 'Dashboard Name', links: [], - variables: {}, + variables: [], panelGroups: [ { group: 'Group 1', @@ -424,22 +424,20 @@ describe('mapToDashboardViewModel', () => { urlUtils.queryToObject.mockReturnValueOnce(); - expect(mapToDashboardViewModel(response)).toMatchObject({ - dashboard: 'Dashboard Name', - links: [], - variables: { - pod: { - label: 'pod', - type: 'text', - value: 'kubernetes', - }, - pod_2: { - label: 'pod_2', - type: 'text', - value: 'kubernetes-2', - }, + expect(mapToDashboardViewModel(response).variables).toEqual([ + { + name: 'pod', + label: 'pod', + type: 'text', + value: 'kubernetes', }, - }); + { + name: 'pod_2', + label: 'pod_2', + type: 'text', + value: 'kubernetes-2', + }, + ]); }); it('sets variables as-is from yml file if URL has no matching variables', () => { @@ -458,22 +456,20 @@ describe('mapToDashboardViewModel', () => { 'var-environment': 'POD', }); - expect(mapToDashboardViewModel(response)).toMatchObject({ - dashboard: 'Dashboard Name', - links: [], - variables: { - pod: { - label: 'pod', - type: 'text', - value: 'kubernetes', - }, - pod_2: { - label: 'pod_2', - type: 'text', - value: 'kubernetes-2', - }, + expect(mapToDashboardViewModel(response).variables).toEqual([ + { + label: 'pod', + name: 'pod', + type: 'text', + value: 'kubernetes', }, - }); + { + label: 'pod_2', + name: 'pod_2', + type: 'text', + value: 'kubernetes-2', + }, + ]); }); it('merges variables from URL with the ones from yml file', () => { @@ -494,22 +490,20 @@ describe('mapToDashboardViewModel', () => { 'var-pod_2': 'POD2', }); - expect(mapToDashboardViewModel(response)).toMatchObject({ - dashboard: 'Dashboard Name', - links: [], - variables: { - pod: { - label: 'pod', - type: 'text', - value: 'POD1', - }, - pod_2: { - label: 'pod_2', - type: 'text', - value: 'POD2', - }, + expect(mapToDashboardViewModel(response).variables).toEqual([ + { + label: 'pod', + name: 'pod', + type: 'text', + value: 'POD1', }, - }); + { + label: 'pod_2', + name: 'pod_2', + type: 'text', + value: 'POD2', + }, + ]); }); }); }); diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js index 390cb2d8eac..de124b0313c 100644 --- a/spec/frontend/monitoring/store/variable_mapping_spec.js +++ b/spec/frontend/monitoring/store/variable_mapping_spec.js @@ -3,29 +3,31 @@ import { mergeURLVariables, optionsFromSeriesData, } from '~/monitoring/stores/variable_mapping'; +import { + templatingVariablesExamples, + storeTextVariables, + storeCustomVariables, + storeMetricLabelValuesVariables, +} from '../mock_data'; import * as urlUtils from '~/lib/utils/url_utility'; -import { mockTemplatingData, mockTemplatingDataResponses } from '../mock_data'; describe('Monitoring variable mapping', () => { describe('parseTemplatingVariables', () => { it.each` - case | input | expected - ${'Returns empty object for no dashboard input'} | ${{}} | ${{}} - ${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}} - ${'Returns empty object for empty templating prop'} | ${mockTemplatingData.emptyTemplatingProp} | ${{}} - ${'Returns empty object for empty variables prop'} | ${mockTemplatingData.emptyVariablesProp} | ${{}} - ${'Returns parsed object for simple text variable'} | ${mockTemplatingData.simpleText} | ${mockTemplatingDataResponses.simpleText} - ${'Returns parsed object for advanced text variable'} | ${mockTemplatingData.advText} | ${mockTemplatingDataResponses.advText} - ${'Returns parsed object for simple custom variable'} | ${mockTemplatingData.simpleCustom} | ${mockTemplatingDataResponses.simpleCustom} - ${'Returns parsed object for advanced custom variable without options'} | ${mockTemplatingData.advCustomWithoutOpts} | ${mockTemplatingDataResponses.advCustomWithoutOpts} - ${'Returns parsed object for advanced custom variable for option without text'} | ${mockTemplatingData.advCustomWithoutOptText} | ${mockTemplatingDataResponses.advCustomWithoutOptText} - ${'Returns parsed object for advanced custom variable without type'} | ${mockTemplatingData.advCustomWithoutType} | ${{}} - ${'Returns parsed object for advanced custom variable without label'} | ${mockTemplatingData.advCustomWithoutLabel} | ${mockTemplatingDataResponses.advCustomWithoutLabel} - ${'Returns parsed object for simple and advanced custom variables'} | ${mockTemplatingData.simpleAndAdv} | ${mockTemplatingDataResponses.simpleAndAdv} - ${'Returns parsed object for metricLabelValues'} | ${mockTemplatingData.metricLabelValues} | ${mockTemplatingDataResponses.metricLabelValues} - ${'Returns parsed object for all variable types'} | ${mockTemplatingData.allVariableTypes} | ${mockTemplatingDataResponses.allVariableTypes} - `('$case', ({ input, expected }) => { - expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected); + case | input + ${'For undefined templating object'} | ${undefined} + ${'For empty templating object'} | ${{}} + `('$case, returns an empty array', ({ input }) => { + expect(parseTemplatingVariables(input)).toEqual([]); + }); + + it.each` + case | input | output + ${'Returns parsed object for text variables'} | ${templatingVariablesExamples.text} | ${storeTextVariables} + ${'Returns parsed object for custom variables'} | ${templatingVariablesExamples.custom} | ${storeCustomVariables} + ${'Returns parsed object for metric label value variables'} | ${templatingVariablesExamples.metricLabelValues} | ${storeMetricLabelValuesVariables} + `('$case, returns an empty array', ({ input, output }) => { + expect(parseTemplatingVariables(input)).toEqual(output); }); }); @@ -41,7 +43,7 @@ describe('Monitoring variable mapping', () => { it('returns empty object if variables are not defined in yml or URL', () => { urlUtils.queryToObject.mockReturnValueOnce({}); - expect(mergeURLVariables({})).toEqual({}); + expect(mergeURLVariables([])).toEqual([]); }); it('returns empty object if variables are defined in URL but not in yml', () => { @@ -50,18 +52,24 @@ describe('Monitoring variable mapping', () => { 'var-instance': 'localhost', }); - expect(mergeURLVariables({})).toEqual({}); + expect(mergeURLVariables([])).toEqual([]); }); it('returns yml variables if variables defined in yml but not in the URL', () => { urlUtils.queryToObject.mockReturnValueOnce({}); - const params = { - env: 'one', - instance: 'localhost', - }; + const variables = [ + { + name: 'env', + value: 'one', + }, + { + name: 'instance', + value: 'localhost', + }, + ]; - expect(mergeURLVariables(params)).toEqual(params); + expect(mergeURLVariables(variables)).toEqual(variables); }); it('returns yml variables if variables defined in URL do not match with yml variables', () => { @@ -69,13 +77,19 @@ describe('Monitoring variable mapping', () => { 'var-env': 'one', 'var-instance': 'localhost', }; - const ymlParams = { - pod: { value: 'one' }, - service: { value: 'database' }, - }; + const variables = [ + { + name: 'env', + value: 'one', + }, + { + name: 'service', + value: 'database', + }, + ]; urlUtils.queryToObject.mockReturnValueOnce(urlParams); - expect(mergeURLVariables(ymlParams)).toEqual(ymlParams); + expect(mergeURLVariables(variables)).toEqual(variables); }); it('returns merged yml and URL variables if there is some match', () => { @@ -83,19 +97,29 @@ describe('Monitoring variable mapping', () => { 'var-env': 'one', 'var-instance': 'localhost:8080', }; - const ymlParams = { - instance: { value: 'localhost' }, - service: { value: 'database' }, - }; - - const merged = { - instance: { value: 'localhost:8080' }, - service: { value: 'database' }, - }; + const variables = [ + { + name: 'instance', + value: 'localhost', + }, + { + name: 'service', + value: 'database', + }, + ]; urlUtils.queryToObject.mockReturnValueOnce(urlParams); - expect(mergeURLVariables(ymlParams)).toEqual(merged); + expect(mergeURLVariables(variables)).toEqual([ + { + name: 'instance', + value: 'localhost:8080', + }, + { + name: 'service', + value: 'database', + }, + ]); }); }); diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js index 740dbaaa2e3..6c8267e6a3c 100644 --- a/spec/frontend/monitoring/store_utils.js +++ b/spec/frontend/monitoring/store_utils.js @@ -35,12 +35,6 @@ export const setupStoreWithDashboard = store => { ); }; -export const setupStoreWithVariable = store => { - store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, { - label1: 'pod', - }); -}; - export const setupStoreWithLinks = store => { store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, { ...metricsDashboardPayload, diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index 039cf275eea..1b4df286868 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -429,14 +429,41 @@ describe('monitoring/utils', () => { describe('convertVariablesForURL', () => { it.each` - input | expected - ${undefined} | ${{}} - ${null} | ${{}} - ${{}} | ${{}} - ${{ env: { value: 'prod' } }} | ${{ 'var-env': 'prod' }} - ${{ 'var-env': { value: 'prod' } }} | ${{ 'var-var-env': 'prod' }} + input | expected + ${[]} | ${{}} + ${[{ name: 'env', value: 'prod' }]} | ${{ 'var-env': 'prod' }} + ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${{ 'var-env1': 'prod' }} + ${[{ name: 'var-env', value: 'prod' }]} | ${{ 'var-var-env': 'prod' }} `('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => { expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected); }); }); + + describe('setCustomVariablesFromUrl', () => { + beforeEach(() => { + jest.spyOn(urlUtils, 'updateHistory'); + }); + + afterEach(() => { + urlUtils.updateHistory.mockRestore(); + }); + + it.each` + input | urlParams + ${[]} | ${''} + ${[{ name: 'env', value: 'prod' }]} | ${'?var-env=prod'} + ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${'?var-env=prod&var-env1=prod'} + `( + 'setCustomVariablesFromUrl updates history with query "$urlParams" with input $input', + ({ input, urlParams }) => { + monitoringUtils.setCustomVariablesFromUrl(input); + + expect(urlUtils.updateHistory).toHaveBeenCalledTimes(1); + expect(urlUtils.updateHistory).toHaveBeenCalledWith({ + url: `http://localhost/${urlParams}`, + title: '', + }); + }, + ); + }); }); diff --git a/spec/helpers/notify_helper_spec.rb b/spec/helpers/notify_helper_spec.rb new file mode 100644 index 00000000000..5b2a06b11e9 --- /dev/null +++ b/spec/helpers/notify_helper_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe NotifyHelper do + include ActionView::Helpers::UrlHelper + + describe 'merge_request_reference_link' do + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project) } + + it 'returns link to merge request with the text reference' do + url = "http://test.host/#{project.full_path}/-/merge_requests/#{merge_request.iid}" + + expect(merge_request_reference_link(merge_request)).to eq(reference_link(merge_request, url)) + end + end + + describe 'issue_reference_link' do + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + + it 'returns link to issue with the text reference' do + url = "http://test.host/#{project.full_path}/-/issues/#{issue.iid}" + + expect(issue_reference_link(issue)).to eq(reference_link(issue, url)) + end + end + + def reference_link(entity, url) + "<a href=\"#{url}\">#{entity.to_reference}</a>" + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index f9daff91871..bc2012e83bd 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -270,4 +270,29 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do it { is_expected. to eq(true) } end end + + describe '#dangling_build?' do + let(:project) { create(:project, :repository) } + let(:command) { described_class.new(project: project, source: source) } + + subject { command.dangling_build? } + + context 'when source is :webide' do + let(:source) { :webide } + + it { is_expected.to eq(true) } + end + + context 'when source is :ondemand_dast_scan' do + let(:source) { :ondemand_dast_scan } + + it { is_expected.to eq(true) } + end + + context 'when source something else' do + let(:source) { :web } + + it { is_expected.to eq(false) } + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb index 6adf2a59b82..42ec9ab6f5d 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb @@ -6,7 +6,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do let(:project) { create(:project, ci_config_path: ci_config_path) } let(:pipeline) { build(:ci_pipeline, project: project) } let(:content) { nil } - let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project, content: content) } + let(:source) { :push } + let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project, content: content, source: source) } subject { described_class.new(pipeline, command) } @@ -143,6 +144,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do end context 'when config is passed as a parameter' do + let(:source) { :ondemand_dast_scan } let(:ci_config_path) { nil } let(:content) do <<~EOY diff --git a/spec/lib/gitlab/danger/sidekiq_queues_spec.rb b/spec/lib/gitlab/danger/sidekiq_queues_spec.rb index afae22eda20..7dd1a2e6924 100644 --- a/spec/lib/gitlab/danger/sidekiq_queues_spec.rb +++ b/spec/lib/gitlab/danger/sidekiq_queues_spec.rb @@ -62,5 +62,21 @@ RSpec.describe Gitlab::Danger::SidekiqQueues do expect(sidekiq_queues.changed_queue_names).to contain_exactly(:post_receive, :process_commit) end + + it 'ignores removed queues' do + old_queues = { + merge: { name: :merge, urgency: :low }, + post_receive: { name: :post_receive, urgency: :high } + } + + new_queues = { + post_receive: { name: :post_receive, urgency: :low } + } + + allow(sidekiq_queues).to receive(:old_queues).and_return(old_queues) + allow(sidekiq_queues).to receive(:new_queues).and_return(new_queues) + + expect(sidekiq_queues.changed_queue_names).to contain_exactly(:post_receive) + end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 9e613749bdf..7c1eb66b543 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -105,6 +105,7 @@ RSpec.describe Notify do it 'contains a link to issue author' do is_expected.to have_body_text(issue.author_name) is_expected.to have_body_text 'created an issue' + is_expected.to have_link(issue.to_reference, href: project_issue_url(issue.project, issue)) end it 'contains a link to the issue' do @@ -467,6 +468,7 @@ RSpec.describe Notify do is_expected.to have_body_text(status) is_expected.to have_body_text(current_user_sanitized) is_expected.to have_body_text(project_merge_request_path(project, merge_request)) + is_expected.to have_link(merge_request.to_reference, href: project_merge_request_url(merge_request.target_project, merge_request)) end end end @@ -497,6 +499,7 @@ RSpec.describe Notify do is_expected.to have_referable_subject(merge_request, reply: true) is_expected.to have_body_text('merged') is_expected.to have_body_text(project_merge_request_path(project, merge_request)) + is_expected.to have_link(merge_request.to_reference, href: project_merge_request_url(merge_request.target_project, merge_request)) end end end @@ -534,6 +537,7 @@ RSpec.describe Notify do is_expected.to have_referable_subject(merge_request, reply: true) is_expected.to have_body_text(project_merge_request_path(project, merge_request)) is_expected.to have_body_text('due to conflict.') + is_expected.to have_link(merge_request.to_reference, href: project_merge_request_url(merge_request.target_project, merge_request)) end end end @@ -567,6 +571,7 @@ RSpec.describe Notify do is_expected.to have_referable_subject(merge_request, reply: true) is_expected.to have_body_text("#{push_user.name} pushed new commits") is_expected.to have_body_text(project_merge_request_path(project, merge_request)) + is_expected.to have_link(merge_request.to_reference, href: project_merge_request_url(merge_request.target_project, merge_request)) end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index d71cd843a47..68f1a0f1ba1 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -425,6 +425,73 @@ RSpec.describe API::MergeRequests do end end + context 'NOT params' do + let(:merge_request2) do + create( + :merge_request, + :simple, + milestone: milestone, + author: user, + assignees: [user], + merge_request_context_commits: [merge_request_context_commit], + source_project: project, + target_project: project, + source_branch: 'what', + title: "What", + created_at: base_time + ) + end + + before do + create(:label_link, label: label, target: merge_request) + create(:label_link, label: label2, target: merge_request2) + end + + it 'returns merge requests without any of the labels given', :aggregate_failures do + get api(endpoint_path, user), params: { not: { labels: ["#{label.title}, #{label2.title}"] } } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(3) + json_response.each do |mr| + expect(mr['labels']).not_to include(label2.title, label.title) + end + end + + it 'returns merge requests without any of the milestones given', :aggregate_failures do + get api(endpoint_path, user), params: { not: { milestone: milestone.title } } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(4) + json_response.each do |mr| + expect(mr['milestone']).not_to eq(milestone.title) + end + end + + it 'returns merge requests without the author given', :aggregate_failures do + get api(endpoint_path, user), params: { not: { author_id: user2.id } } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(5) + json_response.each do |mr| + expect(mr['author']['id']).not_to eq(user2.id) + end + end + + it 'returns merge requests without the assignee given', :aggregate_failures do + get api(endpoint_path, user), params: { not: { assignee_id: user2.id } } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(5) + json_response.each do |mr| + expect(mr['assignee']['id']).not_to eq(user2.id) + end + end + end + context 'source_branch param' do it 'returns merge requests with the given source branch' do get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' } diff --git a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb index a0b62ad5194..5157574ea04 100644 --- a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb +++ b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb @@ -28,23 +28,34 @@ RSpec.describe Ci::CreatePipelineService do end describe '#execute' do - subject { service.execute(:web, content: content) } + context 'when source is a dangling build' do + subject { service.execute(:ondemand_dast_scan, content: content) } - context 'parameter config content' do - it 'creates a pipeline' do - expect(subject).to be_persisted - end + context 'parameter config content' do + it 'creates a pipeline' do + expect(subject).to be_persisted + end - it 'creates builds with the correct names' do - expect(subject.builds.pluck(:name)).to match_array %w[dast] - end + it 'creates builds with the correct names' do + expect(subject.builds.pluck(:name)).to match_array %w[dast] + end + + it 'creates stages with the correct names' do + expect(subject.stages.pluck(:name)).to match_array %w[dast] + end - it 'creates stages with the correct names' do - expect(subject.stages.pluck(:name)).to match_array %w[dast] + it 'sets the correct config source' do + expect(subject.config_source).to eq 'parameter_source' + end end + end + + context 'when source is not a dangling build' do + subject { service.execute(:web, content: content) } - it 'sets the correct config source' do - expect(subject.config_source).to eq 'parameter_source' + it 'raises an exception' do + klass = Gitlab::Ci::Pipeline::Chain::Config::Content::Parameter::UnsupportedSourceError + expect { subject }.to raise_error(klass) end end end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 6a9d8857b32..95ee6fe556c 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -87,6 +87,18 @@ RSpec.describe EventCreateService do expect { service.reopen_mr(merge_request, merge_request.author) }.to change { ResourceStateEvent.count } end end + + describe '#approve_mr' do + let(:merge_request) { create(:merge_request) } + + it { expect(service.approve_mr(merge_request, user)).to be_truthy } + + it 'creates new event' do + service.approve_mr(merge_request, user) + + change { Event.approved_action.where(target: merge_request).count }.by(1) + end + end end describe 'Milestone' do diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb new file mode 100644 index 00000000000..68b9caa30ab --- /dev/null +++ b/spec/services/merge_requests/approval_service_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::ApprovalService do + describe '#execute' do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let!(:todo) { create(:todo, user: user, project: project, target: merge_request) } + + subject(:service) { described_class.new(project, user) } + + context 'with invalid approval' do + before do + allow(merge_request.approvals).to receive(:new).and_return(double(save: false)) + end + + it 'does not create an approval note' do + expect(SystemNoteService).not_to receive(:approve_mr) + + service.execute(merge_request) + end + + it 'does not mark pending todos as done' do + service.execute(merge_request) + + expect(todo.reload).to be_pending + end + end + + context 'with valid approval' do + it 'creates an approval note and marks pending todos as done' do + expect(SystemNoteService).to receive(:approve_mr).with(merge_request, user) + expect(merge_request.approvals).to receive(:reset) + + service.execute(merge_request) + + expect(todo.reload).to be_done + end + + it 'creates approve MR event' do + expect_next_instance_of(EventCreateService) do |instance| + expect(instance).to receive(:approve_mr) + .with(merge_request, user) + end + + service.execute(merge_request) + end + + context 'with remaining approvals' do + it 'fires an approval webhook' do + expect(service).to receive(:execute_hooks).with(merge_request, 'approved') + + service.execute(merge_request) + end + end + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 2a1cb3e0585..4d265258449 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -661,4 +661,14 @@ RSpec.describe SystemNoteService do described_class.design_discussion_added(discussion_note) end end + + describe '.approve_mr' do + it 'calls MergeRequestsService' do + expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service| + expect(service).to receive(:approve_mr) + end + + described_class.approve_mr(noteable, author) + end + end end diff --git a/spec/services/system_notes/merge_requests_service_spec.rb b/spec/services/system_notes/merge_requests_service_spec.rb index 1c378123797..17155969a33 100644 --- a/spec/services/system_notes/merge_requests_service_spec.rb +++ b/spec/services/system_notes/merge_requests_service_spec.rb @@ -261,4 +261,18 @@ RSpec.describe ::SystemNotes::MergeRequestsService do expect(subject.commit_id).to eq(commit_sha) end end + + describe '#approve_mr' do + subject { described_class.new(noteable: noteable, project: project, author: author).approve_mr } + + it_behaves_like 'a system note' do + let(:action) { 'approved' } + end + + context 'when merge request approved' do + it 'sets the note text' do + expect(subject.note).to eq "approved this merge request" + end + end + end end diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb index 2b8daa80ab4..07b6b98222f 100644 --- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb @@ -23,12 +23,12 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests # We cannot use `let_it_be` here otherwise we get: # Failure/Error: allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) # The use of doubles or partial doubles from rspec-mocks outside of the per-test lifecycle is not supported. - let(:project2) do + let!(:project2) do allow_gitaly_n_plus_1 do fork_project(project1, user) end end - let(:project3) do + let!(:project3) do allow_gitaly_n_plus_1 do fork_project(project1, user).tap do |project| project.update!(archived: true) @@ -45,6 +45,9 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests allow_gitaly_n_plus_1 { create(:project, group: subgroup) } end + let!(:label) { create(:label, project: project1) } + let!(:label2) { create(:label, project: project1) } + let!(:merge_request1) do create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, @@ -72,6 +75,9 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests title: '[WIP]') end + let!(:label_link) { create(:label_link, label: label, target: merge_request2) } + let!(:label_link2) { create(:label_link, label: label2, target: merge_request3) } + before do project1.add_maintainer(user) project2.add_developer(user) |