提交 86e1f47c 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 38b39c50
......@@ -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,
});
......
/* 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';
<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))
......
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);
};
<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>
<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>
<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>
......@@ -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,
};
......@@ -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>
......@@ -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 {
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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(
{
......
......@@ -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
......
......@@ -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)
......
......@@ -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'),
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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)
......
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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?
......
......@@ -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
......
......@@ -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
......
......@@ -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] = []
......
......@@ -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
......
......@@ -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
......
......@@ -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)
)
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
- 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)
......@@ -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)
......
---
title: Hide "Import from Jira" option from non-entitled users
merge_request: 32685
author:
type: fixed
---
title: Fix bug with variable substitution in alerts
merge_request: 33772
author:
type: fixed
---
title: Update Auto deploy image to v0.16.1, introducing support for AUTO_DEVOPS_DEPLOY_DEBUG
merge_request: 33799
author:
type: changed
---
title: Add link text to collapsed left sidebar links for screen readers
merge_request: 33866
author:
type: fixed
---
title: Update local IP address and domain name allow list input label
merge_request: 33812
author:
type: changed
---
title: Add whether instance has Auto DevOps enabled to usage ping
merge_request: 33811
author:
type: changed
---
title: Speed up boot time in production
merge_request: 33929
author:
type: performance
---
title: Support Workhorse directly uploading files to S3
merge_request: 29389
author:
type: added
---
title: "Web IDE: Create template files in the folder from which new file request was made"
merge_request: 33585
author: Ashesh Vidyut
type: fixed
......@@ -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
......
......@@ -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"
......
......@@ -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
......
......@@ -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.
......
......@@ -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)
```
......@@ -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
......
......@@ -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:
......
......@@ -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). |
......
......@@ -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
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......
......@@ -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}")
......
......@@ -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?
......
......@@ -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
......
......@@ -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
......
......@@ -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,
......
.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
......
.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
......
......@@ -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 = {
......
......@@ -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
......
......@@ -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.
......
......@@ -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
......
......@@ -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
......
......@@ -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.
......
......@@ -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
......
......@@ -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 },
......
......@@ -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
......
......@@ -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 ""
......
# 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
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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
[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]
1565723794
conaninfo.txt: 2774ebe649804c1cd9430f26ab0ead14
include/hello.h: 8727846905bd09baecf8bdc1edb1f46e
lib/libhello.a: 7f2aaa8b6f3bc316bba59e47b6a0bd43
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"]
1565723790
conanfile.py: 7c042b95312cc4c4ee89199dc51aebf9
<?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>
<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>
{
"_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"
}
{
"_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"
}
<?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>
<?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>
......@@ -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;
......
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);
});
});
});
});
});
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]]);
});
});
});
});
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');
});
});
});
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册