diff options
116 files changed, 1834 insertions, 800 deletions
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index ceaf7058c5a..586d6867ab4 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -4,7 +4,7 @@ import flash from '~/flash'; import { __, sprintf, s__ } from '~/locale'; import { GlModal } from '@gitlab/ui'; import { modalTypes } from '../../constants'; -import { trimPathComponents } from '../../utils'; +import { trimPathComponents, getPathParent } from '../../utils'; export default { components: { @@ -85,8 +85,10 @@ export default { } }, createFromTemplate(template) { + const parent = getPathParent(this.entryName); + const name = parent ? `${parent}/${template.name}` : template.name; this.createTempEntry({ - name: template.name, + name, type: this.modalType, }); diff --git a/app/assets/javascripts/pipelines/components/dag/constants.js b/app/assets/javascripts/pipelines/components/dag/constants.js index 252d2d8e4ad..51b1fb4f4cc 100644 --- a/app/assets/javascripts/pipelines/components/dag/constants.js +++ b/app/assets/javascripts/pipelines/components/dag/constants.js @@ -1,4 +1,10 @@ +/* Error constants */ export const PARSE_FAILURE = 'parse_failure'; export const LOAD_FAILURE = 'load_failure'; export const UNSUPPORTED_DATA = 'unsupported_data'; export const DEFAULT = 'default'; + +/* Interaction handles */ +export const IS_HIGHLIGHTED = 'dag-highlighted'; +export const LINK_SELECTOR = 'dag-link'; +export const NODE_SELECTOR = 'dag-node'; diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue index d91fe9bb7f4..063ec091e4d 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue @@ -1,8 +1,13 @@ <script> import * as d3 from 'd3'; import { uniqueId } from 'lodash'; -import { PARSE_FAILURE } from './constants'; - +import { LINK_SELECTOR, NODE_SELECTOR, PARSE_FAILURE } from './constants'; +import { + highlightLinks, + restoreLinks, + toggleLinkHighlight, + togglePathHighlights, +} from './interactions'; import { getMaxNodes, removeOrphanNodes } from './parsing_utils'; import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils'; @@ -16,11 +21,7 @@ export default { paddingForLabels: 100, labelMargin: 8, - // can plausibly applied through CSS instead, TBD baseOpacity: 0.8, - highlightIn: 1, - highlightOut: 0.2, - containerClasses: ['dag-graph-container', 'gl-display-flex', 'gl-flex-direction-column'].join( ' ', ), @@ -88,6 +89,20 @@ export default { ); }, + appendLinkInteractions(link) { + return link + .on('mouseover', highlightLinks) + .on('mouseout', restoreLinks.bind(null, this.$options.viewOptions.baseOpacity)) + .on('click', toggleLinkHighlight.bind(null, this.$options.viewOptions.baseOpacity)); + }, + + appendNodeInteractions(node) { + return node.on( + 'click', + togglePathHighlights.bind(null, this.$options.viewOptions.baseOpacity), + ); + }, + appendLabelAsForeignObject(d, i, n) { const currentNode = n[i]; const { height, wrapperWidth, width, x, y, textAlign } = labelPosition(d, { @@ -163,15 +178,17 @@ export default { }, createLinks(svg, linksData) { - const link = this.generateLinks(svg, linksData); - this.createGradient(link); - this.createClip(link); - this.appendLinks(link); + const links = this.generateLinks(svg, linksData); + this.createGradient(links); + this.createClip(links); + this.appendLinks(links); + this.appendLinkInteractions(links); }, createNodes(svg, nodeData) { - this.generateNodes(svg, nodeData); + const nodes = this.generateNodes(svg, nodeData); this.labelNodes(svg, nodeData); + this.appendNodeInteractions(nodes); }, drawGraph({ maxNodesPerLayer, linksAndNodes }) { @@ -202,37 +219,39 @@ export default { }, generateLinks(svg, linksData) { - const linkContainerName = 'dag-link'; - return svg .append('g') .attr('fill', 'none') .attr('stroke-opacity', this.$options.viewOptions.baseOpacity) - .selectAll(`.${linkContainerName}`) + .selectAll(`.${LINK_SELECTOR}`) .data(linksData) .enter() .append('g') .attr('id', d => { - return this.createAndAssignId(d, 'uid', linkContainerName); + return this.createAndAssignId(d, 'uid', LINK_SELECTOR); }) - .classed(`${linkContainerName} gl-cursor-pointer`, true); + .classed(`${LINK_SELECTOR} gl-cursor-pointer`, true); }, generateNodes(svg, nodeData) { - const nodeContainerName = 'dag-node'; const { nodeWidth } = this.$options.viewOptions; return svg .append('g') - .selectAll(`.${nodeContainerName}`) + .selectAll(`.${NODE_SELECTOR}`) .data(nodeData) .enter() .append('line') - .classed(`${nodeContainerName} gl-cursor-pointer`, true) + .classed(`${NODE_SELECTOR} gl-cursor-pointer`, true) .attr('id', d => { - return this.createAndAssignId(d, 'uid', nodeContainerName); + return this.createAndAssignId(d, 'uid', NODE_SELECTOR); + }) + .attr('stroke', d => { + const color = this.color(d); + /* eslint-disable-next-line no-param-reassign */ + d.color = color; + return color; }) - .attr('stroke', this.color) .attr('stroke-width', nodeWidth) .attr('stroke-linecap', 'round') .attr('x1', d => Math.floor((d.x1 + d.x0) / 2)) diff --git a/app/assets/javascripts/pipelines/components/dag/interactions.js b/app/assets/javascripts/pipelines/components/dag/interactions.js new file mode 100644 index 00000000000..c9008730c90 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/dag/interactions.js @@ -0,0 +1,134 @@ +import * as d3 from 'd3'; +import { LINK_SELECTOR, NODE_SELECTOR, IS_HIGHLIGHTED } from './constants'; + +export const highlightIn = 1; +export const highlightOut = 0.2; + +const getCurrent = (idx, collection) => d3.select(collection[idx]); +const currentIsLive = (idx, collection) => getCurrent(idx, collection).classed(IS_HIGHLIGHTED); +const getOtherLinks = () => d3.selectAll(`.${LINK_SELECTOR}:not(.${IS_HIGHLIGHTED})`); +const getNodesNotLive = () => d3.selectAll(`.${NODE_SELECTOR}:not(.${IS_HIGHLIGHTED})`); + +const backgroundLinks = selection => selection.style('stroke-opacity', highlightOut); +const backgroundNodes = selection => selection.attr('stroke', '#f2f2f2'); +const foregroundLinks = selection => selection.style('stroke-opacity', highlightIn); +const foregroundNodes = selection => selection.attr('stroke', d => d.color); +const renewLinks = (selection, baseOpacity) => selection.style('stroke-opacity', baseOpacity); +const renewNodes = selection => selection.attr('stroke', d => d.color); + +const getAllLinkAncestors = node => { + if (node.targetLinks) { + return node.targetLinks.flatMap(n => { + return [n.uid, ...getAllLinkAncestors(n.source)]; + }); + } + + return []; +}; + +const getAllNodeAncestors = node => { + let allNodes = []; + + if (node.targetLinks) { + allNodes = node.targetLinks.flatMap(n => { + return getAllNodeAncestors(n.source); + }); + } + + return [...allNodes, node.uid]; +}; + +export const highlightLinks = (d, idx, collection) => { + const currentLink = getCurrent(idx, collection); + const currentSourceNode = d3.select(`#${d.source.uid}`); + const currentTargetNode = d3.select(`#${d.target.uid}`); + + /* Higlight selected link, de-emphasize others */ + backgroundLinks(getOtherLinks()); + foregroundLinks(currentLink); + + /* Do the same to related nodes */ + backgroundNodes(getNodesNotLive()); + foregroundNodes(currentSourceNode); + foregroundNodes(currentTargetNode); +}; + +const highlightPath = (parentLinks, parentNodes) => { + /* de-emphasize everything else */ + backgroundLinks(getOtherLinks()); + backgroundNodes(getNodesNotLive()); + + /* highlight correct links */ + parentLinks.forEach(id => { + foregroundLinks(d3.select(`#${id}`)).classed(IS_HIGHLIGHTED, true); + }); + + /* highlight correct nodes */ + parentNodes.forEach(id => { + foregroundNodes(d3.select(`#${id}`)).classed(IS_HIGHLIGHTED, true); + }); +}; + +const restorePath = (parentLinks, parentNodes, baseOpacity) => { + parentLinks.forEach(id => { + renewLinks(d3.select(`#${id}`), baseOpacity).classed(IS_HIGHLIGHTED, false); + }); + + parentNodes.forEach(id => { + d3.select(`#${id}`).classed(IS_HIGHLIGHTED, false); + }); + + if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) { + renewLinks(getOtherLinks(), baseOpacity); + renewNodes(getNodesNotLive()); + return; + } + + backgroundLinks(getOtherLinks()); + backgroundNodes(getNodesNotLive()); +}; + +export const restoreLinks = (baseOpacity, d, idx, collection) => { + /* in this case, it has just been clicked */ + if (currentIsLive(idx, collection)) { + return; + } + + /* + if there exist live links, reset to highlight out / pale + otherwise, reset to base + */ + + if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) { + renewLinks(d3.selectAll(`.${LINK_SELECTOR}`), baseOpacity); + renewNodes(d3.selectAll(`.${NODE_SELECTOR}`)); + return; + } + + backgroundLinks(getOtherLinks()); + backgroundNodes(getNodesNotLive()); +}; + +export const toggleLinkHighlight = (baseOpacity, d, idx, collection) => { + if (currentIsLive(idx, collection)) { + restorePath([d.uid], [d.source.uid, d.target.uid], baseOpacity); + return; + } + + highlightPath([d.uid], [d.source.uid, d.target.uid]); +}; + +export const togglePathHighlights = (baseOpacity, d, idx, collection) => { + const parentLinks = getAllLinkAncestors(d); + const parentNodes = getAllNodeAncestors(d); + const currentNode = getCurrent(idx, collection); + + /* if this node is already live, make it unlive and reset its path */ + if (currentIsLive(idx, collection)) { + currentNode.classed(IS_HIGHLIGHTED, false); + restorePath(parentLinks, parentNodes, baseOpacity); + return; + } + + highlightPath(parentLinks, parentNodes); +}; diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue new file mode 100644 index 00000000000..8bdf043a106 --- /dev/null +++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue @@ -0,0 +1,70 @@ +<script> +import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui'; + +import { ALERT_MESSAGES, ADMIN_GARBAGE_COLLECTION_TIP } from '../../constants/index'; + +export default { + components: { + GlSprintf, + GlAlert, + GlLink, + }, + model: { + prop: 'deleteAlertType', + event: 'change', + }, + props: { + deleteAlertType: { + type: String, + default: null, + required: false, + validator(value) { + return !value || ALERT_MESSAGES[value] !== undefined; + }, + }, + garbageCollectionHelpPagePath: { type: String, required: false, default: '' }, + isAdmin: { + type: Boolean, + default: false, + required: false, + }, + }, + computed: { + deleteAlertConfig() { + const config = { + title: '', + message: '', + type: 'success', + }; + if (this.deleteAlertType) { + [config.type] = this.deleteAlertType.split('_'); + + config.message = ALERT_MESSAGES[this.deleteAlertType]; + + if (this.isAdmin && config.type === 'success') { + config.title = config.message; + config.message = ADMIN_GARBAGE_COLLECTION_TIP; + } + } + return config; + }, + }, +}; +</script> + +<template> + <gl-alert + v-if="deleteAlertType" + :variant="deleteAlertConfig.type" + :title="deleteAlertConfig.title" + @dismiss="$emit('change', null)" + > + <gl-sprintf :message="deleteAlertConfig.message"> + <template #docLink="{content}"> + <gl-link :href="garbageCollectionHelpPagePath" target="_blank"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </gl-alert> +</template> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue new file mode 100644 index 00000000000..96f221bf71d --- /dev/null +++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue @@ -0,0 +1,67 @@ +<script> +import { GlModal, GlSprintf } from '@gitlab/ui'; +import { n__ } from '~/locale'; +import { REMOVE_TAG_CONFIRMATION_TEXT, REMOVE_TAGS_CONFIRMATION_TEXT } from '../../constants/index'; + +export default { + components: { + GlModal, + GlSprintf, + }, + props: { + itemsToBeDeleted: { + type: Array, + required: false, + default: () => [], + }, + }, + computed: { + modalAction() { + return n__( + 'ContainerRegistry|Remove tag', + 'ContainerRegistry|Remove tags', + this.itemsToBeDeleted.length, + ); + }, + modalDescription() { + if (this.itemsToBeDeleted.length > 1) { + return { + message: REMOVE_TAGS_CONFIRMATION_TEXT, + item: this.itemsToBeDeleted.length, + }; + } + const [first] = this.itemsToBeDeleted; + + return { + message: REMOVE_TAG_CONFIRMATION_TEXT, + item: first?.path, + }; + }, + }, + methods: { + show() { + this.$refs.deleteModal.show(); + }, + }, +}; +</script> + +<template> + <gl-modal + ref="deleteModal" + modal-id="delete-tag-modal" + ok-variant="danger" + @ok="$emit('confirmDelete')" + @cancel="$emit('cancelDelete')" + > + <template #modal-title>{{ modalAction }}</template> + <template #modal-ok>{{ modalAction }}</template> + <p v-if="modalDescription" data-testid="description"> + <gl-sprintf :message="modalDescription.message"> + <template #item + ><b>{{ modalDescription.item }}</b></template + > + </gl-sprintf> + </p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue new file mode 100644 index 00000000000..c254dd05aa4 --- /dev/null +++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue @@ -0,0 +1,30 @@ +<script> +import { GlSprintf } from '@gitlab/ui'; +import { DETAILS_PAGE_TITLE } from '../../constants/index'; + +export default { + components: { GlSprintf }, + props: { + imageName: { + type: String, + required: false, + default: '', + }, + }, + i18n: { + DETAILS_PAGE_TITLE, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-my-2 gl-align-items-center"> + <h4> + <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE"> + <template #imageName> + {{ imageName }} + </template> + </gl-sprintf> + </h4> + </div> +</template> diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js index 0eeb6001772..a1fa995c17f 100644 --- a/app/assets/javascripts/registry/explorer/constants/details.js +++ b/app/assets/javascripts/registry/explorer/constants/details.js @@ -47,3 +47,14 @@ export const LIST_KEY_SIZE = 'total_size'; export const LIST_KEY_LAST_UPDATED = 'created_at'; export const LIST_KEY_ACTIONS = 'actions'; export const LIST_KEY_CHECKBOX = 'checkbox'; +export const ALERT_SUCCESS_TAG = 'success_tag'; +export const ALERT_DANGER_TAG = 'danger_tag'; +export const ALERT_SUCCESS_TAGS = 'success_tags'; +export const ALERT_DANGER_TAGS = 'danger_tags'; + +export const ALERT_MESSAGES = { + [ALERT_SUCCESS_TAG]: DELETE_TAG_SUCCESS_MESSAGE, + [ALERT_DANGER_TAG]: DELETE_TAG_ERROR_MESSAGE, + [ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE, + [ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE, +}; diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue index 9b8ae16e4cd..31dbc1c8203 100644 --- a/app/assets/javascripts/registry/explorer/pages/details.vue +++ b/app/assets/javascripts/registry/explorer/pages/details.vue @@ -7,10 +7,6 @@ import { GlIcon, GlTooltipDirective, GlPagination, - GlModal, - GlSprintf, - GlAlert, - GlLink, GlEmptyState, GlResizeObserverDirective, GlSkeletonLoader, @@ -21,6 +17,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import Tracking from '~/tracking'; +import DeleteAlert from '../components/details_page/delete_alert.vue'; +import DeleteModal from '../components/details_page/delete_modal.vue'; +import DetailsHeader from '../components/details_page/details_header.vue'; import { decodeAndParse } from '../utils'; import { LIST_KEY_TAG, @@ -33,34 +32,29 @@ import { LIST_LABEL_IMAGE_ID, LIST_LABEL_SIZE, LIST_LABEL_LAST_UPDATED, - DELETE_TAG_SUCCESS_MESSAGE, - DELETE_TAG_ERROR_MESSAGE, - DELETE_TAGS_SUCCESS_MESSAGE, - DELETE_TAGS_ERROR_MESSAGE, - REMOVE_TAG_CONFIRMATION_TEXT, - REMOVE_TAGS_CONFIRMATION_TEXT, - DETAILS_PAGE_TITLE, REMOVE_TAGS_BUTTON_TITLE, REMOVE_TAG_BUTTON_TITLE, EMPTY_IMAGE_REPOSITORY_TITLE, EMPTY_IMAGE_REPOSITORY_MESSAGE, - ADMIN_GARBAGE_COLLECTION_TIP, + ALERT_SUCCESS_TAG, + ALERT_DANGER_TAG, + ALERT_SUCCESS_TAGS, + ALERT_DANGER_TAGS, } from '../constants/index'; export default { components: { + DeleteAlert, + DetailsHeader, GlTable, GlFormCheckbox, GlDeprecatedButton, GlIcon, ClipboardButton, GlPagination, - GlModal, + DeleteModal, GlSkeletonLoader, - GlSprintf, GlEmptyState, - GlAlert, - GlLink, }, directives: { GlTooltip: GlTooltipDirective, @@ -73,18 +67,11 @@ export default { height: 40, }, i18n: { - DETAILS_PAGE_TITLE, REMOVE_TAGS_BUTTON_TITLE, REMOVE_TAG_BUTTON_TITLE, EMPTY_IMAGE_REPOSITORY_TITLE, EMPTY_IMAGE_REPOSITORY_MESSAGE, }, - alertMessages: { - success_tag: DELETE_TAG_SUCCESS_MESSAGE, - danger_tag: DELETE_TAG_ERROR_MESSAGE, - success_tags: DELETE_TAGS_SUCCESS_MESSAGE, - danger_tags: DELETE_TAGS_ERROR_MESSAGE, - }, data() { return { selectedItems: [], @@ -92,7 +79,7 @@ export default { selectAllChecked: false, modalDescription: null, isDesktop: true, - deleteAlertType: false, + deleteAlertType: null, }; }, computed: { @@ -119,21 +106,12 @@ export default { { key: LIST_KEY_ACTIONS, label: '' }, ].filter(f => f.key !== LIST_KEY_CHECKBOX || this.isDesktop); }, - isMultiDelete() { - return this.itemsToBeDeleted.length > 1; - }, tracking() { return { - label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete', + label: + this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete', }; }, - modalAction() { - return n__( - 'ContainerRegistry|Remove tag', - 'ContainerRegistry|Remove tags', - this.isMultiDelete ? this.itemsToBeDeleted.length : 1, - ); - }, currentPage: { get() { return this.tagsPagination.page; @@ -142,47 +120,12 @@ export default { this.requestTagsList({ pagination: { page }, params: this.$route.params.id }); }, }, - deleteAlertConfig() { - const config = { - title: '', - message: '', - type: 'success', - }; - if (this.deleteAlertType) { - [config.type] = this.deleteAlertType.split('_'); - - const defaultMessage = this.$options.alertMessages[this.deleteAlertType]; - - if (this.config.isAdmin && config.type === 'success') { - config.title = defaultMessage; - config.message = ADMIN_GARBAGE_COLLECTION_TIP; - } else { - config.message = defaultMessage; - } - } - return config; - }, }, mounted() { this.requestTagsList({ params: this.$route.params.id }); }, methods: { ...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']), - setModalDescription(itemIndex = -1) { - if (itemIndex === -1) { - this.modalDescription = { - message: REMOVE_TAGS_CONFIRMATION_TEXT, - item: this.itemsToBeDeleted.length, - }; - } else { - const { path } = this.tags[itemIndex]; - - this.modalDescription = { - message: REMOVE_TAG_CONFIRMATION_TEXT, - item: path, - }; - } - }, formatSize(size) { return numberToHumanSize(size); }, @@ -197,53 +140,49 @@ export default { } }, selectAll() { - this.selectedItems = this.tags.map((x, index) => index); + this.selectedItems = this.tags.map(x => x.name); this.selectAllChecked = true; }, deselectAll() { this.selectedItems = []; this.selectAllChecked = false; }, - updateSelectedItems(index) { - const delIndex = this.selectedItems.findIndex(x => x === index); + updateSelectedItems(name) { + const delIndex = this.selectedItems.findIndex(x => x === name); if (delIndex > -1) { this.selectedItems.splice(delIndex, 1); this.selectAllChecked = false; } else { - this.selectedItems.push(index); + this.selectedItems.push(name); if (this.selectedItems.length === this.tags.length) { this.selectAllChecked = true; } } }, - deleteSingleItem(index) { - this.setModalDescription(index); - this.itemsToBeDeleted = [index]; + deleteSingleItem(name) { + this.itemsToBeDeleted = [{ ...this.tags.find(t => t.name === name) }]; this.track('click_button'); this.$refs.deleteModal.show(); }, deleteMultipleItems() { - this.itemsToBeDeleted = [...this.selectedItems]; - if (this.selectedItems.length === 1) { - this.setModalDescription(this.itemsToBeDeleted[0]); - } else if (this.selectedItems.length > 1) { - this.setModalDescription(); - } + this.itemsToBeDeleted = this.selectedItems.map(name => ({ + ...this.tags.find(t => t.name === name), + })); this.track('click_button'); this.$refs.deleteModal.show(); }, - handleSingleDelete(index) { - const itemToDelete = this.tags[index]; + handleSingleDelete() { + const [itemToDelete] = this.itemsToBeDeleted; this.itemsToBeDeleted = []; - this.selectedItems = this.selectedItems.filter(i => i !== index); + this.selectedItems = this.selectedItems.filter(name => name !== itemToDelete.name); return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id }) .then(() => { - this.deleteAlertType = 'success_tag'; + this.deleteAlertType = ALERT_SUCCESS_TAG; }) .catch(() => { - this.deleteAlertType = 'danger_tag'; + this.deleteAlertType = ALERT_DANGER_TAG; }); }, handleMultipleDelete() { @@ -252,22 +191,22 @@ export default { this.selectedItems = []; return this.requestDeleteTags({ - ids: itemsToBeDeleted.map(x => this.tags[x].name), + ids: itemsToBeDeleted.map(x => x.name), params: this.$route.params.id, }) .then(() => { - this.deleteAlertType = 'success_tags'; + this.deleteAlertType = ALERT_SUCCESS_TAGS; }) .catch(() => { - this.deleteAlertType = 'danger_tags'; + this.deleteAlertType = ALERT_DANGER_TAGS; }); }, onDeletionConfirmed() { this.track('confirm_delete'); - if (this.isMultiDelete) { + if (this.itemsToBeDeleted.length > 1) { this.handleMultipleDelete(); } else { - this.handleSingleDelete(this.itemsToBeDeleted[0]); + this.handleSingleDelete(); } }, handleResize() { @@ -279,30 +218,14 @@ export default { <template> <div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element"> - <gl-alert - v-if="deleteAlertType" - :variant="deleteAlertConfig.type" - :title="deleteAlertConfig.title" + <delete-alert + v-model="deleteAlertType" + :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath" + :is-admin="config.isAdmin" class="my-2" - @dismiss="deleteAlertType = null" - > - <gl-sprintf :message="deleteAlertConfig.message"> - <template #docLink="{content}"> - <gl-link :href="config.garbageCollectionHelpPagePath" target="_blank"> - {{ content }} - </gl-link> - </template> - </gl-sprintf> - </gl-alert> - <div class="d-flex my-3 align-items-center"> - <h4> - <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE"> - <template #imageName> - {{ imageName }} - </template> - </gl-sprintf> - </h4> - </div> + /> + + <details-header :image-name="imageName" /> <gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty> <template v-if="isDesktop" #head(checkbox)> @@ -327,12 +250,12 @@ export default { </gl-deprecated-button> </template> - <template #cell(checkbox)="{index}"> + <template #cell(checkbox)="{item}"> <gl-form-checkbox ref="rowCheckbox" class="js-row-checkbox" - :checked="selectedItems.includes(index)" - @change="updateSelectedItems(index)" + :checked="selectedItems.includes(item.name)" + @change="updateSelectedItems(item.name)" /> </template> <template #cell(name)="{item, field}"> @@ -373,7 +296,7 @@ export default { {{ timeFormatted(value) }} </span> </template> - <template #cell(actions)="{index, item}"> + <template #cell(actions)="{item}"> <gl-deprecated-button ref="singleDeleteButton" :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE" @@ -381,7 +304,7 @@ export default { :disabled="!item.destroy_path" variant="danger" class="js-delete-registry float-right btn-inverted btn-border-color btn-icon" - @click="deleteSingleItem(index)" + @click="deleteSingleItem(item.name)" > <gl-icon name="remove" /> </gl-deprecated-button> @@ -425,22 +348,11 @@ export default { class="w-100" /> - <gl-modal + <delete-modal ref="deleteModal" - modal-id="delete-tag-modal" - ok-variant="danger" - @ok="onDeletionConfirmed" + :items-to-be-deleted="itemsToBeDeleted" + @confirmDelete="onDeletionConfirmed" @cancel="track('cancel_delete')" - > - <template #modal-title>{{ modalAction }}</template> - <template #modal-ok>{{ modalAction }}</template> - <p v-if="modalDescription"> - <gl-sprintf :message="modalDescription.message"> - <template #item> - <b>{{ modalDescription.item }}</b> - </template> - </gl-sprintf> - </p> - </gl-modal> + /> </div> </template> diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 11064f18418..e4bee01f61f 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -137,7 +137,7 @@ .badge.badge-pill:not(.fly-out-badge), .sidebar-context-title, .nav-item-name { - display: none; + @include gl-sr-only; } .sidebar-top-level-items > li > a { diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 0b1b3f2bcba..98fa8202e25 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -16,19 +16,6 @@ module IssuableActions end end - def permitted_keys - [ - :issuable_ids, - :assignee_id, - :milestone_id, - :state_event, - :subscription_event, - label_ids: [], - add_label_ids: [], - remove_label_ids: [] - ] - end - def show respond_to do |format| format.html do @@ -221,10 +208,20 @@ module IssuableActions end def bulk_update_params - permitted_keys_array = permitted_keys.dup - permitted_keys_array << { assignee_ids: [] } + params.require(:update).permit(bulk_update_permitted_keys) + end - params.require(:update).permit(permitted_keys_array) + def bulk_update_permitted_keys + [ + :issuable_ids, + :assignee_id, + :milestone_id, + :state_event, + :subscription_event, + assignee_ids: [], + add_label_ids: [], + remove_label_ids: [] + ] end def resource_name diff --git a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb index dfda5fca310..e3cef427fd6 100644 --- a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb +++ b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb @@ -7,7 +7,7 @@ class Projects::Ci::DailyBuildGroupReportResultsController < Projects::Applicati REPORT_WINDOW = 90.days before_action :validate_feature_flag! - before_action :authorize_download_code! # Share the same authorization rules as the graphs controller + before_action :authorize_read_build_report_results! before_action :validate_param_type! def index diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 34246f27241..856601dfc01 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -6,7 +6,7 @@ class Projects::GraphsController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :assign_ref_vars - before_action :authorize_download_code! + before_action :authorize_read_repository_graphs! def show respond_to do |format| @@ -54,7 +54,8 @@ class Projects::GraphsController < Projects::ApplicationController end def get_daily_coverage_options - return unless Feature.enabled?(:ci_download_daily_code_coverage, default_enabled: true) + return unless Feature.enabled?(:ci_download_daily_code_coverage, @project, default_enabled: true) + return unless can?(current_user, :read_build_report_results, project) date_today = Date.current report_window = Projects::Ci::DailyBuildGroupReportResultsController::REPORT_WINDOW diff --git a/app/finders/ci/daily_build_group_report_results_finder.rb b/app/finders/ci/daily_build_group_report_results_finder.rb index 3c3c24c1479..3ce38f39b0b 100644 --- a/app/finders/ci/daily_build_group_report_results_finder.rb +++ b/app/finders/ci/daily_build_group_report_results_finder.rb @@ -14,7 +14,7 @@ module Ci end def execute - return none unless can?(current_user, :download_code, project) + return none unless can?(current_user, :read_build_report_results, project) Ci::DailyBuildGroupReportResult.recent_results( { diff --git a/app/graphql/mutations/merge_requests/set_labels.rb b/app/graphql/mutations/merge_requests/set_labels.rb index 9560989a421..c1e45808593 100644 --- a/app/graphql/mutations/merge_requests/set_labels.rb +++ b/app/graphql/mutations/merge_requests/set_labels.rb @@ -24,8 +24,9 @@ module Mutations project = merge_request.project label_ids = label_ids + .map { |gid| GlobalID.parse(gid) } .select(&method(:label_descendant?)) - .map { |gid| GlobalID.parse(gid).model_id } # MergeRequests::UpdateService expects integers + .map(&:model_id) # MergeRequests::UpdateService expects integers attribute_name = case operation_mode when Types::MutationOperationModeEnum.enum[:append] @@ -46,7 +47,7 @@ module Mutations end def label_descendant?(gid) - GlobalID.parse(gid)&.model_class&.ancestors&.include?(Label) + gid&.model_class&.ancestors&.include?(Label) end end end diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb index 6be98fe3d44..55c9084caf2 100644 --- a/app/models/design_management/version.rb +++ b/app/models/design_management/version.rb @@ -88,7 +88,7 @@ module DesignManagement rows = design_actions.map { |action| action.row_attrs(version) } - Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) + Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert version.designs.reset version.validate! design_actions.each(&:performed) diff --git a/app/models/event.rb b/app/models/event.rb index 40f412af7c4..03a43a6e93c 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -76,7 +76,7 @@ class Event < ApplicationRecord # Callbacks after_create :reset_project_activity after_create :set_last_repository_updated_at, if: :push_action? - after_create :track_user_interacted_projects + after_create ->(event) { UserInteractedProject.track(event) } # Scopes scope :recent, -> { reorder(id: :desc) } @@ -429,13 +429,6 @@ class Event < ApplicationRecord .update_all(last_repository_updated_at: created_at) end - def track_user_interacted_projects - # Note the call to .available? is due to earlier migrations - # that would otherwise conflict with the call to .track - # (because the table does not exist yet). - UserInteractedProject.track(self) if UserInteractedProject.available? - end - def design_action_names { created: _('uploaded'), diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index b6882701e23..21cf6bfa414 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -25,8 +25,6 @@ class InternalId < ApplicationRecord validates :usage, presence: true - REQUIRED_SCHEMA_VERSION = 20180305095250 - # Increments #last_value and saves the record # # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). @@ -63,24 +61,16 @@ class InternalId < ApplicationRecord class << self def track_greatest(subject, scope, usage, new_value, init) - return new_value unless available? - InternalIdGenerator.new(subject, scope, usage) .track_greatest(init, new_value) end def generate_next(subject, scope, usage, init) - # Shortcut if `internal_ids` table is not available (yet) - # This can be the case in other (unrelated) migration specs - return (init.call(subject) || 0) + 1 unless available? - InternalIdGenerator.new(subject, scope, usage) .generate(init) end def reset(subject, scope, usage, value) - return false unless available? - InternalIdGenerator.new(subject, scope, usage) .reset(value) end @@ -95,20 +85,6 @@ class InternalId < ApplicationRecord where(filter).delete_all end - - def available? - return true unless Rails.env.test? - - Gitlab::SafeRequestStore.fetch(:internal_ids_available_flag) do - ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION - end - end - - # Flushes cached information about schema - def reset_column_information - Gitlab::SafeRequestStore[:internal_ids_available_flag] = nil - super - end end class InternalIdGenerator diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb index eecb10e6dbc..de97fc33f8d 100644 --- a/app/models/merge_request_context_commit.rb +++ b/app/models/merge_request_context_commit.rb @@ -20,7 +20,7 @@ class MergeRequestContextCommit < ApplicationRecord # create MergeRequestContextCommit by given commit sha and it's diff file record def self.bulk_insert(*args) - Gitlab::Database.bulk_insert('merge_request_context_commits', *args) + Gitlab::Database.bulk_insert('merge_request_context_commits', *args) # rubocop:disable Gitlab/BulkInsert end def to_commit diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb index 9dce7c53ab6..b89d1983ce3 100644 --- a/app/models/merge_request_context_commit_diff_file.rb +++ b/app/models/merge_request_context_commit_diff_file.rb @@ -12,6 +12,6 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord # create MergeRequestContextCommitDiffFile by given diff file record(s) def self.bulk_insert(*args) - Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) + Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index f793bd3d76f..66b27aeac91 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -437,7 +437,7 @@ class MergeRequestDiff < ApplicationRecord transaction do MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all - Gitlab::Database.bulk_insert('merge_request_diff_files', rows) + Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert update!(stored_externally: false) end @@ -495,7 +495,7 @@ class MergeRequestDiff < ApplicationRecord rows = build_external_merge_request_diff_files(rows) if use_external_diff? # Faster inserts - Gitlab::Database.bulk_insert('merge_request_diff_files', rows) + Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert end def build_external_diff_tempfile(rows) diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index 2819ea7ce1e..9f6933d0879 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -27,6 +27,6 @@ class MergeRequestDiffCommit < ApplicationRecord ) end - Gitlab::Database.bulk_insert(self.table_name, rows) + Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert end end diff --git a/app/models/project.rb b/app/models/project.rb index 2d564c8c81d..dcd9f2aac2e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -96,8 +96,7 @@ class Project < ApplicationRecord after_create :create_project_feature, unless: :project_feature after_create :create_ci_cd_settings, - unless: :ci_cd_settings, - if: proc { ProjectCiCdSetting.available? } + unless: :ci_cd_settings after_create :create_container_expiration_policy, unless: :container_expiration_policy diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index c295837002a..e5fc481b035 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -3,9 +3,6 @@ class ProjectCiCdSetting < ApplicationRecord belongs_to :project, inverse_of: :ci_cd_settings - # The version of the schema that first introduced this model/table. - MINIMUM_SCHEMA_VERSION = 20180403035759 - DEFAULT_GIT_DEPTH = 50 before_create :set_default_git_depth @@ -20,16 +17,6 @@ class ProjectCiCdSetting < ApplicationRecord default_value_for :forward_deployment_enabled, true - def self.available? - @available ||= - ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION - end - - def self.reset_column_information - @available = nil - super - end - def forward_deployment_enabled? super && ::Feature.enabled?(:forward_deployment_enabled, project, default_enabled: true) end diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb index f6f72f4b77a..1c615777018 100644 --- a/app/models/user_interacted_project.rb +++ b/app/models/user_interacted_project.rb @@ -9,9 +9,6 @@ class UserInteractedProject < ApplicationRecord CACHE_EXPIRY_TIME = 1.day - # Schema version required for this model - REQUIRED_SCHEMA_VERSION = 20180223120443 - class << self def track(event) # For events without a project, we simply don't care. @@ -38,17 +35,6 @@ class UserInteractedProject < ApplicationRecord end end - # Check if we can safely call .track (table exists) - def available? - @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization - end - - # Flushes cached information about schema - def reset_column_information - @available_flag = nil - super - end - private def cached_exists?(project_id:, user_id:, &block) diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 156a466b3f8..f87c72007ec 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -568,6 +568,14 @@ class ProjectPolicy < BasePolicy rule { build_service_proxy_enabled }.enable :build_service_proxy_enabled + rule { can?(:download_code) }.policy do + enable :read_repository_graphs + end + + rule { can?(:read_build) & can?(:read_pipeline) }.policy do + enable :read_build_report_results + end + private def team_member? diff --git a/app/services/ci/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb index 97f9918fdb7..c756e376901 100644 --- a/app/services/ci/extract_sections_from_build_trace_service.rb +++ b/app/services/ci/extract_sections_from_build_trace_service.rb @@ -5,7 +5,7 @@ module Ci def execute(build) return false unless build.trace_sections.empty? - Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build)) + Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build)) # rubocop:disable Gitlab/BulkInsert true end diff --git a/app/services/clusters/applications/prometheus_config_service.rb b/app/services/clusters/applications/prometheus_config_service.rb index 34d44ab881e..50c4e26b0d0 100644 --- a/app/services/clusters/applications/prometheus_config_service.rb +++ b/app/services/clusters/applications/prometheus_config_service.rb @@ -132,19 +132,21 @@ module Clusters end def alerts(environment) - variables = Gitlab::Prometheus::QueryVariables.call(environment) alerts = Projects::Prometheus::AlertsFinder .new(environment: environment) .execute alerts.map do |alert| - substitute_query_variables(alert.to_param, variables) + hash = alert.to_param + hash['expr'] = substitute_query_variables(hash['expr'], environment) + hash end end - def substitute_query_variables(hash, variables) - hash['expr'] %= variables - hash + def substitute_query_variables(query, environment) + result = ::Prometheus::ProxyVariableSubstitutionService.new(environment, query: query).execute + + result[:params][:query] end def environments diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index 1518b697f86..2902385da4a 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -17,9 +17,8 @@ module Issuable ids = params.delete(:issuable_ids).split(",") items = find_issuables(parent, model_class, ids) - permitted_attrs(type).each do |key| - params.delete(key) unless params[key].present? - end + params.slice!(*permitted_attrs(type)) + params.delete_if { |k, v| v.blank? } if params[:assignee_ids] == [IssuableFinder::Params::NONE.to_s] params[:assignee_ids] = [] diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb index a78e191c85f..b185ab592ff 100644 --- a/app/services/issuable/clone/attributes_rewriter.rb +++ b/app/services/issuable/clone/attributes_rewriter.rb @@ -105,7 +105,7 @@ module Issuable yield(event) end.compact - Gitlab::Database.bulk_insert(table_name, events) + Gitlab::Database.bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 3fa5b60369c..38b10996f44 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -129,15 +129,11 @@ class IssuableBaseService < BaseService add_label_ids = attributes.delete(:add_label_ids) remove_label_ids = attributes.delete(:remove_label_ids) - new_label_ids = existing_label_ids || label_ids || [] + new_label_ids = label_ids || existing_label_ids || [] new_label_ids |= extra_label_ids - if add_label_ids.blank? && remove_label_ids.blank? - new_label_ids = label_ids if label_ids - else - new_label_ids |= add_label_ids if add_label_ids - new_label_ids -= remove_label_ids if remove_label_ids - end + new_label_ids |= add_label_ids if add_label_ids + new_label_ids -= remove_label_ids if remove_label_ids new_label_ids.uniq end diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb index 942cd8162e4..c57773c3302 100644 --- a/app/services/projects/detect_repository_languages_service.rb +++ b/app/services/projects/detect_repository_languages_service.rb @@ -21,7 +21,7 @@ module Projects .update_all(share: update[:share]) end - Gitlab::Database.bulk_insert( + Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert RepositoryLanguage.table_name, detection.insertions(matching_programming_languages) ) diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb index 39cd553261f..e86106f0a09 100644 --- a/app/services/projects/lfs_pointers/lfs_link_service.rb +++ b/app/services/projects/lfs_pointers/lfs_link_service.rb @@ -38,7 +38,7 @@ module Projects rows = existent_lfs_objects .not_linked_to_project(project) .map { |existing_lfs_object| { project_id: project.id, lfs_object_id: existing_lfs_object.id } } - Gitlab::Database.bulk_insert(:lfs_objects_projects, rows) + Gitlab::Database.bulk_insert(:lfs_objects_projects, rows) # rubocop:disable Gitlab/BulkInsert iterations += 1 linked_existing_objects += existent_lfs_objects.map(&:oid) diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb index e0d019f54be..dc23f727079 100644 --- a/app/services/resource_events/change_labels_service.rb +++ b/app/services/resource_events/change_labels_service.rb @@ -22,7 +22,7 @@ module ResourceEvents label_hash.merge(label_id: label.id, action: ResourceLabelEvent.actions['remove']) end - Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels) + Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert resource.expire_note_etag_cache end diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb index 1d3338c1b45..93d2bd11426 100644 --- a/app/services/suggestions/create_service.rb +++ b/app/services/suggestions/create_service.rb @@ -25,7 +25,7 @@ module Suggestions end rows.in_groups_of(100, false) do |rows| - Gitlab::Database.bulk_insert('suggestions', rows) + Gitlab::Database.bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert end end end diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml index 42528f40123..b0593b3bfa2 100644 --- a/app/views/admin/application_settings/_outbound.html.haml +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -14,10 +14,10 @@ .form-group = f.label :outbound_local_requests_whitelist_raw, class: 'label-bold' do - = _('Whitelist to allow requests to the local network from hooks and services') + = _('Local IP addresses and domain names that hooks and services may access.') = f.text_area :outbound_local_requests_whitelist_raw, placeholder: "example.com, 192.168.1.1", class: 'form-control', rows: 8 %span.form-text.text-muted - = _('Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The whitelist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com.') + = _('Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The allowlist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com.') .form-group .form-check diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml index 507dd91d571..7119b22daef 100644 --- a/app/views/projects/issues/import_csv/_button.html.haml +++ b/app/views/projects/issues/import_csv/_button.html.haml @@ -1,4 +1,5 @@ - type = local_assigns.fetch(:type, :icon) +- can_edit = can?(current_user, :admin_project, @project) .dropdown.btn-group %button.btn.rounded-right.text-center{ class: ('has-tooltip' if type == :icon), title: (_('Import issues') if type == :icon), @@ -9,6 +10,7 @@ = _('Import issues') %ul.dropdown-menu %li - %button.btn{ data: { toggle: 'modal', target: '.issues-import-modal' } } + %button{ data: { toggle: 'modal', target: '.issues-import-modal' } } = _('Import CSV') - %li= link_to _('Import from Jira'), project_import_jira_path(@project) + - if can_edit + %li= link_to _('Import from Jira'), project_import_jira_path(@project) diff --git a/app/workers/gitlab/jira_import/import_issue_worker.rb b/app/workers/gitlab/jira_import/import_issue_worker.rb index 78de5cf1307..89f5fe8d462 100644 --- a/app/workers/gitlab/jira_import/import_issue_worker.rb +++ b/app/workers/gitlab/jira_import/import_issue_worker.rb @@ -48,7 +48,7 @@ module Gitlab label_link_attrs << build_label_attrs(issue_id, import_label_id.to_i) - Gitlab::Database.bulk_insert(LabelLink.table_name, label_link_attrs) + Gitlab::Database.bulk_insert(LabelLink.table_name, label_link_attrs) # rubocop:disable Gitlab/BulkInsert end def assign_issue(project_id, issue_id, assignee_ids) @@ -56,7 +56,7 @@ module Gitlab assignee_attrs = assignee_ids.map { |user_id| { issue_id: issue_id, user_id: user_id } } - Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignee_attrs) + Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignee_attrs) # rubocop:disable Gitlab/BulkInsert end def build_label_attrs(issue_id, label_id) diff --git a/changelogs/unreleased/218569-dont-show-import-from-jira-button-for-non-entitled-users.yml b/changelogs/unreleased/218569-dont-show-import-from-jira-button-for-non-entitled-users.yml new file mode 100644 index 00000000000..da712f0aa59 --- /dev/null +++ b/changelogs/unreleased/218569-dont-show-import-from-jira-button-for-non-entitled-users.yml @@ -0,0 +1,5 @@ +--- +title: Hide "Import from Jira" option from non-entitled users +merge_request: 32685 +author: +type: fixed diff --git a/changelogs/unreleased/220144-substitute-variables-in-alerts.yml b/changelogs/unreleased/220144-substitute-variables-in-alerts.yml new file mode 100644 index 00000000000..b270feee3eb --- /dev/null +++ b/changelogs/unreleased/220144-substitute-variables-in-alerts.yml @@ -0,0 +1,5 @@ +--- +title: Fix bug with variable substitution in alerts +merge_request: 33772 +author: +type: fixed diff --git a/changelogs/unreleased/bump_ci_auto_deploy_0_16.yml b/changelogs/unreleased/bump_ci_auto_deploy_0_16.yml new file mode 100644 index 00000000000..b222b69f295 --- /dev/null +++ b/changelogs/unreleased/bump_ci_auto_deploy_0_16.yml @@ -0,0 +1,5 @@ +--- +title: Update Auto deploy image to v0.16.1, introducing support for AUTO_DEVOPS_DEPLOY_DEBUG +merge_request: 33799 +author: +type: changed diff --git a/changelogs/unreleased/cngo-add-link-text-to-collapsed-left-sidebar.yml b/changelogs/unreleased/cngo-add-link-text-to-collapsed-left-sidebar.yml new file mode 100644 index 00000000000..0d7f75d24b8 --- /dev/null +++ b/changelogs/unreleased/cngo-add-link-text-to-collapsed-left-sidebar.yml @@ -0,0 +1,5 @@ +--- +title: Add link text to collapsed left sidebar links for screen readers +merge_request: 33866 +author: +type: fixed diff --git a/changelogs/unreleased/dennis-update-webhooks-page-from-whitelist-to-allowlist.yml b/changelogs/unreleased/dennis-update-webhooks-page-from-whitelist-to-allowlist.yml new file mode 100644 index 00000000000..80d2ec6d7ba --- /dev/null +++ b/changelogs/unreleased/dennis-update-webhooks-page-from-whitelist-to-allowlist.yml @@ -0,0 +1,5 @@ +--- +title: Update local IP address and domain name allow list input label +merge_request: 33812 +author: +type: changed diff --git a/changelogs/unreleased/instance_auto_devops_enabled_usage_ping.yml b/changelogs/unreleased/instance_auto_devops_enabled_usage_ping.yml new file mode 100644 index 00000000000..22e72c5bb8f --- /dev/null +++ b/changelogs/unreleased/instance_auto_devops_enabled_usage_ping.yml @@ -0,0 +1,5 @@ +--- +title: Add whether instance has Auto DevOps enabled to usage ping +merge_request: 33811 +author: +type: changed diff --git a/changelogs/unreleased/sh-avoid-extra-route-reload.yml b/changelogs/unreleased/sh-avoid-extra-route-reload.yml new file mode 100644 index 00000000000..7c363ac92d8 --- /dev/null +++ b/changelogs/unreleased/sh-avoid-extra-route-reload.yml @@ -0,0 +1,5 @@ +--- +title: Speed up boot time in production +merge_request: 33929 +author: +type: performance diff --git a/changelogs/unreleased/sh-workhorse-direct-access-upload.yml b/changelogs/unreleased/sh-workhorse-direct-access-upload.yml new file mode 100644 index 00000000000..41aee482891 --- /dev/null +++ b/changelogs/unreleased/sh-workhorse-direct-access-upload.yml @@ -0,0 +1,5 @@ +--- +title: Support Workhorse directly uploading files to S3 +merge_request: 29389 +author: +type: added diff --git a/changelogs/unreleased/templates-current-folder-fix.yml b/changelogs/unreleased/templates-current-folder-fix.yml new file mode 100644 index 00000000000..9b4999c9d54 --- /dev/null +++ b/changelogs/unreleased/templates-current-folder-fix.yml @@ -0,0 +1,5 @@ +--- +title: "Web IDE: Create template files in the folder from which new file request was made" +merge_request: 33585 +author: Ashesh Vidyut +type: fixed diff --git a/config/application.rb b/config/application.rb index df1ff971203..d0c211bf608 100644 --- a/config/application.rb +++ b/config/application.rb @@ -301,7 +301,10 @@ module Gitlab end config.after_initialize do - Rails.application.reload_routes! + # Devise (see initializers/8_devise.rb) already reloads routes if + # eager loading is enabled, so don't do this twice since it's + # expensive. + Rails.application.reload_routes! unless config.eager_load project_url_helpers = Module.new do extend ActiveSupport::Concern diff --git a/config/initializers/8_devise.rb b/config/initializers/8_devise.rb index 6b4dc91ed86..2be6e535fee 100644 --- a/config/initializers/8_devise.rb +++ b/config/initializers/8_devise.rb @@ -6,6 +6,11 @@ Devise.setup do |config| manager.default_strategies(scope: :user).unshift :two_factor_backupable end + # This is the default. This makes it explicit that Devise loads routes + # before eager loading. Disabling this seems to cause an error loading + # grape-entity `expose` for some reason. + config.reload_routes = true + # ==> Mailer Configuration # Configure the class responsible to send e-mails. config.mailer = "DeviseMailer" diff --git a/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb b/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb index e2ec7b62d31..b41c55ce622 100644 --- a/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb +++ b/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb @@ -20,7 +20,7 @@ class MigrateSamlIdentitiesToScimIdentities < ActiveRecord::Migration[6.0] record.attributes.extract!("extern_uid", "user_id", "group_id", "active", "created_at", "updated_at") end - Gitlab::Database.bulk_insert(:scim_identities, data_to_insert, on_conflict: :do_nothing) + Gitlab::Database.bulk_insert(:scim_identities, data_to_insert, on_conflict: :do_nothing) # rubocop:disable Gitlab/BulkInsert end end diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 3b98ac7316d..3c2812d8b52 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -322,28 +322,6 @@ application server, or a Gitaly node. } ``` -1. Enable automatic failover by editing `/etc/gitlab/gitlab.rb`: - - ```ruby - praefect['failover_enabled'] = true - praefect['failover_election_strategy'] = 'sql' - ``` - - When automatic failover is enabled, Praefect checks the health of internal - Gitaly nodes. If the primary has a certain amount of health checks fail, it - will promote one of the secondaries to be primary, and demote the primary to - be a secondary. - - NOTE: **Note:** Database leader election will be [enabled by default in the - future](https://gitlab.com/gitlab-org/gitaly/-/issues/2682). - - Caution, **automatic failover** favors availability over consistency and will - cause data loss if changes have not been replicated to the newly elected - primary. In the next release, leader election will [prefer to promote up to - date replicas](https://gitlab.com/gitlab-org/gitaly/-/issues/2642), and it - will be an option to favor consistency by marking [out-of-date repositories - read-only](https://gitlab.com/gitlab-org/gitaly/-/issues/2630). - 1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure): @@ -738,7 +716,7 @@ Praefect regularly checks the health of each backend Gitaly node. This information can be used to automatically failover to a new primary node if the current primary node is found to be unhealthy. -- **PostgreSQL (recommended):** Enabled by setting +- **PostgreSQL (recommended):** Enabled by default, and equivalent to: `praefect['failover_election_strategy'] = sql`. This configuration option will allow multiple Praefect nodes to coordinate via the PostgreSQL database to elect a primary Gitaly node. This configuration @@ -749,18 +727,13 @@ current primary node is found to be unhealthy. reconfigured in `/etc/gitlab/gitlab.rb` on the Praefect node. Modify the `praefect['virtual_storages']` field by moving the `primary = true` to promote a different Gitaly node to primary. In the steps above, `gitaly-1` was set to - the primary. -- **Memory:** Enabled by setting `praefect['failover_enabled'] = true` in - `/etc/gitlab/gitlab.rb` on the Praefect node. If a sufficient number of health + the primary. Requires `praefect['failover_enabled'] = false` in the configuration. +- **Memory:** Enabled by setting `praefect['failover_election_strategy'] = 'local'` + in `/etc/gitlab/gitlab.rb` on the Praefect node. If a sufficient number of health checks fail for the current primary backend Gitaly node, and new primary will be elected. **Do not use with multiple Praefect nodes!** Using with multiple Praefect nodes is likely to result in a split brain. -NOTE: **Note:**: Praefect does not yet account for replication lag on -the secondaries during the election process, so data loss can occur -during a failover. Follow issue -[#2642](https://gitlab.com/gitlab-org/gitaly/-/issues/2642) for updates. - It is likely that we will implement support for Consul, and a cloud native strategy in the future. diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md index 39819ccd79b..1dea2de73f6 100644 --- a/doc/administration/object_storage.md +++ b/doc/administration/object_storage.md @@ -141,10 +141,88 @@ Using the default GitLab settings, some object storage back-ends such as and [Alibaba](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/1564) might generate `ETag mismatch` errors. +If you are seeing this ETag mismatch error with Amazon Web Services S3, +it's likely this is due to [encryption settings on your bucket](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html). +See the section on [using Amazon instance profiles](#using-amazon-instance-profiles) on how to fix this issue. + When using GitLab direct upload, the [workaround for MinIO](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/1564#note_244497658) is to use the `--compat` parameter on the server. -We are working on a fix to GitLab component Workhorse, and also -a workaround, in the mean time, to -[allow ETag verification to be disabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18175). +We are working on a fix to the [GitLab Workhorse +component](https://gitlab.com/gitlab-org/gitlab-workhorse/-/issues/222). + +### Using Amazon instance profiles + +Instead of supplying AWS access and secret keys in object storage +configuration, GitLab can be configured to use IAM roles to set up an +[Amazon instance profile](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html). +When this is used, GitLab will fetch temporary credentials each time an +S3 bucket is accessed, so no hard-coded values are needed in the +configuration. + +#### Encrypted S3 buckets + +> Introduced in [GitLab 13.1](https://gitlab.com/gitlab-org/gitlab-workhorse/-/merge_requests/466) only for instance profiles. + +When configured to use an instance profile, GitLab Workhorse +will properly upload files to S3 buckets that have [SSE-S3 or SSE-KMS +encryption enabled by default](https://docs.aws.amazon.com/kms/latest/developerguide/services-s3.html). +Note that customer master keys (CMKs) and SSE-C encryption are not yet +supported since this requires supplying keys to the GitLab +configuration. + +Without instance profiles enabled (or prior to GitLab 13.1), GitLab +Workhorse will upload files to S3 using pre-signed URLs that do not have +a `Content-MD5` HTTP header computed for them. To ensure data is not +corrupted, Workhorse checks that the MD5 hash of the data sent equals +the ETag header returned from the S3 server. When encryption is enabled, +this is not the case, which causes Workhorse to report an `ETag +mismatch` error during an upload. + +With instance profiles enabled, GitLab Workhorse uses an AWS S3 client +that properly computes and sends the `Content-MD5` header to the server, +which eliminates the need for comparing ETag headers. If the data is +corrupted in transit, the S3 server will reject the file. + +#### IAM Permissions + +To set up an instance profile, create an Amazon Identity Access and +Management (IAM) role with the necessary permissions. The following +example is a role for an S3 bucket named `test-bucket`: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:GetObject", + "s3:AbortMultipartUpload", + "s3:DeleteObject" + ], + "Resource": "arn:aws:s3:::test-bucket/*" + } + ] +} +``` + +Associate this role with your GitLab instance, and then configure GitLab +to use it via the `use_iam_profile` configuration option. For example, +when configuring uploads to use object storage, see the `AWS IAM profiles` +section in [S3 compatible connection settings](uploads.md#s3-compatible-connection-settings). + +#### Disabling the feature + +The Workhorse S3 client is only enabled when the `use_iam_profile` +configuration flag is `true`. + +To disable this feature, ask a GitLab administrator with [Rails console access](feature_flags.md#how-to-enable-and-disable-features-behind-flags) to run the +following command: + +```ruby +Feature.disable(:use_workhorse_s3_client) +``` diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md index 7e330682f8c..33af356b37d 100644 --- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md +++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md @@ -380,39 +380,6 @@ user = User.find_by_username '' user.skip_reconfirmation! ``` -### Get an admin token - -```ruby -# Get the first admin's first access token (no longer works on 11.9+. see: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22743) -User.where(admin:true).first.personal_access_tokens.first.token - -# Get the first admin's private token (no longer works on 10.2+) -User.where(admin:true).private_token -``` - -### Create personal access token - -```ruby -personal_access_token = User.find(123).personal_access_tokens.create( - name: 'apitoken', - impersonation: false, - scopes: [:api] -) - -puts personal_access_token.token -``` - -You might also want to manually set the token string: - -```ruby -User.find(123).personal_access_tokens.create( - name: 'apitoken', - token_digest: Gitlab::CryptoHelper.sha256('some-token-string-here'), - impersonation: false, - scopes: [:api] -) -``` - ### Active users & Historical users ```ruby diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index e963bdd6848..bdd372e90ed 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -263,7 +263,7 @@ n_("%{project_name}", "%d projects selected", count) % { project_name: 'GitLab' A namespace is a way to group translations that belong together. They provide context to our translators by adding a prefix followed by the bar symbol (`|`). For example: ```ruby -_('Namespace|Translated string') +'Namespace|Translated string' ``` A namespace provide the following benefits: diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md index c7e0aa13adb..441ef545141 100644 --- a/doc/topics/autodevops/customize.md +++ b/doc/topics/autodevops/customize.md @@ -310,6 +310,7 @@ applications. | `AUTO_DEVOPS_CHART_REPOSITORY_NAME` | From GitLab 11.11, used to set the name of the Helm repository. Defaults to `gitlab`. | | `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME` | From GitLab 11.11, used to set a username to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD`. | | `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD` | From GitLab 11.11, used to set a password to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME`. | +| `AUTO_DEVOPS_DEPLOY_DEBUG` | From GitLab 13.1, if this variable is present, Helm will output debug logs. | | `AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE` | From GitLab 12.5, used in combination with [ModSecurity feature flag](../../user/clusters/applications.md#web-application-firewall-modsecurity) to toggle [ModSecurity's `SecRuleEngine`](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#SecRuleEngine) behavior. Defaults to `DetectionOnly`. | | `BUILDPACK_URL` | Buildpack's full URL. Can point to either [a Git repository URL or a tarball URL](#custom-buildpacks). | | `CANARY_ENABLED` | From GitLab 11.0, used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments-premium). | diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 87c1fe4007a..377b6cd393b 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -56,6 +56,58 @@ the following table. | `read_repository` | [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17894) | Allows read-only access (pull) to the repository through `git clone`. | | `write_repository` | [GitLab 11.11](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/26021) | Allows read-write access (pull, push) to the repository through `git clone`. Required for accessing Git repositories over HTTP when 2FA is enabled. | +## Programmatically creating a personal access token + +You can programmatically create a predetermined personal access token for use in +automation or tests. You will need sufficient access to run a +[Rails console session](../../administration/troubleshooting/debug.md#starting-a-rails-console-session) +for your GitLab instance. + +To create a token belonging to a user with username `automation-bot`, run the +following in the Rails console (`sudo gitlab-rails console`): + +```ruby +user = User.find_by_username('automation-bot') +token = user.personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token') +token.set_token('token-string-here123') +token.save! +``` + +This can be shortened into a single-line shell command using the +[GitLab Rails Runner](../../administration/troubleshooting/debug.md#using-the-rails-runner): + +```shell +sudo gitlab-rails runner "token = User.find_by_username('automation-bot').personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token'); token.set_token('token-string-here123'); token.save!" +``` + +NOTE: **Note:** +The token string must be 20 characters in length, or it will not be +recognized as a personal access token. + +The list of valid scopes and what they do can be found +[in the source code](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/auth.rb). + +## Programmatically revoking a personal access token + +You can programmatically revoke a personal access token. You will need +sufficient access to run a [Rails console session](../../administration/troubleshooting/debug.md#starting-a-rails-console-session) +for your GitLab instance. + +To revoke a known token `token-string-here123`, run the following in the Rails +console (`sudo gitlab-rails console`): + +```ruby +token = PersonalAccessToken.find_by_token('token-string-here123') +token.revoke! +``` + +This can be shorted into a single-line shell command using the +[GitLab Rails Runner](../../administration/troubleshooting/debug.md#using-the-rails-runner): + +```shell +sudo gitlab-rails runner "PersonalAccessToken.find_by_token('token-string-here123').revoke!" +``` + <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/user/project/import/jira.md b/doc/user/project/import/jira.md index fff3cf546b3..0b8807bb9b3 100644 --- a/doc/user/project/import/jira.md +++ b/doc/user/project/import/jira.md @@ -49,6 +49,7 @@ Importing large projects may take several minutes depending on the size of the i 1. On the **{issues}** **Issues** page, click the **Import Issues** (**{import}**) button. 1. Select **Import from Jira**. + This option is only visible if you have the [correct permissions](#permissions). ![Import issues from Jira button](img/jira/import_issues_from_jira_button_v12_10.png) diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md index fbecaf9ef72..ffb1f6a1407 100644 --- a/doc/user/project/service_desk.md +++ b/doc/user/project/service_desk.md @@ -173,12 +173,12 @@ As a result, a new Service Desk issue is created from this email in the `mygroup #### Enable custom email address -This feature comes with the `service_desk_email` feature flag disabled by default. +This feature comes with the `service_desk_custom_address` feature flag disabled by default. To turn on the feature, ask a GitLab administrator with Rails console access to run the following command: ```ruby -Feature.enable(:service_desk_email) +Feature.enable(:service_desk_custom_address) ``` The configuration options are the same as for configuring diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb index 263546bd132..bc113a1e33d 100644 --- a/lib/gitlab/background_migration/backfill_project_repositories.rb +++ b/lib/gitlab/background_migration/backfill_project_repositories.rb @@ -189,7 +189,7 @@ module Gitlab end def perform(start_id, stop_id) - Gitlab::Database.bulk_insert(:project_repositories, project_repositories(start_id, stop_id)) + Gitlab::Database.bulk_insert(:project_repositories, project_repositories(start_id, stop_id)) # rubocop:disable Gitlab/BulkInsert end private diff --git a/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb b/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb index 899f381e911..d2a9939b9ee 100644 --- a/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb +++ b/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb @@ -34,7 +34,7 @@ module Gitlab end end - Gitlab::Database.bulk_insert(TEMP_TABLE, fingerprints) + Gitlab::Database.bulk_insert(TEMP_TABLE, fingerprints) # rubocop:disable Gitlab/BulkInsert execute("ANALYZE #{TEMP_TABLE}") diff --git a/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb b/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb index 956f9daa493..2bce5037d03 100644 --- a/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb +++ b/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb @@ -65,7 +65,7 @@ module Gitlab next if service_ids.empty? migrated_ids += service_ids - Gitlab::Database.bulk_insert(table, data) + Gitlab::Database.bulk_insert(table, data) # rubocop:disable Gitlab/BulkInsert end return if migrated_ids.empty? diff --git a/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb index 35bfc381180..fcbcaacb2d6 100644 --- a/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb +++ b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb @@ -73,7 +73,7 @@ module Gitlab end def insert_into_cluster_kubernetes_namespace(rows) - Gitlab::Database.bulk_insert(Migratable::KubernetesNamespace.table_name, + Gitlab::Database.bulk_insert(Migratable::KubernetesNamespace.table_name, # rubocop:disable Gitlab/BulkInsert rows, disable_quote: [:created_at, :updated_at]) end diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb index d2924d10225..43698b7955f 100644 --- a/lib/gitlab/background_migration/populate_untracked_uploads.rb +++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb @@ -95,7 +95,7 @@ module Gitlab file.to_h.merge(created_at: 'NOW()') end - Gitlab::Database.bulk_insert('uploads', + Gitlab::Database.bulk_insert('uploads', # rubocop:disable Gitlab/BulkInsert rows, disable_quote: :created_at) end diff --git a/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb index cf0f582a2d4..d71a50a0af6 100644 --- a/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb +++ b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb @@ -25,7 +25,7 @@ module Gitlab mentions << mention_record unless mention_record.blank? end - Gitlab::Database.bulk_insert( + Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert resource_user_mention_model.table_name, mentions, return_ids: true, diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml index 5174aed04ba..21aace9f67a 100644 --- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .dast-auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.15.0" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1" dast_environment_deploy: extends: .dast-auto-deploy diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index 1e85aad02d9..381b116dacb 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.15.0" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1" include: - template: Jobs/Deploy/ECS.gitlab-ci.yml diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 008baf8ae0d..f2074d86bd7 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -12,8 +12,8 @@ # # To enable the experiment for 10% of the users: # -# chatops: `/chatops run feature set experiment_key_experiment_percentage 10 --actors` -# console: `Feature.enable_percentage_of_actors(:experiment_key_experiment_percentage, 10)` +# chatops: `/chatops run feature set experiment_key_experiment_percentage 10` +# console: `Feature.enable_percentage_of_time(:experiment_key_experiment_percentage, 10)` # # To disable the experiment: # @@ -26,7 +26,7 @@ # console: `Feature.get(:experiment_key_experiment_percentage).percentage_of_time_value` # -# TODO: rewrite that +# TODO: see https://gitlab.com/gitlab-org/gitlab/-/issues/217490 module Gitlab module Experimentation EXPERIMENTS = { diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb index 147597289cf..0d448b55104 100644 --- a/lib/gitlab/github_import/bulk_importing.rb +++ b/lib/gitlab/github_import/bulk_importing.rb @@ -17,7 +17,7 @@ module Gitlab # Bulk inserts the given rows into the database. def bulk_insert(model, rows, batch_size: 100) rows.each_slice(batch_size) do |slice| - Gitlab::Database.bulk_insert(model.table_name, slice) + Gitlab::Database.bulk_insert(model.table_name, slice) # rubocop:disable Gitlab/BulkInsert end end end diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb index d562958e955..53b17f77ccd 100644 --- a/lib/gitlab/github_import/importer/diff_note_importer.rb +++ b/lib/gitlab/github_import/importer/diff_note_importer.rb @@ -47,7 +47,7 @@ module Gitlab # To work around this we're using bulk_insert with a single row. This # allows us to efficiently insert data (even if it's just 1 row) # without having to use all sorts of hacks to disable callbacks. - Gitlab::Database.bulk_insert(LegacyDiffNote.table_name, [attributes]) + Gitlab::Database.bulk_insert(LegacyDiffNote.table_name, [attributes]) # rubocop:disable Gitlab/BulkInsert rescue ActiveRecord::InvalidForeignKey # It's possible the project and the issue have been deleted since # scheduling this job. In this case we'll just skip creating the note. diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb index 8648cbaec9d..13061d2c9df 100644 --- a/lib/gitlab/github_import/importer/issue_importer.rb +++ b/lib/gitlab/github_import/importer/issue_importer.rb @@ -75,7 +75,7 @@ module Gitlab end end - Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignees) + Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignees) # rubocop:disable Gitlab/BulkInsert end end end diff --git a/lib/gitlab/github_import/importer/label_links_importer.rb b/lib/gitlab/github_import/importer/label_links_importer.rb index 2001b7e3482..77eb4542195 100644 --- a/lib/gitlab/github_import/importer/label_links_importer.rb +++ b/lib/gitlab/github_import/importer/label_links_importer.rb @@ -40,7 +40,7 @@ module Gitlab } end - Gitlab::Database.bulk_insert(LabelLink.table_name, rows) + Gitlab::Database.bulk_insert(LabelLink.table_name, rows) # rubocop:disable Gitlab/BulkInsert end def find_target_id diff --git a/lib/gitlab/github_import/importer/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb index 2b06d1b3baf..41f179d275b 100644 --- a/lib/gitlab/github_import/importer/note_importer.rb +++ b/lib/gitlab/github_import/importer/note_importer.rb @@ -38,7 +38,7 @@ module Gitlab # We're using bulk_insert here so we can bypass any validations and # callbacks. Running these would result in a lot of unnecessary SQL # queries being executed when importing large projects. - Gitlab::Database.bulk_insert(Note.table_name, [attributes]) + Gitlab::Database.bulk_insert(Note.table_name, [attributes]) # rubocop:disable Gitlab/BulkInsert rescue ActiveRecord::InvalidForeignKey # It's possible the project and the issue have been deleted since # scheduling this job. In this case we'll just skip creating the note. diff --git a/lib/gitlab/import/database_helpers.rb b/lib/gitlab/import/database_helpers.rb index aaade39dd62..f8ea7a7adcd 100644 --- a/lib/gitlab/import/database_helpers.rb +++ b/lib/gitlab/import/database_helpers.rb @@ -11,7 +11,7 @@ module Gitlab # We use bulk_insert here so we can bypass any queries executed by # callbacks or validation rules, as doing this wouldn't scale when # importing very large projects. - result = Gitlab::Database + result = Gitlab::Database # rubocop:disable Gitlab/BulkInsert .bulk_insert(relation.table_name, [attributes], return_ids: true) result.first diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 62f1c4ed0e8..0fe0ae1d4b6 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -181,6 +181,7 @@ module Gitlab def features_usage_data_ce { + instance_auto_devops_enabled: alt_usage_data(fallback: nil) { Gitlab::CurrentSettings.auto_devops_enabled? }, container_registry_enabled: alt_usage_data(fallback: nil) { Gitlab.config.registry.enabled }, dependency_proxy_enabled: Gitlab.config.try(:dependency_proxy)&.enabled, gitlab_shared_runners_enabled: alt_usage_data(fallback: nil) { Gitlab.config.gitlab_ci.shared_runners_enabled }, diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb index b3c0e68dbb3..5eab882039d 100644 --- a/lib/object_storage/direct_upload.rb +++ b/lib/object_storage/direct_upload.rb @@ -46,7 +46,7 @@ module ObjectStorage MultipartUpload: multipart_upload_hash, CustomPutHeaders: true, PutHeaders: upload_options - }.compact + }.merge(workhorse_client_hash).compact end def multipart_upload_hash @@ -60,6 +60,32 @@ module ObjectStorage } end + def workhorse_client_hash + return {} unless aws? + + { + UseWorkhorseClient: use_workhorse_s3_client?, + RemoteTempObjectID: object_name, + ObjectStorage: { + Provider: 'AWS', + S3Config: { + Bucket: bucket_name, + Region: credentials[:region], + Endpoint: credentials[:endpoint], + PathStyle: credentials.fetch(:path_style, false), + UseIamProfile: credentials.fetch(:use_iam_profile, false) + } + } + } + end + + def use_workhorse_s3_client? + Feature.enabled?(:use_workhorse_s3_client, default_enabled: true) && + credentials.fetch(:use_iam_profile, false) && + # The Golang AWS SDK does not support V2 signatures + credentials.fetch(:aws_signature_version, 4).to_i >= 4 + end + def provider credentials[:provider].to_s end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 03d7a71a90c..91abc1fd892 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6575,6 +6575,9 @@ msgstr "" msgid "Created by me" msgstr "" +msgid "Created date" +msgstr "" + msgid "Created issue %{issueLink}" msgstr "" @@ -13168,6 +13171,9 @@ msgstr "" msgid "Loading…" msgstr "" +msgid "Local IP addresses and domain names that hooks and services may access." +msgstr "" + msgid "Localization" msgstr "" @@ -15546,10 +15552,10 @@ msgstr "" msgid "PackageRegistry|Upcoming package managers" msgstr "" -msgid "PackageRegistry|You are about to delete <b>%{packageName}</b>, this operation is irreversible, are you sure?" +msgid "PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?" msgstr "" -msgid "PackageRegistry|You are about to delete version %{boldStart}%{version}%{boldEnd} of %{boldStart}%{name}%{boldEnd}. Are you sure?" +msgid "PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?" msgstr "" msgid "PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more." @@ -18597,7 +18603,7 @@ msgstr "" msgid "Requests Profiles" msgstr "" -msgid "Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The whitelist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com." +msgid "Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The allowlist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com." msgstr "" msgid "Require all users in this group to setup Two-factor authentication" @@ -19167,6 +19173,9 @@ msgstr "" msgid "Search projects..." msgstr "" +msgid "Search requirements" +msgstr "" + msgid "Search users" msgstr "" @@ -25000,9 +25009,6 @@ msgstr "" msgid "White helpers give contextual information." msgstr "" -msgid "Whitelist to allow requests to the local network from hooks and services" -msgstr "" - msgid "Who can be an approver?" msgstr "" diff --git a/rubocop/cop/gitlab/bulk_insert.rb b/rubocop/cop/gitlab/bulk_insert.rb new file mode 100644 index 00000000000..c03ffbe0b2a --- /dev/null +++ b/rubocop/cop/gitlab/bulk_insert.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Gitlab + # Cop that disallows the use of `Gitlab::Database.bulk_insert`, in favour of using + # the `BulkInsertSafe` module. + class BulkInsert < RuboCop::Cop::Cop + MSG = 'Use the `BulkInsertSafe` concern, instead of using `Gitlab::Database.bulk_insert`. See https://docs.gitlab.com/ee/development/insert_into_tables_in_batches.html' + + def_node_matcher :raw_union?, <<~PATTERN + (send (const (const nil? :Gitlab) :Database) :bulk_insert ...) + PATTERN + + def on_send(node) + return unless raw_union?(node) + + add_offense(node, location: :expression) + end + end + end + end +end diff --git a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb index 3b9a3dded1e..c0ce934a0f1 100644 --- a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb +++ b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb @@ -9,21 +9,8 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do let(:param_type) { 'coverage' } let(:start_date) { '2019-12-10' } let(:end_date) { '2020-03-09' } - - def create_daily_coverage(group_name, coverage, date) - create( - :ci_daily_build_group_report_result, - project: project, - ref_path: ref_path, - group_name: group_name, - data: { 'coverage' => coverage }, - date: date - ) - end - - def csv_response - CSV.parse(response.body) - end + let(:allowed_to_read) { true } + let(:user) { create(:user) } before do create_daily_coverage('rspec', 79.0, '2020-03-09') @@ -31,6 +18,11 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do create_daily_coverage('rspec', 67.0, '2019-12-09') create_daily_coverage('karma', 71.0, '2019-12-09') + sign_in(user) + + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_build_report_results, project).and_return(allowed_to_read) + get :index, params: { namespace_id: project.namespace, project_id: project, @@ -76,5 +68,28 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do expect(response).to have_gitlab_http_status(:unprocessable_entity) end end + + context 'when user is not allowed to read build report results' do + let(:allowed_to_read) { false } + + it 'responds with 404 error' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + def create_daily_coverage(group_name, coverage, date) + create( + :ci_daily_build_group_report_result, + project: project, + ref_path: ref_path, + group_name: group_name, + data: { 'coverage' => coverage }, + date: date + ) + end + + def csv_response + CSV.parse(response.body) end end diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb index 140af74b576..186b935a49a 100644 --- a/spec/controllers/projects/graphs_controller_spec.rb +++ b/spec/controllers/projects/graphs_controller_spec.rb @@ -42,23 +42,37 @@ RSpec.describe Projects::GraphsController do expect(response).to render_template(:charts) end - it 'sets the daily coverage options' do - Timecop.freeze do + context 'when anonymous users can read build report results' do + it 'sets the daily coverage options' do + Timecop.freeze do + get(:charts, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' }) + + expect(assigns[:daily_coverage_options]).to eq( + base_params: { + start_date: Time.current.to_date - 90.days, + end_date: Time.current.to_date, + ref_path: project.repository.expand_ref('master'), + param_type: 'coverage' + }, + download_path: namespace_project_ci_daily_build_group_report_results_path( + namespace_id: project.namespace, + project_id: project, + format: :csv + ) + ) + end + end + end + + context 'when anonymous users cannot read build report results' do + before do + project.update_column(:public_builds, false) + get(:charts, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' }) + end - expect(assigns[:daily_coverage_options]).to eq( - base_params: { - start_date: Time.current.to_date - 90.days, - end_date: Time.current.to_date, - ref_path: project.repository.expand_ref('master'), - param_type: 'coverage' - }, - download_path: namespace_project_ci_daily_build_group_report_results_path( - namespace_id: project.namespace, - project_id: project, - format: :csv - ) - ) + it 'does not set daily coverage options' do + expect(assigns[:daily_coverage_options]).to be_nil end end end diff --git a/spec/factories/design_management/designs.rb b/spec/factories/design_management/designs.rb index 59d4cc56f95..6d1229063d8 100644 --- a/spec/factories/design_management/designs.rb +++ b/spec/factories/design_management/designs.rb @@ -35,7 +35,7 @@ FactoryBot.define do sha = commit_version[action] version = DesignManagement::Version.new(sha: sha, issue: issue, author: evaluator.author) version.save(validate: false) # We need it to have an ID, validate later - Gitlab::Database.bulk_insert(dv_table_name, [action.row_attrs(version)]) + Gitlab::Database.bulk_insert(dv_table_name, [action.row_attrs(version)]) # rubocop:disable Gitlab/BulkInsert end # always a creation diff --git a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb index 3000ef650d3..e7b11bef787 100644 --- a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb +++ b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb @@ -8,17 +8,6 @@ describe Ci::DailyBuildGroupReportResultsFinder do let(:ref_path) { 'refs/heads/master' } let(:limit) { nil } - def create_daily_coverage(group_name, coverage, date) - create( - :ci_daily_build_group_report_result, - project: project, - ref_path: ref_path, - group_name: group_name, - data: { 'coverage' => coverage }, - date: date - ) - end - let!(:rspec_coverage_1) { create_daily_coverage('rspec', 79.0, '2020-03-09') } let!(:karma_coverage_1) { create_daily_coverage('karma', 89.0, '2020-03-09') } let!(:rspec_coverage_2) { create_daily_coverage('rspec', 95.0, '2020-03-10') } @@ -37,7 +26,7 @@ describe Ci::DailyBuildGroupReportResultsFinder do ).execute end - context 'when current user is allowed to download project code' do + context 'when current user is allowed to read build report results' do let(:current_user) { project.owner } it 'returns all matching results within the given date range' do @@ -61,7 +50,7 @@ describe Ci::DailyBuildGroupReportResultsFinder do end end - context 'when current user is not allowed to download project code' do + context 'when current user is not allowed to read build report results' do let(:current_user) { create(:user) } it 'returns an empty result' do @@ -69,4 +58,15 @@ describe Ci::DailyBuildGroupReportResultsFinder do end end end + + def create_daily_coverage(group_name, coverage, date) + create( + :ci_daily_build_group_report_result, + project: project, + ref_path: ref_path, + group_name: group_name, + data: { 'coverage' => coverage }, + date: date + ) + end end diff --git a/spec/fixtures/packages/conan/package_files/conan_package.tgz b/spec/fixtures/packages/conan/package_files/conan_package.tgz Binary files differnew file mode 100644 index 00000000000..6163364f3f9 --- /dev/null +++ b/spec/fixtures/packages/conan/package_files/conan_package.tgz diff --git a/spec/fixtures/packages/conan/package_files/conaninfo.txt b/spec/fixtures/packages/conan/package_files/conaninfo.txt new file mode 100644 index 00000000000..2a02515a19b --- /dev/null +++ b/spec/fixtures/packages/conan/package_files/conaninfo.txt @@ -0,0 +1,33 @@ +[settings] + arch=x86_64 + build_type=Release + compiler=apple-clang + compiler.libcxx=libc++ + compiler.version=10.0 + os=Macos + +[requires] + + +[options] + shared=False + +[full_settings] + arch=x86_64 + build_type=Release + compiler=apple-clang + compiler.libcxx=libc++ + compiler.version=10.0 + os=Macos + +[full_requires] + + +[full_options] + shared=False + +[recipe_hash] + b4b91125b36b40a7076a98310588f820 + +[env] + diff --git a/spec/fixtures/packages/conan/package_files/conanmanifest.txt b/spec/fixtures/packages/conan/package_files/conanmanifest.txt new file mode 100644 index 00000000000..bc34b81b050 --- /dev/null +++ b/spec/fixtures/packages/conan/package_files/conanmanifest.txt @@ -0,0 +1,4 @@ +1565723794 +conaninfo.txt: 2774ebe649804c1cd9430f26ab0ead14 +include/hello.h: 8727846905bd09baecf8bdc1edb1f46e +lib/libhello.a: 7f2aaa8b6f3bc316bba59e47b6a0bd43 diff --git a/spec/fixtures/packages/conan/recipe_files/conanfile.py b/spec/fixtures/packages/conan/recipe_files/conanfile.py new file mode 100644 index 00000000000..910bd5a0b51 --- /dev/null +++ b/spec/fixtures/packages/conan/recipe_files/conanfile.py @@ -0,0 +1,47 @@ +from conans import ConanFile, CMake, tools + + +class HelloConan(ConanFile): + name = "Hello" + version = "0.1" + license = "<Put the package license here>" + author = "<Put your name here> <And your email here>" + url = "<Package recipe repository url here, for issues about the package>" + description = "<Description of Hello here>" + topics = ("<Put some tag here>", "<here>", "<and here>") + settings = "os", "compiler", "build_type", "arch" + options = {"shared": [True, False]} + default_options = "shared=False" + generators = "cmake" + + def source(self): + self.run("git clone https://github.com/conan-io/hello.git") + # This small hack might be useful to guarantee proper /MT /MD linkage + # in MSVC if the packaged project doesn't have variables to set it + # properly + tools.replace_in_file("hello/CMakeLists.txt", "PROJECT(HelloWorld)", + '''PROJECT(HelloWorld) +include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) +conan_basic_setup()''') + + def build(self): + cmake = CMake(self) + cmake.configure(source_folder="hello") + cmake.build() + + # Explicit way: + # self.run('cmake %s/hello %s' + # % (self.source_folder, cmake.command_line)) + # self.run("cmake --build . %s" % cmake.build_config) + + def package(self): + self.copy("*.h", dst="include", src="hello") + self.copy("*hello.lib", dst="lib", keep_path=False) + self.copy("*.dll", dst="bin", keep_path=False) + self.copy("*.so", dst="lib", keep_path=False) + self.copy("*.dylib", dst="lib", keep_path=False) + self.copy("*.a", dst="lib", keep_path=False) + + def package_info(self): + self.cpp_info.libs = ["hello"] + diff --git a/spec/fixtures/packages/conan/recipe_files/conanmanifest.txt b/spec/fixtures/packages/conan/recipe_files/conanmanifest.txt new file mode 100644 index 00000000000..432b12f39fa --- /dev/null +++ b/spec/fixtures/packages/conan/recipe_files/conanmanifest.txt @@ -0,0 +1,2 @@ +1565723790 +conanfile.py: 7c042b95312cc4c4ee89199dc51aebf9 diff --git a/spec/fixtures/packages/maven/maven-metadata.xml b/spec/fixtures/packages/maven/maven-metadata.xml new file mode 100644 index 00000000000..7d7549df227 --- /dev/null +++ b/spec/fixtures/packages/maven/maven-metadata.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<metadata modelVersion="1.1.0"> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <version>1.0-SNAPSHOT</version> + <versioning> + <snapshot> + <timestamp>20180724.124855</timestamp> + <buildNumber>1</buildNumber> + </snapshot> + <lastUpdated>20180724124855</lastUpdated> + <snapshotVersions> + <snapshotVersion> + <extension>jar</extension> + <value>1.0-20180724.124855-1</value> + <updated>20180724124855</updated> + </snapshotVersion> + <snapshotVersion> + <extension>pom</extension> + <value>1.0-20180724.124855-1</value> + <updated>20180724124855</updated> + </snapshotVersion> + </snapshotVersions> + </versioning> +</metadata> diff --git a/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar b/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar Binary files differnew file mode 100644 index 00000000000..ea3903cf6d9 --- /dev/null +++ b/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar diff --git a/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.pom b/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.pom new file mode 100644 index 00000000000..6b6015314aa --- /dev/null +++ b/spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.pom @@ -0,0 +1,34 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>com.mycompany.app</groupId> + <artifactId>my-app</artifactId> + <packaging>jar</packaging> + <version>1.0-SNAPSHOT</version> + <name>my-app</name> + <url>http://maven.apache.org</url> + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>3.8.1</version> + <scope>test</scope> + </dependency> + </dependencies> + <distributionManagement> + <snapshotRepository> + <id>local</id> + <url>file:///tmp/maven</url> + </snapshotRepository> + </distributionManagement> + <repositories> + <repository> + <id>local</id> + <url>file:///tmp/maven</url> + </repository> + </repositories> + <properties> + <maven.compiler.source>1.6</maven.compiler.source> + <maven.compiler.target>1.6</maven.compiler.target> + </properties> +</project> diff --git a/spec/fixtures/packages/npm/foo-1.0.1.tgz b/spec/fixtures/packages/npm/foo-1.0.1.tgz Binary files differnew file mode 100644 index 00000000000..a2bcdb8d492 --- /dev/null +++ b/spec/fixtures/packages/npm/foo-1.0.1.tgz diff --git a/spec/fixtures/packages/npm/payload.json b/spec/fixtures/packages/npm/payload.json new file mode 100644 index 00000000000..664aa636001 --- /dev/null +++ b/spec/fixtures/packages/npm/payload.json @@ -0,0 +1,30 @@ +{ + "_id":"@root/npm-test", + "name":"@root/npm-test", + "description":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "dist-tags":{ + "latest":"1.0.1" + }, + "versions":{ + "1.0.1":{ + "name":"@root/npm-test", + "version":"1.0.1", + "main":"app.js", + "dependencies":{ + "express":"^4.16.4" + }, + "dist":{ + "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f" + } + } + }, + "_attachments":{ + "@root/npm-test-1.0.1.tgz":{ + "content_type":"application/octet-stream", + "data":"aGVsbG8K", + "length":8 + } + }, + "id":"10", + "package_name":"@root/npm-test" +} diff --git a/spec/fixtures/packages/npm/payload_with_duplicated_packages.json b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json new file mode 100644 index 00000000000..a6ea8760bd5 --- /dev/null +++ b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json @@ -0,0 +1,44 @@ +{ + "_id":"@root/npm-test", + "name":"@root/npm-test", + "description":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "dist-tags":{ + "latest":"1.0.1" + }, + "versions":{ + "1.0.1":{ + "name":"@root/npm-test", + "version":"1.0.1", + "main":"app.js", + "dependencies":{ + "express":"^4.16.4", + "dagre-d3": "~0.3.2" + }, + "devDependencies": { + "dagre-d3": "~0.3.2", + "d3": "~3.4.13" + }, + "bundleDependencies": { + "d3": "~3.4.13" + }, + "peerDependencies": { + "d3": "~3.3.0" + }, + "deprecated": { + "express":"^4.16.4" + }, + "dist":{ + "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f" + } + } + }, + "_attachments":{ + "@root/npm-test-1.0.1.tgz":{ + "content_type":"application/octet-stream", + "data":"aGVsbG8K", + "length":8 + } + }, + "id":"10", + "package_name":"@root/npm-test" +} diff --git a/spec/fixtures/packages/nuget/package.nupkg b/spec/fixtures/packages/nuget/package.nupkg Binary files differnew file mode 100644 index 00000000000..b36856ee569 --- /dev/null +++ b/spec/fixtures/packages/nuget/package.nupkg diff --git a/spec/fixtures/packages/nuget/with_dependencies.nuspec b/spec/fixtures/packages/nuget/with_dependencies.nuspec new file mode 100644 index 00000000000..753037cd05b --- /dev/null +++ b/spec/fixtures/packages/nuget/with_dependencies.nuspec @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> + <metadata> + <id>Test.Package</id> + <version>3.5.2</version> + <authors>Test Author</authors> + <owners>Test Owner</owners> + <requireLicenseAcceptance>false</requireLicenseAcceptance> + <description>Package Description</description> + <dependencies> + <dependency id="Moqi" version="2.5.6" include="Runtime,Compile" /> + <group targetFramework=".NETStandard2.0"> + <dependency id="Test.Dependency" version="2.3.7" exclude="Build,Analyzers" include="Runtime,Compile" /> + <dependency id="Newtonsoft.Json" version="12.0.3" exclude="Build,Analyzers" /> + </group> + <dependency id="Castle.Core" /> + </dependencies> + </metadata> +</package> diff --git a/spec/fixtures/packages/nuget/with_metadata.nuspec b/spec/fixtures/packages/nuget/with_metadata.nuspec new file mode 100644 index 00000000000..0043bc89527 --- /dev/null +++ b/spec/fixtures/packages/nuget/with_metadata.nuspec @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> + <metadata> + <id>DummyProject.WithMetadata</id> + <version>1.2.3</version> + <title>nuspec with metadata</title> + <authors>Author Test</authors> + <owners>Author Test</owners> + <developmentDependency>true</developmentDependency> + <requireLicenseAcceptance>true</requireLicenseAcceptance> + <licenseUrl>https://opensource.org/licenses/MIT</licenseUrl> + <projectUrl>https://gitlab.com/gitlab-org/gitlab</projectUrl> + <iconUrl>https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png</iconUrl> + <description>Description Test</description> + <releaseNotes>Release Notes Test</releaseNotes> + <copyright>Copyright Test</copyright> + <tags>foo bar test tag1 tag2 tag3 tag4 tag5</tags> + </metadata> +</package> diff --git a/spec/fixtures/packages/pypi/sample-project.tar.gz b/spec/fixtures/packages/pypi/sample-project.tar.gz Binary files differnew file mode 100644 index 00000000000..c71b1fef23d --- /dev/null +++ b/spec/fixtures/packages/pypi/sample-project.tar.gz diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js index 62a59a76bf4..da17cc3601e 100644 --- a/spec/frontend/ide/components/new_dropdown/modal_spec.js +++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js @@ -120,6 +120,46 @@ describe('new file modal component', () => { }); }); + describe('createFromTemplate', () => { + let store; + + beforeEach(() => { + store = createStore(); + store.state.entries = { + 'test-path/test': { + name: 'test', + deleted: false, + }, + }; + + vm = createComponentWithStore(Component, store).$mount(); + vm.open('blob'); + + jest.spyOn(vm, 'createTempEntry').mockImplementation(); + }); + + it.each` + entryName | newFilePath + ${''} | ${'.gitignore'} + ${'README.md'} | ${'.gitignore'} + ${'test-path/test/'} | ${'test-path/test/.gitignore'} + ${'test-path/test'} | ${'test-path/.gitignore'} + ${'test-path/test/abc.md'} | ${'test-path/test/.gitignore'} + `( + 'creates a new file with the given template name in appropriate directory for path: $path', + ({ entryName, newFilePath }) => { + vm.entryName = entryName; + + vm.createFromTemplate({ name: '.gitignore' }); + + expect(vm.createTempEntry).toHaveBeenCalledWith({ + name: newFilePath, + type: 'blob', + }); + }, + ); + }); + describe('submitForm', () => { let store; diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js index a6f712b1984..017461dfb84 100644 --- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js @@ -1,5 +1,7 @@ import { mount } from '@vue/test-utils'; import DagGraph from '~/pipelines/components/dag/dag_graph.vue'; +import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/pipelines/components/dag/constants'; +import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions'; import { createSankey } from '~/pipelines/components/dag/drawing_utils'; import { removeOrphanNodes } from '~/pipelines/components/dag/parsing_utils'; import { parsedData } from './mock_data'; @@ -8,8 +10,8 @@ describe('The DAG graph', () => { let wrapper; const getGraph = () => wrapper.find('.dag-graph-container > svg'); - const getAllLinks = () => wrapper.findAll('.dag-link'); - const getAllNodes = () => wrapper.findAll('.dag-node'); + const getAllLinks = () => wrapper.findAll(`.${LINK_SELECTOR}`); + const getAllNodes = () => wrapper.findAll(`.${NODE_SELECTOR}`); const getAllLabels = () => wrapper.findAll('foreignObject'); const createComponent = (propsData = {}) => { @@ -94,4 +96,123 @@ describe('The DAG graph', () => { }); }); }); + + describe('interactions', () => { + const strokeOpacity = opacity => `stroke-opacity: ${opacity};`; + const baseOpacity = () => wrapper.vm.$options.viewOptions.baseOpacity; + + describe('links', () => { + const liveLink = () => getAllLinks().at(4); + const otherLink = () => getAllLinks().at(1); + + describe('on hover', () => { + it('sets the link opacity to baseOpacity and background links to 0.2', () => { + liveLink().trigger('mouseover'); + expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); + expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); + }); + + it('reverts the styles on mouseout', () => { + liveLink().trigger('mouseover'); + liveLink().trigger('mouseout'); + expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); + expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); + }); + }); + + describe('on click', () => { + describe('toggles link liveness', () => { + it('turns link on', () => { + liveLink().trigger('click'); + expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); + expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); + }); + + it('turns link off on second click', () => { + liveLink().trigger('click'); + liveLink().trigger('click'); + expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); + expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); + }); + }); + + it('the link remains live even after mouseout', () => { + liveLink().trigger('click'); + liveLink().trigger('mouseout'); + expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); + expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); + }); + + it('preserves state when multiple links are toggled on and off', () => { + const anotherLiveLink = () => getAllLinks().at(2); + + liveLink().trigger('click'); + anotherLiveLink().trigger('click'); + expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); + expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); + expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); + + anotherLiveLink().trigger('click'); + expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn)); + expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightOut)); + expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut)); + + liveLink().trigger('click'); + expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); + expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); + expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity())); + }); + }); + }); + + describe('nodes', () => { + const liveNode = () => getAllNodes().at(10); + const anotherLiveNode = () => getAllNodes().at(5); + const nodesNotHighlighted = () => getAllNodes().filter(n => !n.classes(IS_HIGHLIGHTED)); + const linksNotHighlighted = () => getAllLinks().filter(n => !n.classes(IS_HIGHLIGHTED)); + const nodesHighlighted = () => getAllNodes().filter(n => n.classes(IS_HIGHLIGHTED)); + const linksHighlighted = () => getAllLinks().filter(n => n.classes(IS_HIGHLIGHTED)); + + describe('on click', () => { + it('highlights the clicked node and predecessors', () => { + liveNode().trigger('click'); + + expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true); + expect(linksNotHighlighted().length < getAllLinks().length).toBe(true); + + linksHighlighted().wrappers.forEach(link => { + expect(link.attributes('style')).toBe(strokeOpacity(highlightIn)); + }); + + nodesHighlighted().wrappers.forEach(node => { + expect(node.attributes('stroke')).not.toBe('#f2f2f2'); + }); + + linksNotHighlighted().wrappers.forEach(link => { + expect(link.attributes('style')).toBe(strokeOpacity(highlightOut)); + }); + + nodesNotHighlighted().wrappers.forEach(node => { + expect(node.attributes('stroke')).toBe('#f2f2f2'); + }); + }); + + it('toggles path off on second click', () => { + liveNode().trigger('click'); + liveNode().trigger('click'); + + expect(nodesNotHighlighted().length).toBe(getAllNodes().length); + expect(linksNotHighlighted().length).toBe(getAllLinks().length); + }); + + it('preserves state when multiple nodes are toggled on and off', () => { + anotherLiveNode().trigger('click'); + liveNode().trigger('click'); + anotherLiveNode().trigger('click'); + expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true); + expect(linksNotHighlighted().length < getAllLinks().length).toBe(true); + }); + }); + }); + }); }); diff --git a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js new file mode 100644 index 00000000000..6ef4dcf96b4 --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js @@ -0,0 +1,111 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import component from '~/registry/explorer/components/details_page/delete_alert.vue'; +import { + DELETE_TAG_SUCCESS_MESSAGE, + DELETE_TAG_ERROR_MESSAGE, + DELETE_TAGS_SUCCESS_MESSAGE, + DELETE_TAGS_ERROR_MESSAGE, + ADMIN_GARBAGE_COLLECTION_TIP, +} from '~/registry/explorer/constants'; + +describe('Delete alert', () => { + let wrapper; + + const findAlert = () => wrapper.find(GlAlert); + const findLink = () => wrapper.find(GlLink); + + const mountComponent = propsData => { + wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData }); + }; + + describe('when deleteAlertType is null', () => { + it('does not show the alert', () => { + mountComponent(); + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('when deleteAlertType is not null', () => { + describe('success states', () => { + describe.each` + deleteAlertType | message + ${'success_tag'} | ${DELETE_TAG_SUCCESS_MESSAGE} + ${'success_tags'} | ${DELETE_TAGS_SUCCESS_MESSAGE} + `('when deleteAlertType is $deleteAlertType', ({ deleteAlertType, message }) => { + it('alert exists', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + }); + + describe('when the user is an admin', () => { + beforeEach(() => { + mountComponent({ + deleteAlertType, + isAdmin: true, + garbageCollectionHelpPagePath: 'foo', + }); + }); + + it(`alert title is ${message}`, () => { + expect(findAlert().attributes('title')).toBe(message); + }); + + it('alert body contains admin tip', () => { + expect(findAlert().text()).toMatchInterpolatedText(ADMIN_GARBAGE_COLLECTION_TIP); + }); + + it('alert body contains link', () => { + const alertLink = findLink(); + expect(alertLink.exists()).toBe(true); + expect(alertLink.attributes('href')).toBe('foo'); + }); + }); + + describe('when the user is not an admin', () => { + it('alert exist and text is appropriate', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(message); + }); + }); + }); + }); + describe('error states', () => { + describe.each` + deleteAlertType | message + ${'danger_tag'} | ${DELETE_TAG_ERROR_MESSAGE} + ${'danger_tags'} | ${DELETE_TAGS_ERROR_MESSAGE} + `('when deleteAlertType is $deleteAlertType', ({ deleteAlertType, message }) => { + it('alert exists', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + }); + + describe('when the user is an admin', () => { + it('alert exist and text is appropriate', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(message); + }); + }); + + describe('when the user is not an admin', () => { + it('alert exist and text is appropriate', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(message); + }); + }); + }); + }); + + describe('dismissing alert', () => { + it('GlAlert dismiss event triggers a change event', () => { + mountComponent({ deleteAlertType: 'success_tags' }); + findAlert().vm.$emit('dismiss'); + expect(wrapper.emitted('change')).toEqual([[null]]); + }); + }); + }); +}); diff --git a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js new file mode 100644 index 00000000000..b30818005c3 --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js @@ -0,0 +1,74 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlSprintf } from '@gitlab/ui'; +import component from '~/registry/explorer/components/details_page/delete_modal.vue'; +import { + REMOVE_TAG_CONFIRMATION_TEXT, + REMOVE_TAGS_CONFIRMATION_TEXT, +} from '~/registry/explorer/constants'; +import { GlModal } from '../../stubs'; + +describe('Delete Modal', () => { + let wrapper; + + const findModal = () => wrapper.find(GlModal); + const findDescription = () => wrapper.find('[data-testid="description"]'); + + const mountComponent = propsData => { + wrapper = shallowMount(component, { + propsData, + stubs: { + GlSprintf, + GlModal, + }, + }); + }; + + it('contains a GlModal', () => { + mountComponent(); + expect(findModal().exists()).toBe(true); + }); + + describe('events', () => { + it.each` + glEvent | localEvent + ${'ok'} | ${'confirmDelete'} + ${'cancel'} | ${'cancelDelete'} + `('GlModal $glEvent emits $localEvent', ({ glEvent, localEvent }) => { + mountComponent(); + findModal().vm.$emit(glEvent); + expect(wrapper.emitted(localEvent)).toBeTruthy(); + }); + }); + + describe('methods', () => { + it('show calls gl-modal show', () => { + mountComponent(); + wrapper.vm.show(); + expect(GlModal.methods.show).toHaveBeenCalled(); + }); + }); + + describe('itemsToBeDeleted contains one element', () => { + beforeEach(() => { + mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] }); + }); + it(`has the correct description`, () => { + expect(findDescription().text()).toBe(REMOVE_TAG_CONFIRMATION_TEXT.replace('%{item}', 'foo')); + }); + it('has the correct action', () => { + expect(wrapper.text()).toContain('Remove tag'); + }); + }); + + describe('itemsToBeDeleted contains more than element', () => { + beforeEach(() => { + mountComponent({ itemsToBeDeleted: [{ path: 'foo' }, { path: 'bar' }] }); + }); + it(`has the correct description`, () => { + expect(findDescription().text()).toBe(REMOVE_TAGS_CONFIRMATION_TEXT.replace('%{item}', '2')); + }); + it('has the correct action', () => { + expect(wrapper.text()).toContain('Remove tags'); + }); + }); +}); diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js new file mode 100644 index 00000000000..ad2b8a12ecc --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js @@ -0,0 +1,27 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlSprintf } from '@gitlab/ui'; +import component from '~/registry/explorer/components/details_page/details_header.vue'; +import { DETAILS_PAGE_TITLE } from '~/registry/explorer/constants'; + +describe('Details Header', () => { + let wrapper; + + const mountComponent = propsData => { + wrapper = shallowMount(component, { + propsData, + stubs: { + GlSprintf, + }, + }); + }; + + it('has the correct title ', () => { + mountComponent(); + expect(wrapper.text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE); + }); + + it('shows imageName in the title', () => { + mountComponent({ imageName: 'foo' }); + expect(wrapper.text()).toContain('foo'); + }); +}); diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js index f6beccda9b1..e2b33826503 100644 --- a/spec/frontend/registry/explorer/mock_data.js +++ b/spec/frontend/registry/explorer/mock_data.js @@ -64,7 +64,7 @@ export const imagesListResponse = { export const tagsListResponse = { data: [ { - tag: 'centos6', + name: 'centos6', revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', short_revision: 'b118ab5b0', size: 19, @@ -75,7 +75,7 @@ export const tagsListResponse = { destroy_path: 'path', }, { - tag: 'test-image', + name: 'test-tag', revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4', short_revision: 'b969de599', size: 19, diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js index 93098403a28..6fa4448083a 100644 --- a/spec/frontend/registry/explorer/pages/details_spec.js +++ b/spec/frontend/registry/explorer/pages/details_spec.js @@ -1,24 +1,20 @@ import { mount } from '@vue/test-utils'; -import { GlTable, GlPagination, GlSkeletonLoader, GlAlert, GlLink } from '@gitlab/ui'; +import { GlTable, GlPagination, GlSkeletonLoader } from '@gitlab/ui'; import Tracking from '~/tracking'; import stubChildren from 'helpers/stub_children'; import component from '~/registry/explorer/pages/details.vue'; +import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue'; +import DeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue'; +import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue'; import { createStore } from '~/registry/explorer/stores/'; import { SET_MAIN_LOADING, - SET_INITIAL_STATE, SET_TAGS_LIST_SUCCESS, SET_TAGS_PAGINATION, + SET_INITIAL_STATE, } from '~/registry/explorer/stores/mutation_types/'; -import { - DELETE_TAG_SUCCESS_MESSAGE, - DELETE_TAG_ERROR_MESSAGE, - DELETE_TAGS_SUCCESS_MESSAGE, - DELETE_TAGS_ERROR_MESSAGE, - ADMIN_GARBAGE_COLLECTION_TIP, -} from '~/registry/explorer/constants'; + import { tagsListResponse } from '../mock_data'; -import { GlModal } from '../stubs'; import { $toast } from '../../shared/mocks'; describe('Details Page', () => { @@ -26,7 +22,7 @@ describe('Details Page', () => { let dispatchSpy; let store; - const findDeleteModal = () => wrapper.find(GlModal); + const findDeleteModal = () => wrapper.find(DeleteModal); const findPagination = () => wrapper.find(GlPagination); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' }); @@ -38,7 +34,8 @@ describe('Details Page', () => { const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked')); const findFirsTagColumn = () => wrapper.find('.js-tag-column'); const findFirstTagNameText = () => wrapper.find('[data-testid="rowNameText"]'); - const findAlert = () => wrapper.find(GlAlert); + const findDeleteAlert = () => wrapper.find(DeleteAlert); + const findDetailsHeader = () => wrapper.find(DetailsHeader); const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' })); @@ -47,9 +44,9 @@ describe('Details Page', () => { store, stubs: { ...stubChildren(component), - GlModal, GlSprintf: false, GlTable, + DeleteModal, }, mocks: { $route: { @@ -70,6 +67,7 @@ describe('Details Page', () => { store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data); store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers); jest.spyOn(Tracking, 'event'); + jest.spyOn(DeleteModal.methods, 'show'); }); afterEach(() => { @@ -100,10 +98,6 @@ describe('Details Page', () => { }); describe('table', () => { - beforeEach(() => { - mountComponent(); - }); - it.each([ 'rowCheckbox', 'rowName', @@ -112,6 +106,7 @@ describe('Details Page', () => { 'rowTime', 'singleDeleteButton', ])('%s exist in the table', element => { + mountComponent(); expect(findFirstRowItem(element).exists()).toBe(true); }); @@ -143,16 +138,20 @@ describe('Details Page', () => { }); describe('row checkbox', () => { + beforeEach(() => { + mountComponent(); + }); + it('if selected adds item to selectedItems', () => { findFirstRowItem('rowCheckbox').vm.$emit('change'); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.selectedItems).toEqual([1]); + expect(wrapper.vm.selectedItems).toEqual([store.state.tags[1].name]); expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBeTruthy(); }); }); - it('if deselect remove index from selectedItems', () => { - wrapper.setData({ selectedItems: [1] }); + it('if deselect remove name from selectedItems', () => { + wrapper.setData({ selectedItems: [store.state.tags[1].name] }); findFirstRowItem('rowCheckbox').vm.$emit('change'); return wrapper.vm.$nextTick().then(() => { expect(wrapper.vm.selectedItems.length).toBe(0); @@ -167,14 +166,17 @@ describe('Details Page', () => { }); it('exists', () => { + mountComponent(); expect(findBulkDeleteButton().exists()).toBe(true); }); it('is disabled if no item is selected', () => { + mountComponent(); expect(findBulkDeleteButton().attributes('disabled')).toBe('true'); }); it('is enabled if at least one item is selected', () => { + mountComponent({ data: () => ({ selectedItems: [store.state.tags[0].name] }) }); wrapper.setData({ selectedItems: [1] }); return wrapper.vm.$nextTick().then(() => { expect(findBulkDeleteButton().attributes('disabled')).toBeFalsy(); @@ -183,30 +185,26 @@ describe('Details Page', () => { describe('on click', () => { it('when one item is selected', () => { - wrapper.setData({ selectedItems: [1] }); + mountComponent({ data: () => ({ selectedItems: [store.state.tags[0].name] }) }); + jest.spyOn(wrapper.vm.$refs.deleteModal, 'show'); findBulkDeleteButton().vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(findDeleteModal().html()).toContain( - 'You are about to remove <b>foo</b>. Are you sure?', - ); - expect(GlModal.methods.show).toHaveBeenCalled(); - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { - label: 'registry_tag_delete', - }); + expect(wrapper.vm.itemsToBeDeleted).toEqual([store.state.tags[0]]); + expect(DeleteModal.methods.show).toHaveBeenCalled(); + expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { + label: 'registry_tag_delete', }); }); it('when multiple items are selected', () => { - wrapper.setData({ selectedItems: [0, 1] }); + mountComponent({ + data: () => ({ selectedItems: store.state.tags.map(t => t.name) }), + }); findBulkDeleteButton().vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(findDeleteModal().html()).toContain( - 'You are about to remove <b>2</b> tags. Are you sure?', - ); - expect(GlModal.methods.show).toHaveBeenCalled(); - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { - label: 'bulk_registry_tag_delete', - }); + + expect(wrapper.vm.itemsToBeDeleted).toEqual(tagsListResponse.data); + expect(DeleteModal.methods.show).toHaveBeenCalled(); + expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { + label: 'bulk_registry_tag_delete', }); }); }); @@ -237,14 +235,10 @@ describe('Details Page', () => { findAllDeleteButtons() .at(0) .vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(findDeleteModal().html()).toContain( - 'You are about to remove <b>bar</b>. Are you sure?', - ); - expect(GlModal.methods.show).toHaveBeenCalled(); - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { - label: 'registry_tag_delete', - }); + + expect(DeleteModal.methods.show).toHaveBeenCalled(); + expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { + label: 'registry_tag_delete', }); }); }); @@ -292,6 +286,7 @@ describe('Details Page', () => { let timeCell; beforeEach(() => { + mountComponent(); timeCell = findFirstRowItem('rowTime'); }); @@ -331,176 +326,97 @@ describe('Details Page', () => { }); describe('modal', () => { - beforeEach(() => { - mountComponent(); - }); - it('exists', () => { + mountComponent(); expect(findDeleteModal().exists()).toBe(true); }); - describe('when ok event is emitted', () => { - beforeEach(() => { - dispatchSpy.mockResolvedValue(); - }); - - it('tracks confirm_delete', () => { - const deleteModal = findDeleteModal(); - deleteModal.vm.$emit('ok'); - return wrapper.vm.$nextTick().then(() => { - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'confirm_delete', { - label: 'registry_tag_delete', - }); + describe('cancel event', () => { + it('tracks cancel_delete', () => { + mountComponent(); + findDeleteModal().vm.$emit('cancel'); + expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', { + label: 'registry_tag_delete', }); }); + }); - describe('when only one element is selected', () => { - it('execute the delete and remove selection', () => { - wrapper.setData({ itemsToBeDeleted: [0] }); - findDeleteModal().vm.$emit('ok'); + describe('confirmDelete event', () => { + describe('when one item is selected to be deleted', () => { + const itemsToBeDeleted = [{ name: 'foo' }]; - expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', { - tag: store.state.tags[0], - params: wrapper.vm.$route.params.id, + it('dispatch requestDeleteTag with the right parameters', () => { + mountComponent({ data: () => ({ itemsToBeDeleted }) }); + findDeleteModal().vm.$emit('confirmDelete'); + expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTag', { + tag: itemsToBeDeleted[0], + params: routeId, }); - // itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items - expect(wrapper.vm.itemsToBeDeleted).toEqual([]); - expect(wrapper.vm.selectedItems).toEqual([]); - expect(findCheckedCheckboxes()).toHaveLength(0); + }); + it('remove the deleted item from the selected items', () => { + mountComponent({ data: () => ({ itemsToBeDeleted, selectedItems: ['foo', 'bar'] }) }); + findDeleteModal().vm.$emit('confirmDelete'); + expect(wrapper.vm.selectedItems).toEqual(['bar']); }); }); - describe('when multiple elements are selected', () => { + describe('when more than one item is selected to be deleted', () => { beforeEach(() => { - wrapper.setData({ itemsToBeDeleted: [0, 1] }); + mountComponent({ + data: () => ({ + itemsToBeDeleted: [{ name: 'foo' }, { name: 'bar' }], + selectedItems: ['foo', 'bar'], + }), + }); }); - it('execute the delete and remove selection', () => { - findDeleteModal().vm.$emit('ok'); - - expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', { - ids: store.state.tags.map(t => t.name), - params: wrapper.vm.$route.params.id, + it('dispatch requestDeleteTags with the right parameters', () => { + findDeleteModal().vm.$emit('confirmDelete'); + expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTags', { + ids: ['foo', 'bar'], + params: routeId, }); - // itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items - expect(wrapper.vm.itemsToBeDeleted).toEqual([]); - expect(findCheckedCheckboxes()).toHaveLength(0); + }); + it('clears the selectedItems', () => { + findDeleteModal().vm.$emit('confirmDelete'); + expect(wrapper.vm.selectedItems).toEqual([]); }); }); }); + }); - it('tracks cancel_delete when cancel event is emitted', () => { - const deleteModal = findDeleteModal(); - deleteModal.vm.$emit('cancel'); - return wrapper.vm.$nextTick().then(() => { - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', { - label: 'registry_tag_delete', - }); - }); + describe('Header', () => { + it('exists', () => { + mountComponent(); + expect(findDetailsHeader().exists()).toBe(true); + }); + + it('has the correct props', () => { + mountComponent(); + expect(findDetailsHeader().props()).toEqual({ imageName: 'foo' }); }); }); - describe('Delete alert', () => { + describe('Delete Alert', () => { const config = { - garbageCollectionHelpPagePath: 'foo', + isAdmin: true, + garbageCollectionHelpPagePath: 'baz', }; + const deleteAlertType = 'success_tag'; - describe('when the user is an admin', () => { - beforeEach(() => { - store.commit(SET_INITIAL_STATE, { ...config, isAdmin: true }); - }); - - afterEach(() => { - store.commit(SET_INITIAL_STATE, config); - }); - - describe.each` - deleteType | successTitle | errorTitle - ${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE} - ${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE} - `('behaves correctly on $deleteType', ({ deleteType, successTitle, errorTitle }) => { - describe('when delete is successful', () => { - beforeEach(() => { - dispatchSpy.mockResolvedValue(); - mountComponent(); - return wrapper.vm[deleteType]('foo'); - }); - - it('alert exists', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('alert body contains admin tip', () => { - expect( - findAlert() - .text() - .replace(/\s\s+/gm, ' '), - ).toBe(ADMIN_GARBAGE_COLLECTION_TIP.replace(/%{\w+}/gm, '')); - }); - - it('alert body contains link', () => { - const alertLink = findAlert().find(GlLink); - expect(alertLink.exists()).toBe(true); - expect(alertLink.attributes('href')).toBe(config.garbageCollectionHelpPagePath); - }); - - it('alert title is appropriate', () => { - expect(findAlert().attributes('title')).toBe(successTitle); - }); - }); - - describe('when delete is not successful', () => { - beforeEach(() => { - mountComponent(); - dispatchSpy.mockRejectedValue(); - return wrapper.vm[deleteType]('foo'); - }); + it('exists', () => { + mountComponent(); + expect(findDeleteAlert().exists()).toBe(true); + }); - it('alert exist and text is appropriate', () => { - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe(errorTitle); - }); - }); + it('has the correct props', () => { + store.commit(SET_INITIAL_STATE, { ...config }); + mountComponent({ + data: () => ({ + deleteAlertType, + }), }); + expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType }); }); - - describe.each` - deleteType | successTitle | errorTitle - ${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE} - ${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE} - `( - 'when the user is not an admin alert behaves correctly on $deleteType', - ({ deleteType, successTitle, errorTitle }) => { - beforeEach(() => { - store.commit('SET_INITIAL_STATE', { ...config }); - }); - - describe('when delete is successful', () => { - beforeEach(() => { - dispatchSpy.mockResolvedValue(); - mountComponent(); - return wrapper.vm[deleteType]('foo'); - }); - - it('alert exist and text is appropriate', () => { - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe(successTitle); - }); - }); - - describe('when delete is not successful', () => { - beforeEach(() => { - mountComponent(); - dispatchSpy.mockRejectedValue(); - return wrapper.vm[deleteType]('foo'); - }); - - it('alert exist and text is appropriate', () => { - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe(errorTitle); - }); - }); - }, - ); }); }); diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 10e7d7b9cc7..fe21ab71290 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -211,7 +211,8 @@ describe Gitlab::UsageData, :aggregate_failures do describe '#features_usage_data_ce' do subject { described_class.features_usage_data_ce } - it 'gathers feature usage data' do + it 'gathers feature usage data', :aggregate_failures do + expect(subject[:instance_auto_devops_enabled]).to eq(Gitlab::CurrentSettings.auto_devops_enabled?) expect(subject[:mattermost_enabled]).to eq(Gitlab.config.mattermost.enabled) expect(subject[:signup_enabled]).to eq(Gitlab::CurrentSettings.allow_signup?) expect(subject[:ldap_enabled]).to eq(Gitlab.config.ldap.enabled) diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index fae0c636bdc..c3890c72852 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -3,11 +3,17 @@ require 'spec_helper' describe ObjectStorage::DirectUpload do + let(:region) { 'us-east-1' } + let(:path_style) { false } + let(:use_iam_profile) { false } let(:credentials) do { provider: 'AWS', aws_access_key_id: 'AWS_ACCESS_KEY_ID', - aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY' + aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY', + region: region, + path_style: path_style, + use_iam_profile: use_iam_profile } end @@ -57,6 +63,62 @@ describe ObjectStorage::DirectUpload do describe '#to_hash' do subject { direct_upload.to_hash } + shared_examples 'a valid S3 upload' do + it_behaves_like 'a valid upload' + + it 'sets Workhorse client data' do + expect(subject[:UseWorkhorseClient]).to eq(use_iam_profile) + expect(subject[:RemoteTempObjectID]).to eq(object_name) + + object_store_config = subject[:ObjectStorage] + expect(object_store_config[:Provider]).to eq 'AWS' + + s3_config = object_store_config[:S3Config] + expect(s3_config[:Bucket]).to eq(bucket_name) + expect(s3_config[:Region]).to eq(region) + expect(s3_config[:PathStyle]).to eq(path_style) + expect(s3_config[:UseIamProfile]).to eq(use_iam_profile) + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(use_workhorse_s3_client: false) + end + + it 'does not enable Workhorse client' do + expect(subject[:UseWorkhorseClient]).to be false + end + end + + context 'when V2 signatures are used' do + before do + credentials[:aws_signature_version] = 2 + end + + it 'does not enable Workhorse client' do + expect(subject[:UseWorkhorseClient]).to be false + end + end + + context 'when V4 signatures are used' do + before do + credentials[:aws_signature_version] = 4 + end + + it 'enables the Workhorse client for instance profiles' do + expect(subject[:UseWorkhorseClient]).to eq(use_iam_profile) + end + end + end + + shared_examples 'a valid Google upload' do + it_behaves_like 'a valid upload' + + it 'does not set Workhorse client data' do + expect(subject.keys).not_to include(:UseWorkhorseClient, :RemoteTempObjectID, :ObjectStorage) + end + end + shared_examples 'a valid upload' do it "returns valid structure" do expect(subject).to have_key(:Timeout) @@ -97,6 +159,16 @@ describe ObjectStorage::DirectUpload do end end + shared_examples 'a valid S3 upload without multipart data' do + it_behaves_like 'a valid S3 upload' + it_behaves_like 'a valid upload without multipart data' + end + + shared_examples 'a valid S3 upload with multipart data' do + it_behaves_like 'a valid S3 upload' + it_behaves_like 'a valid upload with multipart data' + end + shared_examples 'a valid upload without multipart data' do it_behaves_like 'a valid upload' @@ -109,13 +181,50 @@ describe ObjectStorage::DirectUpload do context 'when length is known' do let(:has_length) { true } - it_behaves_like 'a valid upload without multipart data' + it_behaves_like 'a valid S3 upload without multipart data' + + context 'when path style is true' do + let(:path_style) { true } + let(:storage_url) { 'https://s3.amazonaws.com/uploads' } + + before do + stub_object_storage_multipart_init(storage_url, "myUpload") + end + + it_behaves_like 'a valid S3 upload without multipart data' + end + + context 'when IAM profile is true' do + let(:use_iam_profile) { true } + let(:iam_credentials_url) { "http://169.254.169.254/latest/meta-data/iam/security-credentials/" } + let(:iam_credentials) do + { + 'AccessKeyId' => 'dummykey', + 'SecretAccessKey' => 'dummysecret', + 'Token' => 'dummytoken', + 'Expiration' => 1.day.from_now.xmlschema + } + end + + before do + stub_request(:get, iam_credentials_url) + .to_return(status: 200, body: "somerole", headers: {}) + stub_request(:get, "#{iam_credentials_url}somerole") + .to_return(status: 200, body: iam_credentials.to_json, headers: {}) + end + + it_behaves_like 'a valid S3 upload without multipart data' + end end context 'when length is unknown' do let(:has_length) { false } - it_behaves_like 'a valid upload with multipart data' do + it_behaves_like 'a valid S3 upload with multipart data' do + before do + stub_object_storage_multipart_init(storage_url, "myUpload") + end + context 'when maximum upload size is 10MB' do let(:maximum_size) { 10.megabyte } @@ -169,12 +278,14 @@ describe ObjectStorage::DirectUpload do context 'when length is known' do let(:has_length) { true } + it_behaves_like 'a valid Google upload' it_behaves_like 'a valid upload without multipart data' end context 'when length is unknown' do let(:has_length) { false } + it_behaves_like 'a valid Google upload' it_behaves_like 'a valid upload without multipart data' end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 4ef6496f78e..14066b1e9d2 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -68,20 +68,13 @@ describe Event do end end - describe 'after_create :track_user_interacted_projects' do + describe 'after_create UserInteractedProject.track' do let(:event) { build(:push_event, project: project, author: project.owner) } it 'passes event to UserInteractedProject.track' do - expect(UserInteractedProject).to receive(:available?).and_return(true) expect(UserInteractedProject).to receive(:track).with(event) event.save end - - it 'does not call UserInteractedProject.track if its not yet available' do - expect(UserInteractedProject).to receive(:available?).and_return(false) - expect(UserInteractedProject).not_to receive(:track) - event.save - end end end diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 33d03bfc0f5..0dfb59cf43a 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -88,33 +88,6 @@ describe InternalId do expect(normalized).to eq((0..seq.size - 1).to_a) end - - context 'with an insufficient schema version' do - before do - described_class.reset_column_information - # Project factory will also call the current_version - expect(ActiveRecord::Migrator).to receive(:current_version).at_least(:once).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1) - end - - let(:init) { double('block') } - - it 'calculates next internal ids on the fly' do - val = rand(1..100) - - expect(init).to receive(:call).with(issue).and_return(val) - expect(subject).to eq(val + 1) - end - - it 'always attempts to generate internal IDs in production mode' do - stub_rails_env('production') - - val = rand(1..100) - generator = double(generate: val) - expect(InternalId::InternalIdGenerator).to receive(:new).and_return(generator) - - expect(subject).to eq(val) - end - end end describe '.reset' do @@ -152,20 +125,6 @@ describe InternalId do described_class.generate_next(issue, scope, usage, init) end end - - context 'with an insufficient schema version' do - let(:value) { 2 } - - before do - described_class.reset_column_information - # Project factory will also call the current_version - expect(ActiveRecord::Migrator).to receive(:current_version).at_least(:once).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1) - end - - it 'does not reset any of the iids' do - expect(subject).to be_falsey - end - end end describe '.track_greatest' do diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb index 86115a61aa7..ecca371ce4e 100644 --- a/spec/models/project_ci_cd_setting_spec.rb +++ b/spec/models/project_ci_cd_setting_spec.rb @@ -3,25 +3,6 @@ require 'spec_helper' describe ProjectCiCdSetting do - describe '.available?' do - before do - described_class.reset_column_information - end - - it 'returns true' do - expect(described_class).to be_available - end - - it 'memoizes the schema version' do - expect(ActiveRecord::Migrator) - .to receive(:current_version) - .and_call_original - .once - - 2.times { described_class.available? } - end - end - describe 'validations' do it 'validates default_git_depth is between 0 and 1000 or nil' do expect(subject).to validate_numericality_of(:default_git_depth) diff --git a/spec/models/user_interacted_project_spec.rb b/spec/models/user_interacted_project_spec.rb index 75386e220f5..83c66bf1969 100644 --- a/spec/models/user_interacted_project_spec.rb +++ b/spec/models/user_interacted_project_spec.rb @@ -44,21 +44,6 @@ describe UserInteractedProject do end end - describe '.available?' do - before do - described_class.instance_variable_set('@available_flag', nil) - end - - it 'checks schema version and properly caches positive result' do - expect(ActiveRecord::Migrator).to receive(:current_version).and_return(described_class::REQUIRED_SCHEMA_VERSION - 1 - rand(1000)) - expect(described_class.available?).to be_falsey - expect(ActiveRecord::Migrator).to receive(:current_version).and_return(described_class::REQUIRED_SCHEMA_VERSION + rand(1000)) - expect(described_class.available?).to be_truthy - expect(ActiveRecord::Migrator).not_to receive(:current_version) - expect(described_class.available?).to be_truthy # cached response - end - end - it { is_expected.to validate_presence_of(:project_id) } it { is_expected.to validate_presence_of(:user_id) } end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 053b5c3a793..6ec63ba61ca 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -833,4 +833,63 @@ describe ProjectPolicy do it { is_expected.to be_disallowed(:create_web_ide_terminal) } end end + + describe 'read_repository_graphs' do + subject { described_class.new(guest, project) } + + before do + allow(subject).to receive(:allowed?).with(:read_repository_graphs).and_call_original + allow(subject).to receive(:allowed?).with(:download_code).and_return(can_download_code) + end + + context 'when user can download_code' do + let(:can_download_code) { true } + + it { is_expected.to be_allowed(:read_repository_graphs) } + end + + context 'when user cannot download_code' do + let(:can_download_code) { false } + + it { is_expected.to be_disallowed(:read_repository_graphs) } + end + end + + describe 'read_build_report_results' do + subject { described_class.new(guest, project) } + + before do + allow(subject).to receive(:allowed?).with(:read_build_report_results).and_call_original + allow(subject).to receive(:allowed?).with(:read_build).and_return(can_read_build) + allow(subject).to receive(:allowed?).with(:read_pipeline).and_return(can_read_pipeline) + end + + context 'when user can read_build and read_pipeline' do + let(:can_read_build) { true } + let(:can_read_pipeline) { true } + + it { is_expected.to be_allowed(:read_build_report_results) } + end + + context 'when user can read_build but cannot read_pipeline' do + let(:can_read_build) { true } + let(:can_read_pipeline) { false } + + it { is_expected.to be_disallowed(:read_build_report_results) } + end + + context 'when user cannot read_build but can read_pipeline' do + let(:can_read_build) { false } + let(:can_read_pipeline) { true } + + it { is_expected.to be_disallowed(:read_build_report_results) } + end + + context 'when user cannot read_build and cannot read_pipeline' do + let(:can_read_build) { false } + let(:can_read_pipeline) { false } + + it { is_expected.to be_disallowed(:read_build_report_results) } + end + end end diff --git a/spec/rubocop/cop/gitlab/bulk_insert_spec.rb b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb new file mode 100644 index 00000000000..937c709218f --- /dev/null +++ b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/gitlab/bulk_insert' + +describe RuboCop::Cop::Gitlab::BulkInsert do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of Gitlab::Database.bulk_insert' do + expect_offense(<<~SOURCE) + Gitlab::Database.bulk_insert('merge_request_diff_files', rows) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `BulkInsertSafe` concern, instead of using `Gitlab::Database.bulk_insert`. See https://docs.gitlab.com/ee/development/insert_into_tables_in_batches.html + SOURCE + end +end diff --git a/spec/services/clusters/applications/prometheus_config_service_spec.rb b/spec/services/clusters/applications/prometheus_config_service_spec.rb index 993a697b543..b9032e665ec 100644 --- a/spec/services/clusters/applications/prometheus_config_service_spec.rb +++ b/spec/services/clusters/applications/prometheus_config_service_spec.rb @@ -90,23 +90,25 @@ describe Clusters::Applications::PrometheusConfigService do create(:prometheus_alert, project: project, environment: production, - prometheus_metric: metric) + prometheus_metric: metric, + operator: PrometheusAlert.operators['gt'], + threshold: 0) end let(:metric) do create(:prometheus_metric, query: query, project: project) end - let(:query) { '%{ci_environment_slug}' } + let(:query) { 'up{environment="{{ci_environment_slug}}"}' } it 'substitutes query variables' do expect(Gitlab::Prometheus::QueryVariables) .to receive(:call) - .with(production) + .with(production, start_time: nil, end_time: nil) .and_call_original expr = groups.dig(0, 'rules', 0, 'expr') - expect(expr).to include(production.name) + expect(expr).to eq("up{environment=\"#{production.slug}\"} > 0.0") end end @@ -127,13 +129,15 @@ describe Clusters::Applications::PrometheusConfigService do end it 'substitutes query variables once per environment' do + allow(Gitlab::Prometheus::QueryVariables).to receive(:call).and_call_original + expect(Gitlab::Prometheus::QueryVariables) .to receive(:call) - .with(production) + .with(production, start_time: nil, end_time: nil) expect(Gitlab::Prometheus::QueryVariables) .to receive(:call) - .with(staging) + .with(staging, start_time: nil, end_time: nil) subject end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index f82a3cee1d9..c791c454d70 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -42,13 +42,11 @@ describe Issuable::BulkUpdateService do let(:issue_no_labels) { create(:issue, project: project) } let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } - let(:labels) { [] } let(:add_labels) { [] } let(:remove_labels) { [] } let(:bulk_update_params) do { - label_ids: labels.map(&:id), add_label_ids: add_labels.map(&:id), remove_label_ids: remove_labels.map(&:id) } @@ -58,27 +56,6 @@ describe Issuable::BulkUpdateService do bulk_update(issues, bulk_update_params) end - context 'when label_ids are passed' do - let(:issues) { [issue_all_labels, issue_no_labels] } - let(:labels) { [bug, regression] } - - it 'updates the labels of all issues passed to the labels passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id))) - end - - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) - end - - context 'when those label IDs are empty' do - let(:labels) { [] } - - it 'updates the issues passed to have no labels' do - expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) - end - end - end - context 'when add_label_ids are passed' do let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } let(:add_labels) { [bug, regression, merge_requests] } @@ -122,69 +99,21 @@ describe Issuable::BulkUpdateService do expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) end end + end - context 'when add_label_ids and label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } - let(:labels) { [merge_requests] } - let(:add_labels) { [regression] } - - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) - end - - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) - end - - it 'does not update issues not passed in' do - expect(issue_no_labels.label_ids).to be_empty - end - end - - context 'when remove_label_ids and label_ids are passed' do - let(:issues) { [issue_no_labels, issue_bug_and_regression] } - let(:labels) { [merge_requests] } - let(:remove_labels) { [regression] } - - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).flat_map(&:label_ids)).not_to include(regression.id) - end - - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).flat_map(&:label_ids)).not_to include(merge_requests.id) - end - - it 'does not update issues not passed in' do - expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) - end - end - - context 'when add_label_ids, remove_label_ids, and label_ids are passed' do - let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } - let(:labels) { [regression] } - let(:add_labels) { [bug] } - let(:remove_labels) { [merge_requests] } - - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) - end + context 'with issuables at a project level' do + let(:parent) { project } - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).flat_map(&:label_ids)).not_to include(merge_requests.id) - end + context 'with unpermitted attributes' do + let(:issues) { create_list(:issue, 2, project: project) } + let(:label) { create(:label, project: project) } - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).flat_map(&:label_ids)).not_to include(regression.id) - end + it 'does not update the issues' do + bulk_update(issues, label_ids: [label.id]) - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + expect(issues.map(&:reload).map(&:label_ids)).not_to include(label.id) end end - end - - context 'with issuables at a project level' do - let(:parent) { project } describe 'close issues' do let(:issues) { create_list(:issue, 2, project: project) } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 80039049bc3..33ae2682d01 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -3,13 +3,13 @@ require 'spec_helper' describe Issues::UpdateService, :mailer do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:group) { create(:group, :public) } - let(:project) { create(:project, :repository, group: group) } - let(:label) { create(:label, project: project) } - let(:label2) { create(:label) } + let_it_be(:user) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:user3) { create(:user) } + let_it_be(:group) { create(:group, :public) } + let_it_be(:project, reload: true) { create(:project, :repository, group: group) } + let_it_be(:label) { create(:label, project: project) } + let_it_be(:label2) { create(:label, project: project) } let(:issue) do create(:issue, title: 'Old title', @@ -19,7 +19,7 @@ describe Issues::UpdateService, :mailer do author: create(:user)) end - before do + before_all do project.add_maintainer(user) project.add_developer(user2) project.add_developer(user3) @@ -669,28 +669,24 @@ describe Issues::UpdateService, :mailer do context 'when add_label_ids and label_ids are passed' do let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } } - it 'ignores the label_ids parameter' do - expect(result.label_ids).not_to include(label.id) + before do + issue.update(labels: [label2]) end - it 'adds the passed labels' do - expect(result.label_ids).to include(label3.id) + it 'replaces the labels with the ones in label_ids and adds those in add_label_ids' do + expect(result.label_ids).to contain_exactly(label.id, label3.id) end end context 'when remove_label_ids and label_ids are passed' do - let(:params) { { label_ids: [], remove_label_ids: [label.id] } } + let(:params) { { label_ids: [label.id, label2.id, label3.id], remove_label_ids: [label.id] } } before do issue.update(labels: [label, label3]) end - it 'ignores the label_ids parameter' do - expect(result.label_ids).not_to be_empty - end - - it 'removes the passed labels' do - expect(result.label_ids).not_to include(label.id) + it 'replaces the labels with the ones in label_ids and removes those in remove_label_ids' do + expect(result.label_ids).to contain_exactly(label2.id, label3.id) end end diff --git a/spec/views/projects/issues/import_csv/_button.html.haml_spec.rb b/spec/views/projects/issues/import_csv/_button.html.haml_spec.rb new file mode 100644 index 00000000000..440edd376e0 --- /dev/null +++ b/spec/views/projects/issues/import_csv/_button.html.haml_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'projects/issues/import_csv/_button' do + include Devise::Test::ControllerHelpers + + context 'when the user does not have edit permissions' do + before do + render + end + + it 'shows a dropdown button to import CSV' do + expect(rendered).to have_text('Import CSV') + end + + it 'does not show a button to import from Jira' do + expect(rendered).not_to have_text('Import from Jira') + end + end + + context 'when the user has edit permissions' do + let(:project) { create(:project) } + let(:current_user) { create(:user, maintainer_projects: [project]) } + + before do + allow(view).to receive(:project_import_jira_path).and_return('import/jira') + allow(view).to receive(:current_user).and_return(current_user) + + assign(:project, project) + + render + end + + it 'shows a dropdown button to import CSV' do + expect(rendered).to have_text('Import CSV') + end + + it 'shows a button to import from Jira' do + expect(rendered).to have_text('Import from Jira') + end + end +end diff --git a/vendor/project_templates/learn_gitlab.tar.gz b/vendor/project_templates/learn_gitlab.tar.gz Binary files differnew file mode 100644 index 00000000000..1199b6dac02 --- /dev/null +++ b/vendor/project_templates/learn_gitlab.tar.gz |